(opts: SpawnOptions)
| 218 | } |
| 219 | |
| 220 | export async function spawnOpencode(opts: SpawnOptions): Promise<SpawnedOpencode> { |
| 221 | const env = createIsolatedEnv(); |
| 222 | const port = opts.port ?? (await pickFreePort()); |
| 223 | |
| 224 | writeConfigs(env, opts.mockProviderURL, opts); |
| 225 | |
| 226 | // Explicitly strip any inherited OPENCODE_SERVER_PASSWORD from the parent shell — |
| 227 | // our tests run unsecured on a random localhost port, and inherited auth would |
| 228 | // force every SDK request to carry Basic auth headers we don't set. |
| 229 | // Also strip NODE_ENV=test: Bun's test runner sets it automatically and the |
| 230 | // plugin's logger (src/shared/logger.ts) silences all output when NODE_ENV=test. |
| 231 | // We want the subprocess to behave like a real install, so the log file gets |
| 232 | // populated normally for diagnostics. |
| 233 | const childEnv: Record<string, string> = {}; |
| 234 | for (const [key, value] of Object.entries(process.env)) { |
| 235 | if (value === undefined) continue; |
| 236 | if (key === "OPENCODE_SERVER_PASSWORD") continue; |
| 237 | if (key === "OPENCODE_SERVER_USERNAME") continue; |
| 238 | if (key === "NODE_ENV") continue; |
| 239 | childEnv[key] = value; |
| 240 | } |
| 241 | childEnv.OPENCODE_CONFIG_DIR = env.configDir; |
| 242 | childEnv.XDG_CONFIG_HOME = env.configDir; |
| 243 | childEnv.XDG_DATA_HOME = env.dataDir; |
| 244 | childEnv.XDG_CACHE_HOME = env.cacheDir; |
| 245 | // Ensure anthropic doesn't bail for missing env vars — we use a fake key. |
| 246 | childEnv.ANTHROPIC_API_KEY = "test-key-not-real"; |
| 247 | |
| 248 | // Bind to 0.0.0.0 (all interfaces) instead of 127.0.0.1 — empirically on |
| 249 | // GitHub-hosted runners, opencode binding to 127.0.0.1 sometimes results |
| 250 | // in Bun's `fetch()` timing out even though `curl` succeeds. Binding all |
| 251 | // interfaces removes any loopback-specific stack-resolution edge case |
| 252 | // (IPv4-only AF_INET vs IPv4-mapped IPv6, AF_UNSPEC name resolution, etc.). |
| 253 | // Clients still connect to `127.0.0.1:${port}` — only the listen socket |
| 254 | // changes. Safe locally too: process is short-lived, port is random. |
| 255 | const child: ChildProcess = spawn( |
| 256 | "opencode", |
| 257 | ["serve", "--port", String(port), "--hostname", "0.0.0.0"], |
| 258 | { |
| 259 | cwd: env.workdir, |
| 260 | env: childEnv, |
| 261 | stdio: ["ignore", "pipe", "pipe"], |
| 262 | }, |
| 263 | ); |
| 264 | |
| 265 | let stdoutBuf = ""; |
| 266 | let stderrBuf = ""; |
| 267 | child.stdout?.on("data", (chunk: Buffer) => { |
| 268 | stdoutBuf += chunk.toString(); |
| 269 | }); |
| 270 | child.stderr?.on("data", (chunk: Buffer) => { |
| 271 | stderrBuf += chunk.toString(); |
| 272 | }); |
| 273 | |
| 274 | const url = `http://127.0.0.1:${port}`; |
| 275 | try { |
| 276 | await waitForReady(url); |
| 277 | } catch (err) { |
no test coverage detected