server.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import launch from "cross-spawn"
  2. import { type Config } from "./gen/types.gen.js"
  3. import { stop, bindAbort } from "./process.js"
  4. export type ServerOptions = {
  5. hostname?: string
  6. port?: number
  7. signal?: AbortSignal
  8. timeout?: number
  9. config?: Config
  10. }
  11. export type TuiOptions = {
  12. project?: string
  13. model?: string
  14. session?: string
  15. agent?: string
  16. signal?: AbortSignal
  17. config?: Config
  18. }
  19. export async function createOpencodeServer(options?: ServerOptions) {
  20. options = Object.assign(
  21. {
  22. hostname: "127.0.0.1",
  23. port: 4096,
  24. timeout: 5000,
  25. },
  26. options ?? {},
  27. )
  28. const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`]
  29. if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`)
  30. const proc = launch(`opencode`, args, {
  31. env: {
  32. ...process.env,
  33. OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}),
  34. },
  35. })
  36. let clear = () => {}
  37. const url = await new Promise<string>((resolve, reject) => {
  38. const id = setTimeout(() => {
  39. clear()
  40. stop(proc)
  41. reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`))
  42. }, options.timeout)
  43. let output = ""
  44. let resolved = false
  45. proc.stdout?.on("data", (chunk) => {
  46. if (resolved) return
  47. output += chunk.toString()
  48. const lines = output.split("\n")
  49. for (const line of lines) {
  50. if (line.startsWith("opencode server listening")) {
  51. const match = line.match(/on\s+(https?:\/\/[^\s]+)/)
  52. if (!match) {
  53. clear()
  54. stop(proc)
  55. clearTimeout(id)
  56. reject(new Error(`Failed to parse server url from output: ${line}`))
  57. return
  58. }
  59. clearTimeout(id)
  60. resolved = true
  61. resolve(match[1]!)
  62. return
  63. }
  64. }
  65. })
  66. proc.stderr?.on("data", (chunk) => {
  67. output += chunk.toString()
  68. })
  69. proc.on("exit", (code) => {
  70. clearTimeout(id)
  71. let msg = `Server exited with code ${code}`
  72. if (output.trim()) {
  73. msg += `\nServer output: ${output}`
  74. }
  75. reject(new Error(msg))
  76. })
  77. proc.on("error", (error) => {
  78. clearTimeout(id)
  79. reject(error)
  80. })
  81. clear = bindAbort(proc, options.signal, () => {
  82. clearTimeout(id)
  83. reject(options.signal?.reason)
  84. })
  85. })
  86. return {
  87. url,
  88. close() {
  89. clear()
  90. stop(proc)
  91. },
  92. }
  93. }
  94. export function createOpencodeTui(options?: TuiOptions) {
  95. const args = []
  96. if (options?.project) {
  97. args.push(`--project=${options.project}`)
  98. }
  99. if (options?.model) {
  100. args.push(`--model=${options.model}`)
  101. }
  102. if (options?.session) {
  103. args.push(`--session=${options.session}`)
  104. }
  105. if (options?.agent) {
  106. args.push(`--agent=${options.agent}`)
  107. }
  108. const proc = launch(`opencode`, args, {
  109. stdio: "inherit",
  110. env: {
  111. ...process.env,
  112. OPENCODE_CONFIG_CONTENT: JSON.stringify(options?.config ?? {}),
  113. },
  114. })
  115. const clear = bindAbort(proc, options?.signal)
  116. return {
  117. close() {
  118. clear()
  119. stop(proc)
  120. },
  121. }
  122. }