Repository navigation
ts-strictcli@v0.43.0
Framework-owned exits and output, --json interface_version 3 with a stdout guard, check values with failing-checks, declared runtime requirements, help and version commands, and stricter flag and naming rules.
Context
The three implementations move together toward one behavior contract. The framework now owns a command's whole exit path: an early exit raised from anywhere in the call stack, the answer written through ctx.out, the --json document's new output member (interface_version 3), a runtime guard that fails a machine-mode run when anything writes to stdout outside the framework, signal handling, and a --lint-framework-use scan that finds the bypasses statically. Checks gain per-check values, a failing-checks command, and named hook selections; commands gain declared runtime requirements; every app answers help and version commands, and help --json replaces --dump-schema. Several changes are breaking, which in 0.x is a minor: one naming rule refused at declaration, a non-repeatable flag given twice is refused instead of silently keeping the last value, a repeatable global given before and after the command collects every occurrence, check --ignore-warnings is removed in favor of failing-checks, warnings and errors carry prefixes in human mode, and a spawned child still running when its handler ends is killed.
Breaking
- [ts-strictcli] The
--jsondocument isinterface_version3, stdout is guarded under--json, and warnings and errors are prefixed. The document carries anoutputmember afterpayload(the textctx.outwrote plus captured child stdout,nullwhen none), and a child run throughspawnorrun(argv, { stream: true })under--jsonno longer writes beside it. While a machine-mode handler runs,process.stdout.writeis replaced: any byte written outside the framework (console.log,process.stdout) fails the run with exit 1 unless the code is already nonzero, plus the error diagnosticstdout written outside the framework: <n> bytes: "<excerpt>", andprocess.exitends the run through the exit step withprocess exit called outside the framework with code <code>. In human modectx.warnprintswarning: <message>andctx.errorprintserror: <message>. - [ts-strictcli] A child started through
ctx.effects.spawnthat is still running when its handler ends is now killed (SIGTERM, then SIGKILL after one second) and fails the run: exit 1 unless the status is already nonzero, pluserror: child process <pid> was still running when the handler ended and was killed: <argv>. The exit step no longer waits for such a child. Wait on every child you spawn, or end it with the newSpawned.kill();Spawned.sendSignal(signal)sends any signal. - [ts-strictcli]
check --ignore-warningsandRunChecksOptions.ignoreWarningsare removed:checkstays the full report and exits nonzero on warnings, and the newfailing-checkscommand (same selection flags) is the run where only error-level failures fail. Every[checks.<name>]in checks.toml must now declaredescription(one non-empty line) andsubject(the options subject file its option belongs in: lowercase letters, digits, and hyphens, nevermanifest); a checks.toml lacking either is refused at construction withchecks.toml: check "<name>": missing required field "description"(or"subject"), so add both to every declared check.--dump-schemapublishes them on each check entry. - [ts-strictcli] Names follow one rule, refused where they are declared: command, group, deprecated-command, and long flag names (at every level, globals and choice scopes included), choice, constraint, tag, check, hook, grant, and update-resource names must be lowercase kebab-case of at least two characters (
[a-z][a-z0-9]*(-[a-z0-9]+)*), and ashort:must be one ASCII letter (uppercase allowed).helpandversionare framework command names, refused for any command, group, or deprecated command at any depth. Rename a violating name, for exampleflag("m", t.str, { short: "m", ... })toflag("message", t.str, { short: "m", ... }); positional argument names and the App name are unaffected. - [ts-strictcli]
--dump-schemais replaced by<app> help --json, which prints the help document on stdout (the--jsondocument goes to stderr) and writes no file: produce a committed schema with<app> help --json > .strictcli/schema.json. TheschemaPathapp option is removed,project_idis now thenameof the nearestpackage.jsonabove the program's entry script rather than thepackage.jsonin the working directory, and passing--dump-schemais refused naminghelp --json.--helpand--versionstay text only: with--jsonthey are refused naming the command that prints the machine form. The root and group help pages now end withUse '<app> help <command>' for more information. - [ts-strictcli] Breaking: a flag that is not repeatable is refused when it is given more than once.
--commits a --commits bused to keep onlybsilently; it now exits 1 with--commits: given more than once, as '--commits a' and '--commits b'; it takes one value. Every spelling counts ----commits=a, a short alias, and for a bool--cachegiven twice or beside--no-cache-- and so do flags inside a choice's scope, member flags, a global given before and after the command, a repeated--unset-<prop>, and the reserved--config. Repeatable, list, and dict flags still collect every occurrence, and environment variables and config files are sources, not occurrences, so their precedence is unchanged. A script or test that passes a flag twice to override an earlier value must pass it once. - [ts-strictcli] Breaking: a repeatable global flag given both before and after the command collects every occurrence.
app --label a run --label bused to deliver onlyb, silently droppinga; it now delivers[a, b], in command-line order, exactly as if every occurrence had been typed on one side -- souniquerefuses a value repeated across the command name. This applies to repeatable, list, and dict globals; a global that is not repeatable is still refused when given on both sides. Environment variables and config files are sources, not occurrences: they are never joined to command-line values, and an occurrence on either side still replaces them. A script that repeated a global after the command to override the value before it must pass it once.
Features
- [ts-strictcli] Early exit, output writers, signals, and --lint-framework-use.
throw new ExitNow(code, message)ends a command from anywhere in its call stack through the exit step:finallyblocks run, the command exitscode(1 to 255), and the message is printed aserror: <message>(under--json, the last error diagnostic);app.call()rejects withExitError, and MCPtools/callanswersexit code <code>: <message>.ctx.out(text)writes the command's answer and is never hidden by--quiet;payloadRenderer:declares how a payload reads in human mode;ctx.document()writes anownsStdoutcommand's document byte-exactly in both modes.app.run()catches SIGINT and SIGTERM while the handler runs and abortsctx.signal; when the handler returns, the command exits 130 or 143 witherror: canceled by signal <SIGNAL>, and a second signal gets the default action. Every app reserves--lint-framework-use, which, run alone from the project root, scans the modules reachable from thepackage.jsonbinentries (test files excluded) for process exits, direct stdout and stderr writes, argv access, and environment reads, and exits 1 listing each finding as<path>:<line>: <rule>: <message>. - [ts-strictcli]
ExitErrorcarriespayload: what the command supplied throughctx.payloadbefore its early exit,nullwhen it supplied nothing. - [ts-strictcli] Check values,
failing-checks, and named hooks.app.setCheckValueResolver((name) => CheckValue | undefined)lets an app assign each check a{ value, source }:offdoes not run the check (shown as anOFFrow with its source),warnreports its failures as warnings,errorruns it as registered; a value above the registered severity, outside the three, or without a source is refused. The newfailing-checkscommand runs the same selection ascheckand prints only error-level failures, exiting nonzero exactly when one exists.[hooks.<name>]tables in checks.toml (tag = "<expression>") name a selection thatcheck --hook <name>andfailing-checks --hook <name>run, andcheck --listshows each check's value and where it came from. - [ts-strictcli] The
helpandversioncommands. Every app answers<app> help <groups> <command>with the page--helpprints,<app> help <command> --<flag>with that one flag's help (a scoped flag with the selector and choice that own it),<app> help --depth N [<group>]with the command tree N levels deep, and<app> versionwith the version line; under--json,helpprints the help document (pruned to the address, which it records) andversionprints{"name", "version"}. - [ts-strictcli] Declared runtime requirements.
requirement({ name, help, install, load })declares something a command needs at run time once,requires: [...]references it from each command that needs it, andctx.need(requirement)returns the loaded value. The framework loads a command's requirements before its handler on every door (run(),test(),call(), and MCP) and in dry mode; a missing one ends the command with exit 1 andcommand '<path>' needs <name> (<help>), which is not available: <reason>; install it: <install>.--helpshows aRequirements:section and the help document arequireslist.
Fixes
- [ts-strictcli] effects-bypass no longer loses a handler at a template substitution. A
${...}inside a template literal ended the handler's scope for the check, so a direct call after it went unreported, and the template's text was read as code; regular expression literals are now read as literals too. - [ts-strictcli] Under
--jsona spawned child's stdout now reaches theoutputmember in the order it arrives, interleaved withctx.outtext, instead of all at once when the child exits; a handler that only awaitsctx.signalno longer makes Node exit with status 13 before a signal can arrive; andspawnno longer hangs in a program run withnode --input-type=module. - [ts-strictcli] A
check --tagexpression that does not parse is reported aserror: tag expression: ...(anerrordiagnostic under--json) instead of rejecting the run. - [ts-strictcli] The check commands'
payload_schemain--dump-schemalistsitemsbeforetype, as Go writes it, so a checks-enabled app's schema dump is byte-identical across the Python, Go, and TypeScript implementations. - [ts-strictcli] A short that two choices of one choice flag reuse for different flags now names the flag of the elected choice, as in Python and Go.
send --via email -x 2was refused as an out-of-scope flag of the other choice, because the short was always bound to the last-declared flag; now-xmeans--subjectunder--via email, so--via email -x 1 --subject 2is refused as--subjectgiven more than once.