import { spawn } from "node:child_process"; import { constants } from "node:fs"; import { access } from "node:fs/promises"; import { join } from "node:path"; import type { BashOperations } from "@earendil-works/pi-coding-agent"; import { span } from "./src/debug.ts"; import { findExecutable } from "./src/pi-ext-executable.ts"; const FRESH_ARGS = [ "--no-config-file", "--no-history", "--error-style", "fancy", "--table-mode", "markdown", "-c", ]; export function findNu(): string | undefined { return findExecutable(process.platform === "win32" ? ["nu.exe"] : ["nu"]); } function stopProcess(pid: number | undefined): void { if (!pid) return; if (process.platform === "win32") { const root = process.env["SystemRoot"] ?? "C:\\Windows"; spawn( join(root, "System32", "taskkill.exe"), ["/F", "/T", "/PID", String(pid)], { stdio: "ignore", windowsHide: true, }, ).unref(); } else { try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch { /* already exited */ } } } } export function freshOperations(binary: string): BashOperations { return { async exec(command, cwd, { onData, signal, timeout, env }) { const finish = span?.( "fresh", timeout === undefined ? undefined : { timeoutMs: timeout * 1_000 }, ); let exitCode: number | null = null; let timedOut = false; let outputBytes = 0; try { if (signal?.aborted) throw new Error("aborted"); if ( timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0 || timeout * 1000 > 2_147_483_647) ) throw new Error( "Invalid timeout: must be a positive number of seconds within the timer range", ); await access(cwd, constants.F_OK); const child = spawn(binary, [...FRESH_ARGS, command], { cwd, env, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], windowsHide: true, }); const onAbort = () => stopProcess(child.pid); const timer = timeout === undefined ? undefined : setTimeout(() => { timedOut = true; onAbort(); }, timeout * 1000); signal?.addEventListener("abort", onAbort, { once: true }); if (signal?.aborted) onAbort(); if (finish) { const forward = (chunk: Buffer) => { outputBytes += chunk.length; onData(chunk); }; child.stdout.on("data", forward); child.stderr.on("data", forward); } else { child.stdout.on("data", onData); child.stderr.on("data", onData); } try { exitCode = await new Promise((resolve, reject) => { child.once("error", reject); child.once("close", resolve); }); if (signal?.aborted) throw new Error("aborted"); if (timedOut) throw new Error(`timeout:${timeout}`); return { exitCode: exitCode ?? (child.signalCode ? 1 : null) }; } finally { if (timer) clearTimeout(timer); signal?.removeEventListener("abort", onAbort); } } finally { const status = signal?.aborted ? "aborted" : timedOut ? "timeout" : exitCode === 0 ? "success" : exitCode === null ? "failed" : "nonzero"; finish?.( status === "success" || status === "nonzero" ? "finish" : "error", { status, count: outputBytes, }, ); } }, }; }