repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/git-safe/spawn.ts
Raw// spawn.ts — Shared spawn helper with timeout and git safety env vars
import { spawn } from "node:child_process";
export interface SpawnResult {
stdout: string;
stderr: string;
code: number | null;
killed: boolean;
}
/**
* Spawn a command with timeout and structured error output.
* Kills the process on timeout with SIGKILL.
*
* GIT_TERMINAL_PROMPT=0 is always set — prevents credential prompts from hanging.
*/
export function spawnWithTimeout(
cmd: string,
args: string[],
opts: { cwd?: string; timeout?: number; env?: Record<string, string> } = {},
): Promise<SpawnResult> {
const { cwd, timeout = 60_000, env: extraEnv } = opts;
return new Promise((resolve) => {
const env = {
...process.env,
GIT_TERMINAL_PROMPT: "0",
GIT_TERMINAL_PROMPT_AUTOCHECK: "0",
...extraEnv,
};
const proc = spawn(cmd, args, { cwd, env: env as Record<string, string> });
let stdout = "";
let stderr = "";
let timedOut = false;
let settled = false;
const finish = (result: SpawnResult) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(result);
};
const timer = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, timeout);
proc.stdout.on("data", (d: Buffer) => (stdout += d));
proc.stderr.on("data", (d: Buffer) => (stderr += d));
proc.on("close", (code) => {
finish({ stdout, stderr, code, killed: timedOut });
});
proc.on("error", (err) => {
finish({ stdout, stderr: err.message, code: null, killed: timedOut });
});
});
}
/**
* Spawn and expect exit code 0. Throws structured error on failure.
*/
export async function spawnExpect(
cmd: string,
args: string[],
opts: { cwd?: string; timeout?: number; env?: Record<string, string> } = {},
): Promise<string> {
const result = await spawnWithTimeout(cmd, args, opts);
if (result.killed) {
throw new SpawnTimeoutError(cmd, args, opts.timeout ?? 60_000);
}
if (result.code !== 0) {
throw new SpawnExitError(
cmd,
args,
result.code,
result.stderr,
result.stdout,
);
}
return result.stdout;
}
// ── Error types ────────────────────────────────────────────────────────────
export class SpawnTimeoutError extends Error {
constructor(
public readonly cmd: string,
public readonly args: string[],
public readonly timeoutMs: number,
) {
super(`Command timed out after ${timeoutMs}ms: ${cmd} ${args.join(" ")}`);
this.name = "SpawnTimeoutError";
}
}
export class SpawnExitError extends Error {
constructor(
public readonly cmd: string,
public readonly args: string[],
public readonly exitCode: number | null,
public readonly stderr: string,
public readonly stdout: string,
) {
const stderrLine = stderr.trim().split("\n").pop() ?? "";
super(
`Command failed (exit ${exitCode}): ${cmd} ${args.join(" ")}\n${stderrLine}`,
);
this.name = "SpawnExitError";
}
}