Why closeout
closeout against signal-exit, exit-hook and restore-cursor, one capability per row, every cell linked to the test, grade or source that proves it.
signal-exit runs a handler when a process ends, exit-hook lets that handler be async, and restore-cursor shows the cursor again. closeout does all three — through drop-in paths graded by each one's own test suite — and adds what none of them has: one handler on every exit path told which one it was, phases that put the terminal back last, raw mode and the alternate screen restored as well as the cursor, and a deadline that ends a hung shutdown and names the handler that hung.
The table below is the whole comparison. Every mark links to its evidence: a test in this
repository for ours, and for theirs the source file of the exact version compat-oracle grades,
or that package's own test suite. scripts/capabilities-lock.test.ts fails the build when a
cited test no longer contains the title it is cited for, when a source no longer contains the
line it is quoted for, or when a source we say lacks something has gained it.
✓ yes · ◐ partial, with what is missing · ✗ no · — does not apply. Every mark links to its evidence: our test or grade, or the incumbent’s source at the version compat-oracle grades.
Every way out
One registry for every exit path
A handler registered once runs on a signal, an uncaught throw, an unhandled rejection, an emptied event loop (
beforeExit) andprocess.exit(), so no door out skips the cleanup.closeout- closeout: yes
The handler is told which door, and what was thrown
Every handler gets
{ path, signal, code, error }, so it can tell Ctrl-C from a crash and keep the temp directory for a bug report.closeout- closeout: yes
restore-cursor- restore-cursor: does not applytakes no handler of yours
Exactly once when two doors fire together
Ctrl-C twice, or a signal and the
exitbehind it, is one shutdown, so a lock is never released twice.closeout- closeout: yes
signal-exit- signal-exit: yes
exit-hook- exit-hook: yes
One handler's throw does not skip the rest
A handler that throws is reported and the handlers after it still run, so the one that restores the terminal is not the one that gets skipped.
closeout- closeout: yes
A shutdown that ends
A deadline that names the handler that hung
A handler that never returns cannot hold Ctrl-C hostage: after the deadline (2000 ms by default) the process leaves with the code it was leaving with and prints which handlers had not returned.
closeout- closeout: yes
Phases: flush, release, then restore last
Handlers run in
flush,release,restoreorder, each phase awaited before the next, so the terminal comes back after the log is written whichever registered first.closeout- closeout: yes
The restore still runs after a handler hung
Past the deadline the later phases are still run, just no longer waited for, so a hung
flushdoes not decide that the cursor stays hidden.closeout- closeout: yes
The terminal
Cursor, raw mode and the alternate screen restored
hideCursor,rawModeandalternateScreenmake the change and register its undo in therestorephase, so a full-screen program that dies by any door hands back a usable terminal.closeout- closeout: yes
A second signal waits for the first shutdown
A second Ctrl-C, or a SIGTERM behind it, while a handler still holds the first shutdown waits for that shutdown, so the restore runs before the process dies.
closeout- closeout: yes
No escape sequences into a pipe
A stream that is not a terminal gets no cursor or screen sequence on the way in or out, so a log carries only what the program wrote.
closeout- closeout: yes
restore-cursor- restore-cursor: yes
Exit status
A signalled process dies of the signal
After the handlers run, the signal is re-raised, so a shell prints
^C,makestops, and a CI runner marks the job cancelled rather than failed.closeout- closeout: yes
signal-exit- signal-exit: yes
A program that owns the signal keeps it
When the program has its own listener for the signal, the handlers run and the program still decides what happens next, with one delivery for one Ctrl-C.
closeout- closeout: yes
signal-exit- signal-exit: yes
Agents and plugins
The shutdown as one
--jsonline or an agent eventreportToJson()andreportToEvent()project the record the handlers were given, withtimedOutand the handlers that hung, so a log and an agent read the same facts.closeout- closeout: yes
signal-exit- signal-exit: no
exit-hook- exit-hook: no
restore-cursor- restore-cursor: no
Plugins: named handlers in a phase, readable as data
A plugin contributes named handlers to
flushorrelease, is validated against the family schema, andcontributions()lists the whole shutdown in order without running it.closeout- closeout: yes
signal-exit- signal-exit: no
exit-hook- exit-hook: no
restore-cursor- restore-cursor: no
Compatibility
Passes exit-hook's own test suite
closeout/exit-hookis graded by exit-hook 5.1.0's own tests, unedited, so changing the import keeps exit-hook's behaviour, 128 + n exit codes included.signal-exit- signal-exit: does not applya different API
restore-cursor- restore-cursor: does not applya different API
Passes restore-cursor's own test suite
closeout/restore-cursoris graded by restore-cursor 5.1.0's own tests, which spawn a child per case and check which stream the cursor comes back on.signal-exit- signal-exit: does not applya different API
restore-cursor- restore-cursor: yesits own suite, the control run
Passes signal-exit's own test suite, level with signal-exit
closeout/signal-exitpasses 134 of the 135 cases of signal-exit 4.1.0's own suite, and signal-exit itself passes the same 134: the one case short fails for both on current Node.restore-cursor- restore-cursor: does not applya different API
Reading it
- "ours" means closeout's own API,
onExitandinstall(). The three drop-ins keep their incumbents' behaviour, which is what their grades measure:closeout/exit-hookexits128 + nand ignores SIGHUP because exit-hook does. Compatibility lists what each drop-in keeps. - Parity rows are here too. signal-exit and exit-hook both run a handler once when two doors fire together, and signal-exit re-raises a signal as closeout does. A row where they match us is a row a reader would otherwise have to go and check.
- — does not apply is not a soft ✗. restore-cursor takes no handler of yours, so rows about your handlers do not apply to it; the cell says why.
- signal-exit's grade is level with signal-exit.
closeout/signal-exitpasses 134 of the 135 cases of signal-exit's own suite, and signal-exit passes the same 134. The one case both fail,does not exit if user handles signal, fails for signal-exit itself on every Node released since its last version.
What is not in the table
A row goes in only when every cell of it can be proved. These were left out:
- "The exit code is the program's." On a signal, a throw and a rejection, closeout decides
how the process leaves before any handler runs, and
matrix.test.tsproves it for the deadline. Onprocess.exit()a handler that assignsprocess.exitCodestill changes the code, because Node reads it after theexitlisteners — see Signals and exit status. The row would not be true on every path, so it is not a row. npx closeout check. The plugin checker ships and is shown on Plugins, where the example is run by this site's tests; closeout has no unit test of the command itself yet.- Weight. Zero dependencies is a fact of the manifest, and the per-entry byte budgets are
asserted by
weight.test.tsand published on Benchmarks, measured the same way on both sides. They are not a yes-or-no capability.closeout/exit-hookis larger than exit-hook, and closeout's README says so. - SIGKILL. No package can run a handler on it, closeout included; a row would be four ✗s that say nothing about any of them.
Plugins
closeout's plugin host: a plugin contributes named shutdown handlers to flush or release, is validated against the family schema, reads as data before it runs, and is checked by npx closeout check.
Compatibility
How closeout's three drop-ins are graded — each incumbent's own test suite, unedited — the current grades, including signal-exit's 134 / 135 level with signal-exit itself, and the differences that remain.