UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

106 lines 4.06 kB
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync, } from "node:child_process"; import crossSpawn from "cross-spawn"; const EXIT_STDIO_GRACE_MS = 100; export function spawnProcess(command, args, options) { return process.platform === "win32" ? crossSpawn(command, args, options) : nodeSpawn(command, args, options); } export function spawnProcessSync(command, args, options) { return process.platform === "win32" ? crossSpawn.sync(command, args, options) : nodeSpawnSync(command, args, options); } /** * Wait for a child process to terminate without hanging on inherited stdio handles. * * A short-lived child can `exit` while a detached descendant keeps its stdout/stderr * pipe open. We must not resolve and destroy the streams on a fixed deadline measured * from `exit`, or output still being written past that deadline is silently lost * (earendil-works/pi#5303). Instead, after `exit` we wait for the pipes to fall idle: * the grace timer is re-armed on every chunk, so an actively writing descendant keeps * us reading, while a quiet inherited handle (e.g. a Windows daemonized descendant * that never lets `close` fire) still releases us after the grace elapses. */ export function waitForChildProcess(child) { return new Promise((resolve, reject) => { let settled = false; let exited = false; let exitCode = null; let postExitTimer; let stdoutEnded = child.stdout === null; let stderrEnded = child.stderr === null; const cleanup = () => { if (postExitTimer) { clearTimeout(postExitTimer); postExitTimer = undefined; } child.removeListener("error", onError); child.removeListener("exit", onExit); child.removeListener("close", onClose); child.stdout?.removeListener("end", onStdoutEnd); child.stderr?.removeListener("end", onStderrEnd); child.stdout?.removeListener("data", onData); child.stderr?.removeListener("data", onData); }; const finalize = (code) => { if (settled) return; settled = true; cleanup(); child.stdout?.destroy(); child.stderr?.destroy(); resolve(code); }; const maybeFinalizeAfterExit = () => { if (!exited || settled) return; if (stdoutEnded && stderrEnded) { finalize(exitCode); } }; const armIdleTimer = () => { if (postExitTimer) clearTimeout(postExitTimer); postExitTimer = setTimeout(() => finalize(exitCode), EXIT_STDIO_GRACE_MS); }; const onData = () => { // Output is still arriving after exit; defer finalizing so we don't // destroy the stream mid-write and truncate the tail. if (exited && !settled) armIdleTimer(); }; const onStdoutEnd = () => { stdoutEnded = true; maybeFinalizeAfterExit(); }; const onStderrEnd = () => { stderrEnded = true; maybeFinalizeAfterExit(); }; const onError = (err) => { if (settled) return; settled = true; cleanup(); reject(err); }; const onExit = (code) => { exited = true; exitCode = code; maybeFinalizeAfterExit(); if (!settled) { armIdleTimer(); } }; const onClose = (code) => { finalize(code); }; child.stdout?.once("end", onStdoutEnd); child.stderr?.once("end", onStderrEnd); child.stdout?.on("data", onData); child.stderr?.on("data", onData); child.once("error", onError); child.once("exit", onExit); child.once("close", onClose); }); } //# sourceMappingURL=child-process.js.map