/** * Subagents v2 — Process helpers. * * Cross-platform process tree termination. * Windows: taskkill /T /F * POSIX: process group SIGTERM → SIGKILL escalation */ import { spawn, type ChildProcess } from "node:child_process"; // ── Public API ────────────────────────────────────────────────────────────────── /** * Kill a process tree on Windows using taskkill /T /F. */ export async function killWindowsTree(pid: number): Promise { if (!Number.isInteger(pid) || pid <= 0) return false; try { await run("taskkill", ["/PID", String(pid), "/T"], true); await sleep(500); await run("taskkill", ["/PID", String(pid), "/T", "/F"], true); return true; } catch { return false; } } /** * Kill a process tree on POSIX by signalling the process group. * Sends SIGTERM first, then SIGKILL after graceMs (default 2s). */ export async function killPosixTree( pid: number, graceMs = 2_000, ): Promise { if (!Number.isInteger(pid) || pid <= 0) return; try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch { return; // already gone } } await sleep(graceMs); try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch { // already gone } } } /** * Terminate a child process tree, platform-appropriate. * On Windows: taskkill /T /F * On POSIX: process group kill */ export async function terminateProcessTree( child: ChildProcess, ): Promise { if (!child.pid) return; // Try the gentle approach first if (child.exitCode === null && !child.killed) { if (process.platform === "win32") { await killWindowsTree(child.pid); } else { // Detach kills the process group try { process.kill(-child.pid, "SIGTERM"); } catch { // Fallback: kill just the child child.kill("SIGTERM"); } } } // Make sure it's dead if (child.exitCode === null && !child.killed) { try { child.kill("SIGKILL"); } catch { /* already dead */ } } } /** * Spawn a process in its own process group (POSIX) or detached (Windows) * so we can kill the whole tree later. */ export function spawnDetached( command: string, args: string[], options: { cwd?: string; env?: Record } = {}, ): ChildProcess { const isWindowsScript = process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command); const child = spawn(command, args, { cwd: options.cwd, env: { ...process.env, ...options.env }, stdio: ["pipe", "pipe", "pipe"], detached: true, windowsHide: true, // Node cannot execute npm's codex.cmd wrapper directly on Windows. // Arguments here are extension-controlled, not user-provided. shell: isWindowsScript, }); return child; } // ── Helpers ───────────────────────────────────────────────────────────────────── function run( command: string, args: string[], ignoreError = false, ): Promise<{ stdout: string; stderr: string; code: number }> { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true, }); let stdout = ""; let stderr = ""; child.stdout?.on("data", (chunk: Buffer) => (stdout += chunk.toString())); child.stderr?.on("data", (chunk: Buffer) => (stderr += chunk.toString())); child.on("error", (err) => { if (ignoreError) resolve({ stdout, stderr, code: -1 }); else reject(err); }); child.on("close", (code) => { const exitCode = code ?? -1; if (exitCode !== 0 && !ignoreError) { reject(new Error(`${command} exited with code ${exitCode}: ${stderr}`)); } else { resolve({ stdout, stderr, code: exitCode }); } }); }); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); }