/** * Background bash job — spawns a `bash -c` subprocess and tracks its life. * Mirrors `apps/infrastructure/src/bgbash/job.rs`. */ import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; /** A handle to a spawned background bash job. */ export class BashJob { readonly id: string; readonly command: string; private process: ChildProcess | null = null; private cancelled = false; private finished = false; constructor(command: string) { this.id = randomUUID(); this.command = command; } /** Spawn the `bash -c` child process and begin tracking it. */ start(): void { const child = spawn("bash", ["-c", this.command], { stdio: ["ignore", "pipe", "pipe"], }); this.process = child; // Drain stdout/stderr so the process doesn't block on a full pipe, and // mark finished when it exits. child.stdout?.resume(); child.stderr?.resume(); child.on("exit", () => { this.finished = true; }); child.on("error", () => { this.finished = true; }); } /** Cancel the job: set the cancelled flag and kill the child process. */ cancel(): void { this.cancelled = true; if (this.process && !this.finished) { try { this.process.kill("SIGKILL"); } catch { /* already gone */ } } } /** Whether the job is currently running. */ isRunning(): boolean { if (this.cancelled || this.finished) return false; if (!this.process) return false; return this.process.exitCode === null && !this.finished; } } /** Spawn a background bash job and return its handle (node-side). */ export function spawnBashJob(command: string): BashJob { const job = new BashJob(command); job.start(); return job; }