/** * Shared OpenCode client manager. * Reuses an existing server on port 4096; starts one on a random port as fallback. * Tracks spawned servers so they can be cleaned up on process exit. */ import { execSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; const activeServers = new Set<{ close(): void }>(); // Clean up spawned OpenCode servers on process exit function cleanup() { for (const server of activeServers) { try { server.close(); } catch { /* ignore */ } } activeServers.clear(); } process.on('beforeExit', cleanup); process.on('SIGTERM', cleanup); process.on('SIGINT', cleanup); const isWindows = process.platform === 'win32'; /** Cached resolved binary path */ let _resolvedBinary: string | undefined | null = null; /** Resolve the opencode binary, with caching. */ function resolveOpencodeBinary(): string | undefined { if (_resolvedBinary !== null) return _resolvedBinary ?? undefined; // PATH lookup try { const cmd = isWindows ? 'where opencode 2>nul' : 'which opencode 2>/dev/null'; const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }) .trim() .split(/\r?\n/)[0] ?.trim(); if (result && existsSync(result)) { _resolvedBinary = result; return result; } } catch { /* not on PATH */ } // Common install locations const home = homedir(); const candidates = isWindows ? [ join(process.env.APPDATA || '', 'npm', 'opencode.cmd'), join(process.env.NVM_SYMLINK || '', 'opencode.cmd'), join(process.env.FNM_MULTISHELL_PATH || '', 'opencode.cmd'), join(home, 'scoop', 'shims', 'opencode.exe'), ] : [ join(home, '.opencode', 'bin', 'opencode'), join(home, '.npm-global', 'bin', 'opencode'), '/usr/local/bin/opencode', '/opt/homebrew/bin/opencode', join(home, '.local', 'bin', 'opencode'), ]; for (const c of candidates) { if (c && existsSync(c)) { _resolvedBinary = c; return c; } } _resolvedBinary = undefined; return undefined; } export async function getOpencodeClient(binaryPath?: string) { const { createOpencodeClient, createOpencode } = await import('../opencode/index'); // Try connecting to an existing server first try { const client = createOpencodeClient(); await client.config.providers(); // probe return { client, server: undefined }; } catch { // No running server — start a temporary one on a random port const resolvedPath = binaryPath ?? resolveOpencodeBinary(); const timeout = isWindows ? 15_000 : 5000; const oc = await createOpencode({ port: 0, binaryPath: resolvedPath, timeout }); activeServers.add(oc.server); return { client: oc.client, server: oc.server }; } } export function releaseOpencodeServer(server: { close(): void } | undefined) { if (!server) return; try { server.close(); } catch { /* ignore */ } activeServers.delete(server); }