67 lines · 2.4 KB · TypeScript
RawDownload
// Find (and optionally kill) leaked dev-stack processes — vite dev servers,// PGlite dev-dbs, e2e emulators — left behind when a session dies without// running its teardown. Default: kill only ORPHANS (processes whose checkout// path no longer exists, e.g. a removed worktree) and list the rest.// `--all` also kills live-checkout servers (do this only when you know no// other agent session is using them). `--dry-run` lists without killing.import { execFileSync } from "node:child_process";import { existsSync } from "node:fs"; const args = new Set(process.argv.slice(2));const dryRun = args.has("--dry-run");const killAll = args.has("--all"); const DEV_PATTERN = /vite dev|dev-db\.ts|scripts\/dev\.ts/; const ps = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" }); interface Candidate {  readonly pid: number;  readonly command: string;  readonly checkout: string | undefined;  readonly orphan: boolean;} const candidates: Candidate[] = [];for (const line of ps.split("\n")) {  const match = line.match(/^\s*(\d+)\s+(.*)$/);  if (!match) continue;  const [, pidText, command] = match;  if (!DEV_PATTERN.test(command!)) continue;  if (Number(pidText) === process.pid) continue;  // The checkout root is whatever absolute path prefixes node_modules/ or a  // workspace dir in the command line.  const pathMatch = command!.match(/(\/[^ ]*?)\/(?:node_modules|apps|packages|e2e)\//);  const checkout = pathMatch?.[1];  const orphan = checkout !== undefined && !existsSync(checkout);  candidates.push({ pid: Number(pidText), command: command!.slice(0, 160), checkout, orphan });} if (candidates.length === 0) {  console.log("reap: no dev-stack processes found.");  process.exit(0);} for (const c of candidates) {  const shouldKill = !dryRun && (c.orphan || killAll);  const tag = c.orphan ? "ORPHAN" : "live  ";  console.log(`${shouldKill ? "KILL " : "keep "} ${tag} pid=${c.pid} ${c.command}`);  if (shouldKill) {    try {      // Group kill (detached boots) with a direct-pid fallback.      try {        process.kill(-c.pid, "SIGTERM");      } catch {        process.kill(c.pid, "SIGTERM");      }    } catch (error) {      console.error(`  failed to kill ${c.pid}: ${String(error)}`);    }  }} if (!killAll && candidates.some((c) => !c.orphan)) {  console.log(    "\nLive-checkout servers were kept (another session may own them); --all kills those too.",  );}