Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
8.9 kB · 281 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282import { type ChildProcess, type ExecFileException, type ExecFileOptionsWithStringEncoding, type ExecFileSyncOptions, type ExecFileSyncOptionsWithStringEncoding, type ExecSyncOptions, type ExecSyncOptionsWithStringEncoding, execFile, execFileSync, execSync, type SpawnOptions, type SpawnSyncOptions, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns, spawn, spawnSync,} from "node:child_process";import { readFileSync } from "node:fs";import { constants } from "node:os";import { basename } from "node:path";
const EXIT_STDIO_GRACE_MS = 100;
/** windowsHide for every non-interactive spawn (console children of a windowless parent flash a fresh console on Windows); only spawns that intentionally hand the user a console call node:child_process directly. */export function spawnHidden(command: string, args: readonly string[], options: SpawnOptions = {}): ChildProcess { return spawn(command, args, { ...options, windowsHide: true });}
export function spawnSyncHidden( command: string, args: readonly string[], options: SpawnSyncOptionsWithStringEncoding,): SpawnSyncReturns<string>;export function spawnSyncHidden( command: string, args?: readonly string[], options?: SpawnSyncOptions,): SpawnSyncReturns<Buffer>;export function spawnSyncHidden( command: string, args: readonly string[] = [], options: SpawnSyncOptions = {},): SpawnSyncReturns<string | Buffer> { return spawnSync(command, args, { ...options, windowsHide: true });}
export function execSyncHidden(command: string, options: ExecSyncOptionsWithStringEncoding): string;export function execSyncHidden(command: string, options?: ExecSyncOptions): Buffer;export function execSyncHidden(command: string, options: ExecSyncOptions = {}): string | Buffer { return execSync(command, { ...options, windowsHide: true });}
export function execFileHidden( file: string, args: readonly string[], options: ExecFileOptionsWithStringEncoding, callback: (error: ExecFileException | null, stdout: string, stderr: string) => void,): ChildProcess { return execFile(file, args, { ...options, windowsHide: true }, callback);}
export function execFileSyncHidden( file: string, args: readonly string[], options: ExecFileSyncOptionsWithStringEncoding,): string;export function execFileSyncHidden(file: string, args?: readonly string[], options?: ExecFileSyncOptions): Buffer;export function execFileSyncHidden( file: string, args: readonly string[] = [], options: ExecFileSyncOptions = {},): string | Buffer { return execFileSync(file, args, { ...options, windowsHide: true });}
const WINDOWS_SHELL_COMMANDS = new Set(["npm", "npx", "pnpm", "yarn", "yarnpkg", "corepack"]);
export function shouldUseWindowsShell(command: string): boolean { if (process.platform !== "win32") return false; const commandName = basename(command).toLowerCase(); return commandName.endsWith(".cmd") || commandName.endsWith(".bat") || WINDOWS_SHELL_COMMANDS.has(commandName);}
/** Cheap kill(0) existence probe; counts zombies as existing. */export function processIdExists(pid: number): boolean { try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }}
/** A zombie has already exited; it only lingers until its parent reaps it. */export function isZombieProcess(pid: number): boolean { if (process.platform === "win32") { return false; } try { const stat = readFileSync(`/proc/${pid}/stat`, "utf8"); const state = stat .slice(stat.lastIndexOf(")") + 2) .trimStart() .charAt(0); return state === "Z"; } catch { // Fall through to the portable process listing used on macOS and BSD. } try { const state = execFileSyncHidden("ps", ["-p", String(pid), "-o", "stat="], { encoding: "utf8" }).trim(); return state.startsWith("Z"); } catch { return false; }}
/** True only for a process that is actually running: zombies do not count. */export function isProcessAlive(pid: number): boolean { return processIdExists(pid) && !isZombieProcess(pid);}
/** True while the group has any member left, zombies included; a group can outlive its leader. */export function processGroupExists(pgid: number): boolean { if (process.platform === "win32") { return false; } try { process.kill(-pgid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }}
/** True while the group has a RUNNING member; unreaped zombies have exited and must not block a group stop. */export function processGroupHasLiveMember(pgid: number): boolean { if (!processGroupExists(pgid)) { return false; } try { const listing = execFileSync("ps", ["-A", "-o", "pgid=", "-o", "stat="], { encoding: "utf8" }); for (const line of listing.split("\n")) { const fields = line.trim().split(/\s+/); if (fields.length < 2) continue; if (Number(fields[0]) === pgid && !fields[1]!.startsWith("Z")) { return true; } } return false; } catch { // Unverifiable listing reads alive: callers keep escalating instead of dropping records over live descendants. return true; }}
/** * Signal the group only while it is provably still the target: the leader process (even a zombie) * anchors its pgid against reuse; once the leader is gone, a live member must hold the pgid at * signal time, narrowing reuse exposure to the inherent kill() TOCTOU of any single-pid signal. */export function signalProcessGroupIfHeld(pgid: number, signal: NodeJS.Signals): boolean { if (!processIdExists(pgid) && !processGroupHasLiveMember(pgid)) { return false; } signalProcessGroupOrProcess(pgid, signal); return true;}
export function signalProcessGroupOrProcess(pid: number, signal: NodeJS.Signals): void { try { process.kill(-pid, signal); return; } catch { // Fall back when process groups are unavailable or the group already exited. } try { process.kill(pid, signal); } catch { // The process may already be fully reaped. }}
/** * Wait for a child process to terminate without hanging on inherited stdio handles. * * On Windows, daemonized descendants can inherit the child's stdout/stderr pipe * handles. In that case the child emits `exit`, but `close` can hang forever even * though the original process is already gone. We wait briefly for stdio to end, * then forcibly stop tracking the inherited handles. */function signalExitCode(signal: NodeJS.Signals | null): number | null { if (!signal) return null; const signalNumber = constants.signals[signal]; return signalNumber === undefined ? 1 : 128 + signalNumber;}
function normalizedExitCode(code: number | null, signal: NodeJS.Signals | null): number | null { return code ?? signalExitCode(signal);}
export function waitForChildProcess(child: ChildProcess): Promise<number | null> { return new Promise((resolve, reject) => { let settled = false; let exited = false; let exitCode: number | null = null; let exitSignal: NodeJS.Signals | null = null; let postExitTimer: NodeJS.Timeout | undefined; let stdoutEnded = child.stdout === null || child.stdout.readableEnded; let stderrEnded = child.stderr === null || child.stderr.readableEnded;
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); };
const finalize = (code: number | null) => { if (settled) return; settled = true; cleanup(); child.stdout?.destroy(); child.stderr?.destroy(); resolve(code); };
const maybeFinalizeAfterExit = () => { if (!exited || settled) return; if (stdoutEnded && stderrEnded) { finalize(normalizedExitCode(exitCode, exitSignal)); } };
const onStdoutEnd = () => { stdoutEnded = true; maybeFinalizeAfterExit(); };
const onStderrEnd = () => { stderrEnded = true; maybeFinalizeAfterExit(); };
const onError = (err: Error) => { if (settled) return; settled = true; cleanup(); reject(err); };
const onExit = (code: number | null, signal: NodeJS.Signals | null = null) => { exited = true; exitCode = code; exitSignal = signal; maybeFinalizeAfterExit(); if (!settled) { postExitTimer = setTimeout(() => finalize(normalizedExitCode(code, signal)), EXIT_STDIO_GRACE_MS); } };
const onClose = (code: number | null, signal: NodeJS.Signals | null = null) => { finalize(normalizedExitCode(code, signal)); };
child.stdout?.once("end", onStdoutEnd); child.stderr?.once("end", onStderrEnd); child.once("error", onError); child.once("exit", onExit); child.once("close", onClose);
if (child.exitCode !== null || child.signalCode !== null) { onExit(child.exitCode, child.signalCode); } });}