File size: 3,128 Bytes
89a2873 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 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)
},
}
}
|