server.ts 2.7 KB

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