/** * StdioTransport — spawns a child process and pipes stdin/stdout through * Content-Length framing + JSON-RPC. */ import { FrameDecoder } from "./framing.js"; import { JsonRpcClient, type WriteFn } from "./rpc.js"; export interface SpawnedProcess { readonly stdin: { readonly write: (bytes: Uint8Array) => void }; readonly stdout: | AsyncIterable | { readonly on: (event: string, cb: (data: Uint8Array) => void) => void }; readonly stderr?: | AsyncIterable | { readonly on: (event: string, cb: (data: Uint8Array) => void) => void } | undefined; readonly pid: number | undefined; readonly kill: () => void; } export type SpawnProcess = ( command: readonly string[], opts: { readonly cwd: string; readonly env?: Readonly> | undefined }, ) => SpawnedProcess; export interface Connection { readonly send: (method: string, params?: unknown) => Promise; readonly notify: (method: string, params?: unknown) => void; readonly onNotification: (method: string, handler: (params: unknown) => void) => void; readonly close: () => void; readonly pid: number | undefined; } export interface StdioTransportDeps { readonly spawn: SpawnProcess; readonly command: readonly string[]; readonly env?: Readonly>; } export function createStdioTransport( deps: StdioTransportDeps, cwd: string, ): { connection: Connection; promise: Promise } { const spawnOpts: { readonly cwd: string; readonly env?: Readonly> } = { cwd, }; if (deps.env) { (spawnOpts as { env?: Readonly> }).env = deps.env; } const proc = deps.spawn(deps.command, spawnOpts); const decoder = new FrameDecoder(); const writeFn: WriteFn = (bytes) => proc.stdin.write(bytes); const rpc = new JsonRpcClient(writeFn); const stdoutSource = proc.stdout; const promise = new Promise((resolve, reject) => { if (Symbol.asyncIterator in stdoutSource) { (async () => { try { for await (const chunk of stdoutSource as AsyncIterable) { const messages = decoder.decode(chunk); for (const msg of messages) { rpc.handleMessage(msg); } } resolve(); } catch (err: unknown) { reject(err); } })(); } else { const source = stdoutSource as { readonly on: (event: string, cb: (data: Uint8Array) => void) => void; }; source.on("data", (data: Uint8Array) => { const messages = decoder.decode(data); for (const msg of messages) { rpc.handleMessage(msg); } }); source.on("end", () => resolve()); source.on("error", (err: unknown) => reject(err)); } }); const connection: Connection = { send: (method, params) => rpc.request(method, params), notify: (method, params) => rpc.notify(method, params), onNotification: (method, handler) => rpc.onNotification(method, handler), close: () => { rpc.close(); proc.kill(); }, pid: proc.pid, }; return { connection, promise }; }