import { execFileSync, spawn, type ChildProcess } from "node:child_process";import * as FS from "node:fs";import { createServer, type AddressInfo } from "node:net";import * as OS from "node:os";import * as Path from "node:path";import { pathToFileURL } from "node:url"; import type { ChatAttachment, ModelCapabilities, ProviderApprovalDecision, RuntimeMode, ServerProviderModel,} from "@t3tools/contracts";import { createOpencodeClient, type Agent, type FilePartInput, type OpencodeClient, type PermissionRuleset, type ProviderListResponse, type QuestionAnswer, type QuestionRequest,} from "@opencode-ai/sdk/v2"; const OPENCODE_SERVER_READY_PREFIX = "opencode server listening";const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000;const DEFAULT_HOSTNAME = "127.0.0.1"; const OPENAI_VARIANTS = ["none", "minimal", "low", "medium", "high", "xhigh"];const ANTHROPIC_VARIANTS = ["high", "max"];const GOOGLE_VARIANTS = ["low", "high"]; export const DEFAULT_OPENCODE_MODEL_CAPABILITIES: ModelCapabilities = { reasoningEffortLevels: [], supportsFastMode: false, supportsThinkingToggle: false, contextWindowOptions: [], promptInjectedEffortLevels: [],}; export interface OpenCodeServerProcess { readonly url: string; readonly process: ChildProcess; close(): void;} export interface OpenCodeServerConnection { readonly url: string; readonly process: ChildProcess | null; readonly external: boolean; close(): void;} function buildOpenCodeBasicAuthorizationHeader(password: string): string { return `Basic ${Buffer.from(`opencode:${password}`, "utf8").toString("base64")}`;} export interface OpenCodeCommandResult { readonly stdout: string; readonly stderr: string; readonly code: number;} export interface OpenCodeInventory { readonly providerList: ProviderListResponse; readonly agents: ReadonlyArray<Agent>;} export interface ParsedOpenCodeModelSlug { readonly providerID: string; readonly modelID: string;} function titleCaseSlug(value: string): string { return value .split(/[-_/]+/) .filter((segment) => segment.length > 0) .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(" ");} function parseServerUrlFromOutput(output: string): string | null { for (const line of output.split("\n")) { if (!line.startsWith(OPENCODE_SERVER_READY_PREFIX)) { continue; } const match = line.match(/on\s+(https?:\/\/[^\s]+)/); return match?.[1] ?? null; } return null;} function isPrimaryAgent(agent: Agent): boolean { return !agent.hidden && (agent.mode === "primary" || agent.mode === "all");} function inferVariantValues(providerID: string): ReadonlyArray<string> { if (providerID === "anthropic") { return ANTHROPIC_VARIANTS; } if (providerID === "openai" || providerID === "opencode") { return OPENAI_VARIANTS; } if (providerID.startsWith("google")) { return GOOGLE_VARIANTS; } return [];} function inferDefaultVariant( providerID: string, variants: ReadonlyArray<string>,): string | undefined { if (variants.length === 1) { return variants[0]; } if (providerID === "anthropic" || providerID.startsWith("google")) { return variants.includes("high") ? "high" : undefined; } if (providerID === "openai" || providerID === "opencode") { return variants.includes("medium") ? "medium" : variants.includes("high") ? "high" : undefined; } return undefined;} function buildVariantOptions( providerID: string, model: ProviderListResponse["all"][number]["models"][string],) { const variantValues = Object.keys(model.variants ?? {}); const resolvedValues = variantValues.length > 0 ? variantValues : [...inferVariantValues(providerID)]; const defaultVariant = inferDefaultVariant(providerID, resolvedValues); return resolvedValues.map((value) => { const option: { value: string; label: string; isDefault?: boolean } = { value, label: titleCaseSlug(value), }; if (defaultVariant === value) { option.isDefault = true; } return option; });} function buildAgentOptions(agents: ReadonlyArray<Agent>) { const primaryAgents = agents.filter(isPrimaryAgent); const defaultAgent = primaryAgents.find((agent) => agent.name === "build")?.name ?? primaryAgents[0]?.name ?? undefined; return primaryAgents.map((agent) => { const option: { value: string; label: string; isDefault?: boolean } = { value: agent.name, label: titleCaseSlug(agent.name), }; if (defaultAgent === agent.name) { option.isDefault = true; } return option; });} function openCodeCapabilitiesForModel(input: { readonly providerID: string; readonly model: ProviderListResponse["all"][number]["models"][string]; readonly agents: ReadonlyArray<Agent>;}): ModelCapabilities { const variantOptions = buildVariantOptions(input.providerID, input.model); const agentOptions = buildAgentOptions(input.agents); return { ...DEFAULT_OPENCODE_MODEL_CAPABILITIES, ...(variantOptions.length > 0 ? { variantOptions } : {}), ...(agentOptions.length > 0 ? { agentOptions } : {}), };} export function parseOpenCodeModelSlug( slug: string | null | undefined,): ParsedOpenCodeModelSlug | null { if (typeof slug !== "string") { return null; } const trimmed = slug.trim(); const separator = trimmed.indexOf("/"); if (separator <= 0 || separator === trimmed.length - 1) { return null; } return { providerID: trimmed.slice(0, separator), modelID: trimmed.slice(separator + 1), };} export function toOpenCodeModelSlug(providerID: string, modelID: string): string { return `${providerID}/${modelID}`;} export function openCodeQuestionId( index: number, question: QuestionRequest["questions"][number],): string { const header = question.header .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "-"); return header.length > 0 ? `question-${index}-${header}` : `question-${index}`;} export function toOpenCodeFileParts(input: { readonly attachments: ReadonlyArray<ChatAttachment> | undefined; readonly resolveAttachmentPath: (attachment: ChatAttachment) => string | null;}): Array<FilePartInput> { const parts: Array<FilePartInput> = []; for (const attachment of input.attachments ?? []) { const attachmentPath = input.resolveAttachmentPath(attachment); if (!attachmentPath) { continue; } parts.push({ type: "file", mime: attachment.mimeType, filename: attachment.name, url: pathToFileURL(attachmentPath).href, }); } return parts;} export function buildOpenCodePermissionRules(runtimeMode: RuntimeMode): PermissionRuleset { if (runtimeMode === "full-access") { return [{ permission: "*", pattern: "*", action: "allow" }]; } return [ { permission: "*", pattern: "*", action: "ask" }, { permission: "bash", pattern: "*", action: "ask" }, { permission: "edit", pattern: "*", action: "ask" }, { permission: "webfetch", pattern: "*", action: "ask" }, { permission: "websearch", pattern: "*", action: "ask" }, { permission: "codesearch", pattern: "*", action: "ask" }, { permission: "external_directory", pattern: "*", action: "ask" }, { permission: "doom_loop", pattern: "*", action: "ask" }, { permission: "question", pattern: "*", action: "allow" }, ];} export function toOpenCodePermissionReply( decision: ProviderApprovalDecision,): "once" | "always" | "reject" { switch (decision) { case "accept": return "once"; case "acceptForSession": return "always"; case "decline": case "cancel": default: return "reject"; }} export function toOpenCodeQuestionAnswers( request: QuestionRequest, answers: Record<string, unknown>,): Array<QuestionAnswer> { return request.questions.map((question, index) => { const raw = answers[openCodeQuestionId(index, question)] ?? answers[question.header] ?? answers[question.question]; if (Array.isArray(raw)) { return raw.filter((value): value is string => typeof value === "string"); } if (typeof raw === "string") { return raw.trim().length > 0 ? [raw] : []; } return []; });} export async function findAvailablePort(): Promise<number> { const server = createServer(); await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(0, DEFAULT_HOSTNAME, () => resolve()); }); const address = server.address() as AddressInfo; const port = address.port; await new Promise<void>((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); return port;} export function resolveOpenCodeBinaryPath(binaryPath: string): string { if (Path.isAbsolute(binaryPath)) { return binaryPath; } return execFileSync("which", [binaryPath], { encoding: "utf8", timeout: 3_000, }).trim();} export function detectMacosSigkillHint(binaryPath: string): string | null { try { // Check for quarantine xattr first. const resolvedPath = resolveOpenCodeBinaryPath(binaryPath); const xattr = execFileSync("xattr", ["-l", resolvedPath], { encoding: "utf8", timeout: 3_000, }); if (xattr.includes("com.apple.quarantine")) { return ( `macOS quarantine is blocking the OpenCode binary. ` + `Run: xattr -d com.apple.quarantine ${resolvedPath}` ); } // Look for a recent crash report with the termination reason. const crashDir = Path.join(OS.homedir(), "Library/Logs/DiagnosticReports"); const binaryName = Path.basename(resolvedPath); const recentReports = FS.readdirSync(crashDir) .filter((f) => f.startsWith(binaryName) && f.endsWith(".ips")) .toSorted() .toReversed() .slice(0, 1); for (const report of recentReports) { const content = FS.readFileSync(Path.join(crashDir, report), "utf8"); if (content.includes('"namespace":"CODESIGNING"')) { return ( "macOS killed the process due to an invalid code signature. " + "The binary may be corrupted — try reinstalling OpenCode." ); } } } catch { // Best-effort detection — don't fail the original error path. } return null;} export async function startOpenCodeServerProcess(input: { readonly binaryPath: string; readonly port?: number; readonly hostname?: string; readonly timeoutMs?: number;}): Promise<OpenCodeServerProcess> { const hostname = input.hostname ?? DEFAULT_HOSTNAME; const port = input.port ?? (await findAvailablePort()); const timeoutMs = input.timeoutMs ?? DEFAULT_OPENCODE_SERVER_TIMEOUT_MS; const args = ["serve", `--hostname=${hostname}`, `--port=${port}`]; const child = spawn(input.binaryPath, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, OPENCODE_CONFIG_CONTENT: JSON.stringify({}), }, }); child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); let stdout = ""; let stderr = ""; let closed = false; const close = () => { if (closed) { return; } closed = true; child.kill(); }; const url = await new Promise<string>((resolve, reject) => { const timeout = setTimeout(() => { close(); reject(new Error(`Timed out waiting for OpenCode server start after ${timeoutMs}ms.`)); }, timeoutMs); const cleanup = () => { clearTimeout(timeout); child.stdout.off("data", onStdout); child.stderr.off("data", onStderr); child.off("error", onError); child.off("close", onClose); }; const onStdout = (chunk: string) => { stdout += chunk; const parsed = parseServerUrlFromOutput(stdout); if (!parsed) { return; } cleanup(); resolve(parsed); }; const onStderr = (chunk: string) => { stderr += chunk; }; const onError = (error: Error) => { cleanup(); reject(error); }; const onClose = (code: number | null, signal: NodeJS.Signals | null) => { cleanup(); const exitReason = signal ? `signal: ${signal}` : `code: ${code ?? "unknown"}`; const hint = signal === "SIGKILL" && process.platform === "darwin" ? detectMacosSigkillHint(input.binaryPath) : null; reject( new Error( [ `OpenCode server exited before startup completed (${exitReason}).`, hint, stdout.trim() ? `stdout:\n${stdout.trim()}` : null, stderr.trim() ? `stderr:\n${stderr.trim()}` : null, ] .filter(Boolean) .join("\n\n"), ), ); }; child.stdout.on("data", onStdout); child.stderr.on("data", onStderr); child.once("error", onError); child.once("close", onClose); }); return { url, process: child, close, };} export async function connectToOpenCodeServer(input: { readonly binaryPath: string; readonly serverUrl?: string | null; readonly port?: number; readonly hostname?: string; readonly timeoutMs?: number;}): Promise<OpenCodeServerConnection> { const serverUrl = input.serverUrl?.trim(); if (serverUrl) { return { url: serverUrl, process: null, external: true, close() {}, }; } const server = await startOpenCodeServerProcess({ binaryPath: input.binaryPath, ...(input.port !== undefined ? { port: input.port } : {}), ...(input.hostname !== undefined ? { hostname: input.hostname } : {}), ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}), }); return { url: server.url, process: server.process, external: false, close: () => server.close(), };} export async function runOpenCodeCommand(input: { readonly binaryPath: string; readonly args: ReadonlyArray<string>;}): Promise<OpenCodeCommandResult> { const child = spawn(input.binaryPath, [...input.args], { stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32", env: process.env, }); child.stdout?.setEncoding("utf8"); child.stderr?.setEncoding("utf8"); const stdoutChunks: Array<string> = []; const stderrChunks: Array<string> = []; child.stdout?.on("data", (chunk: string) => stdoutChunks.push(chunk)); child.stderr?.on("data", (chunk: string) => stderrChunks.push(chunk));Lines 1–500 of 574. Load more
import { execFileSync, spawn, type ChildProcess } from "node:child_process"; import * as FS from "node:fs"; import { createServer, type AddressInfo } from "node:net"; import * as OS from "node:os"; import * as Path from "node:path"; import { pathToFileURL } from "node:url"; import type { ChatAttachment, ModelCapabilities, ProviderApprovalDecision, RuntimeMode, ServerProviderModel, } from "@t3tools/contracts"; import { createOpencodeClient, type Agent, type FilePartInput, type OpencodeClient, type PermissionRuleset, type ProviderListResponse, type QuestionAnswer, type QuestionRequest, } from "@opencode-ai/sdk/v2"; const OPENCODE_SERVER_READY_PREFIX = "opencode server listening"; const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000; const DEFAULT_HOSTNAME = "127.0.0.1"; const OPENAI_VARIANTS = ["none", "minimal", "low", "medium", "high", "xhigh"]; const ANTHROPIC_VARIANTS = ["high", "max"]; const GOOGLE_VARIANTS = ["low", "high"]; export const DEFAULT_OPENCODE_MODEL_CAPABILITIES: ModelCapabilities = { reasoningEffortLevels: [], supportsFastMode: false, supportsThinkingToggle: false, contextWindowOptions: [], promptInjectedEffortLevels: [], }; export interface OpenCodeServerProcess { readonly url: string; readonly process: ChildProcess; close(): void; } export interface OpenCodeServerConnection { readonly url: string; readonly process: ChildProcess | null; readonly external: boolean; close(): void; } function buildOpenCodeBasicAuthorizationHeader(password: string): string { return `Basic ${Buffer.from(`opencode:${password}`, "utf8").toString("base64")}`; } export interface OpenCodeCommandResult { readonly stdout: string; readonly stderr: string; readonly code: number; } export interface OpenCodeInventory { readonly providerList: ProviderListResponse; readonly agents: ReadonlyArray<Agent>; } export interface ParsedOpenCodeModelSlug { readonly providerID: string; readonly modelID: string; } function titleCaseSlug(value: string): string { return value .split(/[-_/]+/) .filter((segment) => segment.length > 0) .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(" "); } function parseServerUrlFromOutput(output: string): string | null { for (const line of output.split("\n")) { if (!line.startsWith(OPENCODE_SERVER_READY_PREFIX)) { continue; } const match = line.match(/on\s+(https?:\/\/[^\s]+)/); return match?.[1] ?? null; } return null; } function isPrimaryAgent(agent: Agent): boolean { return !agent.hidden && (agent.mode === "primary" || agent.mode === "all"); } function inferVariantValues(providerID: string): ReadonlyArray<string> { if (providerID === "anthropic") { return ANTHROPIC_VARIANTS; } if (providerID === "openai" || providerID === "opencode") { return OPENAI_VARIANTS; } if (providerID.startsWith("google")) { return GOOGLE_VARIANTS; } return []; } function inferDefaultVariant( providerID: string, variants: ReadonlyArray<string>, ): string | undefined { if (variants.length === 1) { return variants[0]; } if (providerID === "anthropic" || providerID.startsWith("google")) { return variants.includes("high") ? "high" : undefined; } if (providerID === "openai" || providerID === "opencode") { return variants.includes("medium") ? "medium" : variants.includes("high") ? "high" : undefined; } return undefined; } function buildVariantOptions( providerID: string, model: ProviderListResponse["all"][number]["models"][string], ) { const variantValues = Object.keys(model.variants ?? {}); const resolvedValues = variantValues.length > 0 ? variantValues : [...inferVariantValues(providerID)]; const defaultVariant = inferDefaultVariant(providerID, resolvedValues); return resolvedValues.map((value) => { const option: { value: string; label: string; isDefault?: boolean } = { value, label: titleCaseSlug(value), }; if (defaultVariant === value) { option.isDefault = true; } return option; }); } function buildAgentOptions(agents: ReadonlyArray<Agent>) { const primaryAgents = agents.filter(isPrimaryAgent); const defaultAgent = primaryAgents.find((agent) => agent.name === "build")?.name ?? primaryAgents[0]?.name ?? undefined; return primaryAgents.map((agent) => { const option: { value: string; label: string; isDefault?: boolean } = { value: agent.name, label: titleCaseSlug(agent.name), }; if (defaultAgent === agent.name) { option.isDefault = true; } return option; }); } function openCodeCapabilitiesForModel(input: { readonly providerID: string; readonly model: ProviderListResponse["all"][number]["models"][string]; readonly agents: ReadonlyArray<Agent>; }): ModelCapabilities { const variantOptions = buildVariantOptions(input.providerID, input.model); const agentOptions = buildAgentOptions(input.agents); return { ...DEFAULT_OPENCODE_MODEL_CAPABILITIES, ...(variantOptions.length > 0 ? { variantOptions } : {}), ...(agentOptions.length > 0 ? { agentOptions } : {}), }; } export function parseOpenCodeModelSlug( slug: string | null | undefined, ): ParsedOpenCodeModelSlug | null { if (typeof slug !== "string") { return null; } const trimmed = slug.trim(); const separator = trimmed.indexOf("/"); if (separator <= 0 || separator === trimmed.length - 1) { return null; } return { providerID: trimmed.slice(0, separator), modelID: trimmed.slice(separator + 1), }; } export function toOpenCodeModelSlug(providerID: string, modelID: string): string { return `${providerID}/${modelID}`; } export function openCodeQuestionId( index: number, question: QuestionRequest["questions"][number], ): string { const header = question.header .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "-"); return header.length > 0 ? `question-${index}-${header}` : `question-${index}`; } export function toOpenCodeFileParts(input: { readonly attachments: ReadonlyArray<ChatAttachment> | undefined; readonly resolveAttachmentPath: (attachment: ChatAttachment) => string | null; }): Array<FilePartInput> { const parts: Array<FilePartInput> = []; for (const attachment of input.attachments ?? []) { const attachmentPath = input.resolveAttachmentPath(attachment); if (!attachmentPath) { continue; } parts.push({ type: "file", mime: attachment.mimeType, filename: attachment.name, url: pathToFileURL(attachmentPath).href, }); } return parts; } export function buildOpenCodePermissionRules(runtimeMode: RuntimeMode): PermissionRuleset { if (runtimeMode === "full-access") { return [{ permission: "*", pattern: "*", action: "allow" }]; } return [ { permission: "*", pattern: "*", action: "ask" }, { permission: "bash", pattern: "*", action: "ask" }, { permission: "edit", pattern: "*", action: "ask" }, { permission: "webfetch", pattern: "*", action: "ask" }, { permission: "websearch", pattern: "*", action: "ask" }, { permission: "codesearch", pattern: "*", action: "ask" }, { permission: "external_directory", pattern: "*", action: "ask" }, { permission: "doom_loop", pattern: "*", action: "ask" }, { permission: "question", pattern: "*", action: "allow" }, ]; } export function toOpenCodePermissionReply( decision: ProviderApprovalDecision, ): "once" | "always" | "reject" { switch (decision) { case "accept": return "once"; case "acceptForSession": return "always"; case "decline": case "cancel": default: return "reject"; } } export function toOpenCodeQuestionAnswers( request: QuestionRequest, answers: Record<string, unknown>, ): Array<QuestionAnswer> { return request.questions.map((question, index) => { const raw = answers[openCodeQuestionId(index, question)] ?? answers[question.header] ?? answers[question.question]; if (Array.isArray(raw)) { return raw.filter((value): value is string => typeof value === "string"); } if (typeof raw === "string") { return raw.trim().length > 0 ? [raw] : []; } return []; }); } export async function findAvailablePort(): Promise<number> { const server = createServer(); await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(0, DEFAULT_HOSTNAME, () => resolve()); }); const address = server.address() as AddressInfo; const port = address.port; await new Promise<void>((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); return port; } export function resolveOpenCodeBinaryPath(binaryPath: string): string { if (Path.isAbsolute(binaryPath)) { return binaryPath; } return execFileSync("which", [binaryPath], { encoding: "utf8", timeout: 3_000, }).trim(); } export function detectMacosSigkillHint(binaryPath: string): string | null { try { // Check for quarantine xattr first. const resolvedPath = resolveOpenCodeBinaryPath(binaryPath); const xattr = execFileSync("xattr", ["-l", resolvedPath], { encoding: "utf8", timeout: 3_000, }); if (xattr.includes("com.apple.quarantine")) { return ( `macOS quarantine is blocking the OpenCode binary. ` + `Run: xattr -d com.apple.quarantine ${resolvedPath}` ); } // Look for a recent crash report with the termination reason. const crashDir = Path.join(OS.homedir(), "Library/Logs/DiagnosticReports"); const binaryName = Path.basename(resolvedPath); const recentReports = FS.readdirSync(crashDir) .filter((f) => f.startsWith(binaryName) && f.endsWith(".ips")) .toSorted() .toReversed() .slice(0, 1); for (const report of recentReports) { const content = FS.readFileSync(Path.join(crashDir, report), "utf8"); if (content.includes('"namespace":"CODESIGNING"')) { return ( "macOS killed the process due to an invalid code signature. " + "The binary may be corrupted — try reinstalling OpenCode." ); } } } catch { // Best-effort detection — don't fail the original error path. } return null; } export async function startOpenCodeServerProcess(input: { readonly binaryPath: string; readonly port?: number; readonly hostname?: string; readonly timeoutMs?: number; }): Promise<OpenCodeServerProcess> { const hostname = input.hostname ?? DEFAULT_HOSTNAME; const port = input.port ?? (await findAvailablePort()); const timeoutMs = input.timeoutMs ?? DEFAULT_OPENCODE_SERVER_TIMEOUT_MS; const args = ["serve", `--hostname=${hostname}`, `--port=${port}`]; const child = spawn(input.binaryPath, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, OPENCODE_CONFIG_CONTENT: JSON.stringify({}), }, }); child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); let stdout = ""; let stderr = ""; let closed = false; const close = () => { if (closed) { return; } closed = true; child.kill(); }; const url = await new Promise<string>((resolve, reject) => { const timeout = setTimeout(() => { close(); reject(new Error(`Timed out waiting for OpenCode server start after ${timeoutMs}ms.`)); }, timeoutMs); const cleanup = () => { clearTimeout(timeout); child.stdout.off("data", onStdout); child.stderr.off("data", onStderr); child.off("error", onError); child.off("close", onClose); }; const onStdout = (chunk: string) => { stdout += chunk; const parsed = parseServerUrlFromOutput(stdout); if (!parsed) { return; } cleanup(); resolve(parsed); }; const onStderr = (chunk: string) => { stderr += chunk; }; const onError = (error: Error) => { cleanup(); reject(error); }; const onClose = (code: number | null, signal: NodeJS.Signals | null) => { cleanup(); const exitReason = signal ? `signal: ${signal}` : `code: ${code ?? "unknown"}`; const hint = signal === "SIGKILL" && process.platform === "darwin" ? detectMacosSigkillHint(input.binaryPath) : null; reject( new Error( [ `OpenCode server exited before startup completed (${exitReason}).`, hint, stdout.trim() ? `stdout:\n${stdout.trim()}` : null, stderr.trim() ? `stderr:\n${stderr.trim()}` : null, ] .filter(Boolean) .join("\n\n"), ), ); }; child.stdout.on("data", onStdout); child.stderr.on("data", onStderr); child.once("error", onError); child.once("close", onClose); }); return { url, process: child, close, }; } export async function connectToOpenCodeServer(input: { readonly binaryPath: string; readonly serverUrl?: string | null; readonly port?: number; readonly hostname?: string; readonly timeoutMs?: number; }): Promise<OpenCodeServerConnection> { const serverUrl = input.serverUrl?.trim(); if (serverUrl) { return { url: serverUrl, process: null, external: true, close() {}, }; } const server = await startOpenCodeServerProcess({ binaryPath: input.binaryPath, ...(input.port !== undefined ? { port: input.port } : {}), ...(input.hostname !== undefined ? { hostname: input.hostname } : {}), ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}), }); return { url: server.url, process: server.process, external: false, close: () => server.close(), }; } export async function runOpenCodeCommand(input: { readonly binaryPath: string; readonly args: ReadonlyArray<string>; }): Promise<OpenCodeCommandResult> { const child = spawn(input.binaryPath, [...input.args], { stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32", env: process.env, }); child.stdout?.setEncoding("utf8"); child.stderr?.setEncoding("utf8"); const stdoutChunks: Array<string> = []; const stderrChunks: Array<string> = []; child.stdout?.on("data", (chunk: string) => stdoutChunks.push(chunk)); child.stderr?.on("data", (chunk: string) => stderrChunks.push(chunk));