import { spawn } from 'child_process'; export interface CLIRunOptions { command: string; args: string[]; stdin?: string; timeout?: number; cwd?: string; env?: Record; } export interface CLIRunResult { stdout: string; stderr: string; exitCode: number; } // 10 MB limit per stream to prevent memory issues from runaway output const MAX_BUFFER_SIZE = 10 * 1024 * 1024; export function runCLIProcess(options: CLIRunOptions): Promise { const { command, args, stdin, timeout = 120000, cwd, env } = options; return new Promise((resolve, reject) => { const processEnv = env ? { ...process.env, ...env } : process.env; const child = spawn(command, args, { cwd, env: processEnv, stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; let killed = false; let settled = false; let forceKillTimer: ReturnType | undefined; const settle = (fn: () => void) => { if (settled) return; settled = true; clearTimeout(timer); if (forceKillTimer) clearTimeout(forceKillTimer); fn(); }; const timer = setTimeout(() => { killed = true; child.kill('SIGTERM'); // Give it a moment to clean up, then force kill forceKillTimer = setTimeout(() => { if (!child.killed) { child.kill('SIGKILL'); } }, 5000); }, timeout); child.stdout.on('data', (data: Buffer) => { if (stdout.length < MAX_BUFFER_SIZE) { stdout += data.toString(); } }); child.stderr.on('data', (data: Buffer) => { if (stderr.length < MAX_BUFFER_SIZE) { stderr += data.toString(); } }); child.on('error', (error: Error) => { settle(() => reject(new Error(`Failed to spawn CLI process "${command}": ${error.message}`))); }); child.on('close', (exitCode: number | null) => { if (killed) { settle(() => reject(new Error(`CLI process "${command}" timed out after ${timeout}ms`))); return; } const code = exitCode ?? 1; if (code !== 0) { settle(() => reject( new Error( `CLI process "${command}" exited with code ${code}: ${stderr.trim() || stdout.trim()}` ) ) ); return; } settle(() => resolve({ stdout, stderr, exitCode: code })); }); // Pipe stdin if provided; swallow write errors since the child // may have exited before consuming input — the close/error // handlers above will report the actual failure. child.stdin.on('error', () => {}); if (stdin !== undefined) { child.stdin.write(stdin); child.stdin.end(); } else { child.stdin.end(); } }); }