Skip to content

ts-strictcli@v0.43.0

Choose a tag to compare

@smm-h smm-h released this 29 Sep 17:45
· 167 commits to main since this release

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 --json document is interface_version 3, stdout is guarded under --json, and warnings and errors are prefixed. The document carries an output member after payload (the text ctx.out wrote plus captured child stdout, null when none), and a child run through spawn or run(argv, { stream: true }) under --json no longer writes beside it. While a machine-mode handler runs, process.stdout.write is 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 diagnostic stdout written outside the framework: <n> bytes: "<excerpt>", and process.exit ends the run through the exit step with process exit called outside the framework with code <code>. In human mode ctx.warn prints warning: <message> and ctx.error prints error: <message>.
  • [ts-strictcli] A child started through ctx.effects.spawn that 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, plus error: 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 new Spawned.kill(); Spawned.sendSignal(signal) sends any signal.
  • [ts-strictcli] check --ignore-warnings and RunChecksOptions.ignoreWarnings are removed: check stays the full report and exits nonzero on warnings, and the new failing-checks command (same selection flags) is the run where only error-level failures fail. Every [checks.<name>] in checks.toml must now declare description (one non-empty line) and subject (the options subject file its option belongs in: lowercase letters, digits, and hyphens, never manifest); a checks.toml lacking either is refused at construction with checks.toml: check "<name>": missing required field "description" (or "subject"), so add both to every declared check. --dump-schema publishes 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 a short: must be one ASCII letter (uppercase allowed). help and version are framework command names, refused for any command, group, or deprecated command at any depth. Rename a violating name, for example flag("m", t.str, { short: "m", ... }) to flag("message", t.str, { short: "m", ... }); positional argument names and the App name are unaffected.
  • [ts-strictcli] --dump-schema is replaced by <app> help --json, which prints the help document on stdout (the --json document goes to stderr) and writes no file: produce a committed schema with <app> help --json > .strictcli/schema.json. The schemaPath app option is removed, project_id is now the name of the nearest package.json above the program's entry script rather than the package.json in the working directory, and passing --dump-schema is refused naming help --json. --help and --version stay text only: with --json they are refused naming the command that prints the machine form. The root and group help pages now end with Use '<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 b used to keep only b silently; 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 --cache given 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 b used to deliver only b, silently dropping a; it now delivers [a, b], in command-line order, exactly as if every occurrence had been typed on one side -- so unique refuses 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: finally blocks run, the command exits code (1 to 255), and the message is printed as error: <message> (under --json, the last error diagnostic); app.call() rejects with ExitError, and MCP tools/call answers exit 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 an ownsStdout command's document byte-exactly in both modes. app.run() catches SIGINT and SIGTERM while the handler runs and aborts ctx.signal; when the handler returns, the command exits 130 or 143 with error: 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 the package.json bin entries (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] ExitError carries payload: what the command supplied through ctx.payload before its early exit, null when 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 }: off does not run the check (shown as an OFF row with its source), warn reports its failures as warnings, error runs it as registered; a value above the registered severity, outside the three, or without a source is refused. The new failing-checks command runs the same selection as check and prints only error-level failures, exiting nonzero exactly when one exists. [hooks.<name>] tables in checks.toml (tag = "<expression>") name a selection that check --hook <name> and failing-checks --hook <name> run, and check --list shows each check's value and where it came from.
  • [ts-strictcli] The help and version commands. Every app answers <app> help <groups> <command> with the page --help prints, <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> version with the version line; under --json, help prints the help document (pruned to the address, which it records) and version prints {"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, and ctx.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 and command '<path>' needs <name> (<help>), which is not available: <reason>; install it: <install>. --help shows a Requirements: section and the help document a requires list.

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 --json a spawned child's stdout now reaches the output member in the order it arrives, interleaved with ctx.out text, instead of all at once when the child exits; a handler that only awaits ctx.signal no longer makes Node exit with status 13 before a signal can arrive; and spawn no longer hangs in a program run with node --input-type=module.
  • [ts-strictcli] A check --tag expression that does not parse is reported as error: tag expression: ... (an error diagnostic under --json) instead of rejecting the run.
  • [ts-strictcli] The check commands' payload_schema in --dump-schema lists items before type, 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 2 was refused as an out-of-scope flag of the other choice, because the short was always bound to the last-declared flag; now -x means --subject under --via email, so --via email -x 1 --subject 2 is refused as --subject given more than once.