| import launch from "cross-spawn" |
| import { type Config } from "./gen/types.gen.js" |
| import { stop, bindAbort } from "./process.js" |
|
|
| export type ServerOptions = { |
| hostname?: string |
| port?: number |
| signal?: AbortSignal |
| timeout?: number |
| config?: Config |
| } |
|
|
| export type TuiOptions = { |
| project?: string |
| model?: string |
| session?: string |
| agent?: string |
| signal?: AbortSignal |
| config?: Config |
| } |
|
|
| export async function createOpencodeServer(options?: ServerOptions) { |
| options = Object.assign( |
| { |
| hostname: "127.0.0.1", |
| port: 4096, |
| timeout: 5000, |
| }, |
| options ?? {}, |
| ) |
|
|
| const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`] |
| if (options.config?.logLevel) args.push(`--log-level=${options.config.logLevel}`) |
|
|
| const proc = launch(`opencode`, args, { |
| env: { |
| ...process.env, |
| OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}), |
| }, |
| }) |
| let clear = () => {} |
|
|
| const url = await new Promise<string>((resolve, reject) => { |
| const id = setTimeout(() => { |
| clear() |
| stop(proc) |
| reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`)) |
| }, options.timeout) |
| let output = "" |
| let resolved = false |
| proc.stdout?.on("data", (chunk) => { |
| if (resolved) return |
| output += chunk.toString() |
| const lines = output.split("\n") |
| for (const line of lines) { |
| if (line.startsWith("opencode server listening")) { |
| const match = line.match(/on\s+(https?:\/\/[^\s]+)/) |
| if (!match) { |
| clear() |
| stop(proc) |
| clearTimeout(id) |
| reject(new Error(`Failed to parse server url from output: ${line}`)) |
| return |
| } |
| clearTimeout(id) |
| resolved = true |
| resolve(match[1]!) |
| return |
| } |
| } |
| }) |
| proc.stderr?.on("data", (chunk) => { |
| output += chunk.toString() |
| }) |
| proc.on("exit", (code) => { |
| clearTimeout(id) |
| let msg = `Server exited with code ${code}` |
| if (output.trim()) { |
| msg += `\nServer output: ${output}` |
| } |
| reject(new Error(msg)) |
| }) |
| proc.on("error", (error) => { |
| clearTimeout(id) |
| reject(error) |
| }) |
| clear = bindAbort(proc, options.signal, () => { |
| clearTimeout(id) |
| reject(options.signal?.reason) |
| }) |
| }) |
|
|
| return { |
| url, |
| close() { |
| clear() |
| stop(proc) |
| }, |
| } |
| } |
|
|
| export function createOpencodeTui(options?: TuiOptions) { |
| const args = [] |
|
|
| if (options?.project) { |
| args.push(`--project=${options.project}`) |
| } |
| if (options?.model) { |
| args.push(`--model=${options.model}`) |
| } |
| if (options?.session) { |
| args.push(`--session=${options.session}`) |
| } |
| if (options?.agent) { |
| args.push(`--agent=${options.agent}`) |
| } |
|
|
| const proc = launch(`opencode`, args, { |
| stdio: "inherit", |
| env: { |
| ...process.env, |
| OPENCODE_CONFIG_CONTENT: JSON.stringify(options?.config ?? {}), |
| }, |
| }) |
|
|
| const clear = bindAbort(proc, options?.signal) |
|
|
| return { |
| close() { |
| clear() |
| stop(proc) |
| }, |
| } |
| } |
|
|