armExitGuard(): ExitGuard
Makes exiting 0 something a command has to say, rather than what happens when nothing is left to do.
Node exits 0 when its event loop goes quiet, which means every await in a command is also a
way to report success by accident: whatever it was waiting for never comes, the last handle
closes, and the process ends cleanly having done nothing. cli.ts has hit this often enough that
five separate comments in it say "set the exit code BEFORE this await" — a config failure that
printed nothing and exited 0, a search that found nothing and exited 0, a qunitx repl missing-file.ts with empty stderr. This is that rule as a mechanism instead of a habit.
Armed, the exit code is 1 until ExitGuard.reported sets it to what the work decided. An
exit that never got there is therefore a failure, and says so on stderr with whatever describe
knows about where the work had got to — because the failure it replaces (exit 0, no output) is
one nobody can debug from a CI log.
import { armExitGuard, type ExitTarget } from './exit-guard.ts'; const said: string[] = []; const target: ExitTarget = { exitCode: 0, on: () => {} }; const guard = armExitGuard(() => 'phase: running', target, (message) => said.push(message)); target.exitCode; // 1 — until the work says otherwise guard.reported(0); target.exitCode; // 0 said.length; // 0 — nothing to explain
target: ExitTarget = process