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.
A plugin is one plain object, shared by the whole burgee family. closeout reads its
handlers key and ignores every other layer's keys without complaining, so the same object
works on whichever packages of the family a program has installed.
import { install } from 'closeout';
import { attach, contributions, register } from 'closeout/plugin';
register({
name: 'acme',
handlers: [
{ name: 'unlock', run: () => console.log('lock released') },
{ name: 'flush-audit-log', phase: 'flush', run: () => console.log('audit log flushed') },
],
});
for (const c of contributions()) console.log(`${c.phase}: ${c.id}`);
attach(install().registry);contributions() lists the whole contributed shutdown, in the order it will run, without
running any of it. attach(registry) wires each handler into its phase, and returns the
function that takes them back off:
flush: acme:flush-audit-log
release: acme:unlock
audit log flushed
lock releasedA plugin handler is named "<plugin>:<handler>", which is what the
deadline's report prints when it hangs.
What a plugin may declare
| key | rule |
|---|---|
name | required: the plugin's name |
contract | optional: the plugin contract it was written for; a newer one than this closeout knows is refused |
handlers[].name | required, because a hang has to be named |
handlers[].run | required: the handler, handed the same record onExit handlers get |
handlers[].phase | flush or release (the default) — not restore |
restore is closeout's own last phase. A plugin handler admitted to it could run after the
terminal was handed back depending on nothing but which registered first, which is the
coincidence phases exist to replace. A refusal throws with a code — E_PLUGIN_SCHEMA,
E_PLUGIN_CONTRACT or E_NO_CONTRIBUTION — and a message that says what to use instead, and
the refused plugin is not kept.
Checking a plugin before it ships
npx closeout check <file> loads a plugin module's default export, validates it and reports
what closeout would do with it. It exits 0 when the plugin contributes, 1 on a refusal with a
code and a fix, and 2 on a usage error:
export default {
name: 'acme',
handlers: [{ name: 'unlock', phase: 'restore', run: () => {} }],
};E_PLUGIN_SCHEMA: plugin "acme": handlers[0]: "restore" is not a phase a plugin may use
fix: use one of flush, release — "restore" is closeout’s own last phase, and a handler placed after it could not clean up what it was registered to clean upWhat is tested
plugin.test.ts: a plugin's handler runs before the restore and is awaited first; the restore survives a plugin handler that never returns; the contributions read as data; another layer's object is kept; arestorehandler, a handler with norunor no name, and a newer contract are each refused.
Reports for logs and agents
The shutdown as data: one record per shutdown, projected as a --json line by reportToJson and as an agent event by reportToEvent, with timedOut and the handlers that hung.
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.