Releases: dbtlr/loomcli
Release list
v0.2.0
This release adds plugins. An Application installs each one explicitly through its plugins list, and a plugin contributes options, one middleware with declared activation, extension values, failure renderers, and a claim on the process signals. Core installs nothing on its own.
@loomcli/plugins is a new package. It ships the first-party plugins as separately installable subpath exports, starting with @loomcli/plugins/help and @loomcli/plugins/version, and it declares @loomcli/core as a peer dependency at its own version, so pin both packages at the same version.
Two changes are breaking. Command takes one options object, and ExitCode gains 130 and 143 for cancelled runs. Each migration section below gives the exact steps.
Breaking Changes
- Change
Commandto take one options object.new Command(name, { globals })replaces the positionalnew Command(name, globals)form, which no longer compiles and no longer builds. - Add
descriptionandversionas core facts.descriptionis optional on the Application, on a Command, on an option, and on an argument, and holds a character other than whitespace and no line terminator.versionis an optional string on the Application under the same rule, and a version that is not a string, is blank, or holds a line terminator fails build withThe Application version must be a string that holds a character other than whitespace and no line terminator. Supply a string such as "1.2.0".Build validates both ininspect()and inrun(). - Add the facts to
inspect().CommandGraphreportsversionas a string,0.0.0when the Application declares none, which means unversioned, so a projection never branches on an absent version. It reportsdescription, and eachCommandNode,ArgumentNode, andOptionNodereports its owndescription, orundefinedwhere the declaration omits one. The rootCommandNodereports the Application's description, the valueCommandGraph.descriptionholds. - Export the
CommandOptionstype from core.
Migration
Affected surface. Every new Command(name, globals) call that supplies a GlobalOptions value positionally. The Application constructor, the declaration calls, and every runtime behavior are unchanged.
Why. A Command now carries facts beside its globals, so its second argument is one options object, as the Application's already is. The retired form supplies a value that holds no globals key, so the globals would vanish silently and an operator, not the author, would meet the result as an unknown-option error.
Before and after.
Before:
import { Command } from '@loomcli/core';
import { globals } from '../globals.js';
export const get = new Command('get', globals).argument('path', { required: true }).action(getValue);After:
import { Command } from '@loomcli/core';
import { globals } from '../globals.js';
export const get = new Command('get', { description: 'Reads one value.', globals })
.argument('path', { required: true })
.action(getValue);Steps.
- Replace each
new Command(name, globals)withnew Command(name, { globals }). - Leave
new Command(name)unchanged, because a Command without globals still omits the second argument. - Add
descriptionto a Command, an option, or an argument, anddescriptionandversionto the Application, where a projection needs them. Each fact is optional. - Read the new
inspect()fields in any projection that renders the graph.
Validation. Run the application's type check to find each remaining positional call, which reports Type 'GlobalOptions<...>' has no properties in common with type 'CommandOptions<...>'. Run one invocation of a Command the application declares, such as node ./your-cli.js get user.name: a missed call reports Invalid declaration: Command "get" takes an options object. Supply { globals } instead of a positional GlobalOptions value. and exits with code 1. Run the application's test command to confirm the graph builds and dispatches.
- Add
run({ signal }). It accepts a caller-ownedAbortSignalthat cancels the run; core subscribes at run entry and honors the abort at every phase boundary. A value that is not anAbortSignalis an internal error with code 1. - Add the signals slot. One installed plugin claims
SIGINT,SIGTERM, or both, each of them once; core installs one process listener per claimed signal once the graph has validated, removes them on every exit path of that run, and re-raises a repeated signal so the default disposition ends the process when no other listener remains. - Add a typed cancellation reason. The
signalon a middleware context and on an action context aborts with a reason core owns, the exportedCancellationReason,{ source: 'SIGINT' | 'SIGTERM' | 'caller', cause?: unknown }, wherecausecarries the caller's ownsignal.reason, so a middleware readssourceand never infers a signal name. - Change
ExitCodeto widen from0 | 1 | 2to0 | 1 | 2 | 130 | 143: 130 forSIGINTor a caller abort, 143 forSIGTERM.
Migration
Affected surface. Every consumer that switches exhaustively on the published ExitCode type, including a switch with no default case or a type-level exhaustiveness check.
Why. A cancelled run now resolves a code of its own, 130 or 143, instead of falling into an existing code. A consumer that matched every member of ExitCode before this change now has a switch that no longer type-checks, because two members are unhandled.
Before and after.
Before:
import type { ExitCode } from '@loomcli/core';
function describe(code: ExitCode): string {
switch (code) {
case 0:
return 'succeeded';
case 1:
return 'failed';
case 2:
return 'invalid input';
}
}After:
import type { ExitCode } from '@loomcli/core';
function describe(code: ExitCode): string {
switch (code) {
case 0:
return 'succeeded';
case 1:
return 'failed';
case 2:
return 'invalid input';
case 130:
return 'cancelled by SIGINT or a caller abort';
case 143:
return 'cancelled by SIGTERM';
}
}Steps.
- Find every exhaustive
switchor lookup overExitCodewith the type checker; a missing case reports the unhandled literals. - Add a
130case forSIGINTor a caller-supplied abort, and a143case forSIGTERM. - Where the consumer treats an unknown code as success, confirm that treatment still holds for 130 and 143, since a script that reports 0 after an interrupt carries on as if the work finished.
- If the consumer owns a supervising process, decide whether to propagate 130 or 143 to its own exit code or to translate them, and update its own documented exit codes accordingly.
Validation. Run pnpm exec tsc --noEmit, or the consumer's own type check, to confirm every ExitCode switch compiles with the two new cases. A code of 130 or 143 reaches a consumer only when the application installs a plugin that owns the signals slot, or when the caller supplies run({ signal }); with neither, core installs no listener and a process signal keeps its default effect. With an owner installed, interrupt a long-running invocation with Ctrl-C and confirm the process exits 130, then send SIGTERM to another and confirm it exits 143.
See the core reference for the exit code table and the signals and cancellation section, and ADR-0018 for the decision.
Changes
- Add plugins.
plugin(identity, definition)returns a frozen value that an Application installs throughpluginsin its options object, in the order every contribution composes. A plugin holds declarations alone and performs no work when it is created or installed. Build rejects an entry that is not a plugin, an identity installed twice, and an empty identity. See the plugin contract. - Add plugin options. A plugin declares options under
optionswith the parsing keystype,short,shortOnly,polarity,multiple,default,description, andextensions, and no schema or presence rule. They join the globals table after the application's own globals, so the pre-scan consumes them at any placement with the same value rules and the same diagnostics. Every collision by key or by spelling is a build error. A plugin option reaches its own plugin's middleware alone: an action never receives it. - Add middleware with declared activation and lazy loading. Core runs the middleware of each installed plugin whose activation matched between routing and the callable check, in installation order, and calls a plugin's
loadonly when the chain reaches it.activateis'always'or a list of the plugin's own option names, evaluated from the pre-scan before any plugin code loads. A middleware receivesoptions,graph,command,host,out,signal, andnext, takes over by returning without callingnext(), and reads what it wrapped from theChainOutcomethatnext()resolves. - Add extensions.
extension(identity, { schema, target })returns a descriptor that is also a factory, and a declaration lists the values it produces underextensionson the Application, on a Command, on any option, and on an argument. Build validates each value once, synchronously, and stores its plain-data output on the graph node under the extension's identity.readExtension(node, descriptor)is the typed read; a fact whose plugin is not installed stays inert and is still reported. - Add failure renderers from plugins. A plugin registers renderers under
failures, which enter resolution after the application's own in installation order. The same class registered by two contributors resolves first-in-wins. - Add
scopeand...
v0.1.1
Changes
- Change the verified platforms to macOS and Linux. Windows is no longer exercised in CI and is unverified.
chore(release): Release v0.1.0 - Introduce the typed command framework
This first release provides typed command declarations, local and global options, nested Commands, and Standard Schema validation.
Applications can inspect the command graph, stream stdin, and customize output and failure rendering. The package supports Node.js 22.23.2 or later and Bun 1.4.0 or later.
New applications install @loomcli/core. The migration below applies to applications built against the unpublished @loom/core source package. See the core reference for the SDK contract.
Breaking Changes
- Change the core package name to
@loomcli/corebefore the first publication. The validation context key follows the package name.
Migration
Affected surface. Core dependencies, import specifiers, and direct Standard Schema libraryOptions access.
Why. The published libraries use the maintained @loomcli npm scope.
Before and after.
import { Application } from '@loom/core';import { Application } from '@loomcli/core';Steps.
- Replace the
@loom/coredependency with@loomcli/coreat the release version. - Update core import specifiers to
@loomcli/core. - Use the exported
validationContextaccessor orvalidationContextKeyinstead of a literallibraryOptionskey.
Validation. Compile the application against the installed package and run its command and validation tests.