server.ts 2.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  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 async function createOpencodeServer(options?: ServerOptions) {
  11. options = Object.assign(
  12. {
  13. hostname: "127.0.0.1",
  14. port: 4096,
  15. timeout: 5000,
  16. },
  17. options ?? {},
  18. )
  19. const proc = spawn(`opencode`, [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`], {
  20. signal: options.signal,
  21. env: {
  22. ...process.env,
  23. OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}),
  24. },
  25. })
  26. const url = await new Promise<string>((resolve, reject) => {
  27. const id = setTimeout(() => {
  28. reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`))
  29. }, options.timeout)
  30. let output = ""
  31. proc.stdout?.on("data", (chunk) => {
  32. output += chunk.toString()
  33. const lines = output.split("\n")
  34. for (const line of lines) {
  35. if (line.startsWith("opencode server listening")) {
  36. const match = line.match(/on\s+(https?:\/\/[^\s]+)/)
  37. if (!match) {
  38. throw new Error(`Failed to parse server url from output: ${line}`)
  39. }
  40. clearTimeout(id)
  41. resolve(match[1])
  42. return
  43. }
  44. }
  45. })
  46. proc.stderr?.on("data", (chunk) => {
  47. output += chunk.toString()
  48. })
  49. proc.on("exit", (code) => {
  50. clearTimeout(id)
  51. let msg = `Server exited with code ${code}`
  52. if (output.trim()) {
  53. msg += `\nServer output: ${output}`
  54. }
  55. reject(new Error(msg))
  56. })
  57. proc.on("error", (error) => {
  58. clearTimeout(id)
  59. reject(error)
  60. })
  61. if (options.signal) {
  62. options.signal.addEventListener("abort", () => {
  63. clearTimeout(id)
  64. reject(new Error("Aborted"))
  65. })
  66. }
  67. })
  68. return {
  69. url,
  70. close() {
  71. proc.kill()
  72. },
  73. }
  74. }