108 lines
3 KiB
TypeScript
108 lines
3 KiB
TypeScript
|
|
/**
|
||
|
|
* 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);
|
||
|
|
}
|