Repository navigation
Releases: stricttools/strictcli
Release list
go-strictcli@v0.39.2
A declared HTTP read, strictcli.Read(), is refused on any method other than GET, HEAD, or OPTIONS, so a read_only command or a dry run can no longer send a mutating request as a read.
Fixes
- [go-strictcli]
strictcli.Read()on an HTTP method other thanGET,HEAD, orOPTIONSis now a hard error naming the method, returned before any request is sent. Before, a read_only command could declare aDELETE(or any other method) a read, and under--dry-runthe request was really sent while the preview said no changes were made.
go-strictcli@v0.39.1
The framework's check and failing-checks commands refuse a run that selects no checks with exit 1 and a message naming the selector flags, instead of printing their help and exiting 0.
Fixes
- [go-strictcli] The framework's
checkandfailing-checkscommands refuse a run that selects no checks (none of--all,--tag,--name,--hook, and--list) with exit 1 and a message naming the selector flags, instead of printing their help and exiting 0, which read as a pass
go-strictcli@v0.39.0
Adds NewGrant, ErrTimedOut, declared scratch directories (WithScratchDir and ctx.ScratchDir), WithArgsAfterSeparator, HTTP bodies streamed from a file, declared reads, redaction, and the Stdin, Timeout, and Mode effect options; App.Test changes nothing process-wide; test coverage is declared by the source-tree root; the check context factory receives the dispatch's Context; the effects-bypass check skips _test.go files; go-toml-edit is required at its new path.
Breaking
- [go-strictcli]
Test()changes nothing process-wide, so calls may overlap. Eachapp.Test(argv)call captures its output in its own streams, including the stdout and stderr of children run throughSpawnor a streamedRun, and the state a dispatch writes belongs to that dispatch instead of theApp: a test can run a long-lived command in one goroutine while it tests another, on one app or several.EffectLog()returns the most recently finished dispatch's log. Breaking: a byte a handler writes toos.Stdoutdirectly (fmt.Println) is no longer captured inResult.Stdout, and under--jsonTest()no longer arms the stray-stdout guard, which stays onRun(); write throughctx.Out, as--lint-framework-usealready requires. - [go-strictcli] Test coverage is declared by the source-tree root.
WithSourceTreeRoot(path)replacesWithTestCoverageDir(path): the app names the root of its source tree, and strictcli keeps the coverage state in.strictmetadata/.cli-test-coverage/under it, with per-process shard files inshards/(never committed) and the committed manifest inmanifest.json(formerlycoverage/andtest-coverage.json). Coverage is on only when that directory exists at construction.WithTestCoverageDiris now refused at registration, namingWithSourceTreeRoot. To migrate, move.strictcli/test-coverage.jsonto.strictmetadata/.cli-test-coverage/manifest.json(selfdoc layout migratedoes this) and declare the root instead of the directory. - [go-strictcli] The check context factory receives the dispatch's
*Context.SetCheckContextnow takesfunc(*Context) (CheckContext, error):checkandfailing-checkscall it with their own dispatch's context, so a tool can build its check context over that command's effects handle, and an error from the factory refuses the run with its message. A no-argument factory must be rewritten to take the*Contextand return(context, nil).
Features
- [go-strictcli] Stdin and Timeout effect options.
strictcli.Stdin(data)onRunandSpawnwrites bytes to the child's stdin and closes it, so secrets never enter argv; the would-do log and effect record show only the byte count (stdin: N bytes, content withheld).strictcli.Timeout(d)onRunandSpawned.Waitkills the child oncedhas passed and returns an error naming the command and the timeout; a non-positivedis refused, and dry mode renders(timeout: 30s). - [go-strictcli] Declared reads, a settable HTTP client, and redaction.
strictcli.Read()onHTTPandstrictcli.Observe()onRundeclare that one call changes nothing: it is legal in a read-only command, never recorded, and performed for real under--dry-run(a stale brand once a mutation has been recorded); withoutRead()every request stays a recorded mutation whatever its method.strictcli.WithHTTPClient(client)sets the client every live HTTP effect uses, so tests can point commands at anhttptestserver; without it the framework uses its own client with a 60-second timeout instead ofhttp.DefaultClient, andstrictcli.Timeout(d)now also bounds one HTTP request.strictcli.Redact(values...)replaces each value with«redacted»in the would-do log, the effect record, and every error the call returns, so an API key in a query string never reaches--dry-runoutput; an HTTP body renders only as its byte count.strictcli.Mode(mode)onWritesets the file's permission bits, rendered as(mode: 0600). - [go-strictcli] HTTP bodies streamed from a file.
strictcli.BodyFile(path)onHTTPsends a regular file as the request body, read while the request is sent instead of into memory first, with the file size as theContent-Length;strictcli.BodyFileRange(path, offset, length)sends one byte range of it, as a multipart upload's parts need. A missing or irregular file, a range outside the file, a repeated option, or a combination withBodyis refused at the call. Dry mode renders(body: N bytes from <path>), addingat offset Nfor a range, and the record carriesbody_file,body_offset, andbody_bytes. - [go-strictcli] A command can receive the arguments after
--unparsed.WithArgsAfterSeparator(name, help)hands every token after the first bare--to the handler throughctx.ArgsAfterSeparator(), untouched (a--dry-runor a second--there included), while the tokens before it parse as before, so a stray word is still refused. Help lists it underArguments:as-- <name>..., the help document publishes it asargs_after_separator(its command defaultnulljoins thedefaultsblock), andCalland the MCP tools take it as a list of strings under its name. A command without the declaration is unchanged. - [go-strictcli]
ErrTimedOut. An effects-handle run, spawned child's wait, or HTTP request that exceeds itsTimeoutreturns an error matchingstrictcli.ErrTimedOutwitherrors.Is(a redacted message too), so a caller tells a timeout from any other failure without reading the message. - [go-strictcli]
NewGrant.strictcli.NewGrant(name, reason, effectClass)builds a grant authorizing one effect class (ProcMutate,FileWrite, ...), so a caller declares a grant without spelling the struct's effect-class field. - [go-strictcli] Scratch directories. A command declaring
WithScratchDir()can askctx.ScratchDir()for a private directory under~/.cache/strictcli/scratch/, made on the first request and removed when the dispatch ends; making and removing it is never an effect, so a read_only command or a--dry-runcan run a program that writes for itself (a package manager's cache) there. A command that does not declare one is refused. Thecheckandfailing-checkscommands declare one, so a check context factory can hand it to its checks.
Fixes
- [go-strictcli] The README now says where an app's check list belongs: in the app's own package, passed with
WithChecks(path)or embedded withgo:embedand passed toWithChecksEmbed; strictcli has no standard folder or default path for it. - [go-strictcli] go-toml-edit is required at its new path,
github.com/stricttools/go-toml-edit, at 0.5.0. A program using strictcli no longer pulls the module under its old path. - [go-strictcli] The
effects-bypasscheck skips_test.gofiles. A test file builds only into the test binary, never into the program whose command handlers the check guards, so a test's handler or helper writing a fixture directly is no longer reported as a bypass.
py-strictcli@v0.46.0
Test coverage is declared by the source-tree root: App(source_tree_root=...) replaces test_coverage_dir=, and the coverage state lives in .strictmetadata/.cli-test-coverage/ under it.
Context
selfdoc's .strictmetadata/ layout gives strictcli's files a home under one source-tree root. Removing test_coverage_dir= is breaking, which in 0.x is a minor.
Breaking
- [strictcli] Test coverage is declared by the source-tree root.
App(source_tree_root=...)replacestest_coverage_dir=: the app names the root of its source tree, and strictcli keeps the coverage state in.strictmetadata/.cli-test-coverage/under it, with per-process shard files inshards/(never committed) and the committed manifest inmanifest.json(formerlycoverage/andtest-coverage.json). Coverage is on only when that directory exists at construction.test_coverage_dir=is now refused at registration, namingsource_tree_root. To migrate, move.strictcli/test-coverage.jsonto.strictmetadata/.cli-test-coverage/manifest.json(selfdoc layout migratedoes this) and declare the root instead of the directory.
Fixes
- [strictcli] The README now says where an app's check list belongs: in the app's own package as package data, passed with
checks_path=; strictcli has no standard folder or default path for it.
ts-strictcli@v0.44.0
Integer values accept only plain decimal digits with an optional leading minus sign: a plus sign and leading zeros are refused.
Context
The owner ruled one integer form for all three implementations, wherever the framework parses an integer from text (the command line, an environment binding, and config set): 30 and -5 parse, while +30, 030, whitespace, digit separators, and every other form are refused with the existing expected-integer error. Before, Go and TypeScript accepted +30 and 030, and Python also accepted non-ASCII digits. This is breaking, which in 0.x is a minor.
Breaking
- [ts-strictcli] Integer values refuse a plus sign and leading zeros. A
t.intflag, positional argument, environment value, orconfig setvalue now accepts only plain decimal digits with an optional leading minus sign:30and-5parse, while+30,030, and-030are refused withexpected integer, got '<value>'(naming the variable for an environment value). Write30instead of+30or030.
py-strictcli@v0.45.0
Integer values accept only plain decimal digits with an optional leading minus sign: a plus sign and leading zeros are refused.
Context
The owner ruled one integer form for all three implementations, wherever the framework parses an integer from text (the command line, an environment binding, and config set): 30 and -5 parse, while +30, 030, whitespace, digit separators, and every other form are refused with the existing expected-integer error. Before, Go and TypeScript accepted +30 and 030, and Python also accepted non-ASCII digits. This is breaking, which in 0.x is a minor.
Breaking
- [strictcli] Integer values refuse a plus sign, leading zeros, and non-ASCII digits. An
intflag, positional argument, environment value, orconfig setvalue now accepts only plain ASCII decimal digits with an optional leading minus sign:30and-5parse, while+30,030,-030, and digits such as٣٠are refused withexpected integer, got '<value>'(naming the variable for an environment value). Write30instead.
go-strictcli@v0.38.0
Integer values accept only plain decimal digits with an optional leading minus sign: a plus sign and leading zeros are refused.
Context
The owner ruled one integer form for all three implementations, wherever the framework parses an integer from text (the command line, an environment binding, and config set): 30 and -5 parse, while +30, 030, whitespace, digit separators, and every other form are refused with the existing expected-integer error. Before, Go and TypeScript accepted +30 and 030, and Python also accepted non-ASCII digits. This is breaking, which in 0.x is a minor.
Breaking
- [go-strictcli] Integer values refuse a plus sign and leading zeros. An
intflag, positional argument, environment value, orconfig setvalue now accepts only plain decimal digits with an optional leading minus sign:30and-5parse, while+30,030, and-030are refused withexpected integer, got '<value>'(naming the variable for an environment value). Write30instead of+30or030.
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 cal...
py-strictcli@v0.44.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
- [strictcli] ctx.out and the output member.
ctx.out(text)writes a command's answer to stdout, never hidden by--quiet; under--jsonthe same bytes go into the document's newoutputmember, andinterface_versionis now3. Consumers validating the document's key set must acceptoutput. - [strictcli] Prefixed warnings and errors. In human mode
ctx.errorprintserror: <message>andctx.warnprintswarning: <message>on stderr;--jsondiagnostics stay unprefixed. A handler that spelled its ownerror:prefix now prints it twice. - [strictcli] Runtime stdout guard under --json. While a handler runs in machine mode, stdout is redirected: any byte written outside the framework (a
print,os.write(1, ...), an unmanaged child) fails the run with exit 1 and a diagnostic naming the bytes, andos._exitends the run through the framework. Owns-stdout commands must write their document throughctx.document(). - [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.send_signal(signum)sends any signal. - [strictcli]
check --ignore-warningsandrun_checks(ignore_warnings=)are 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. - [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 example@strictcli.flag("m", short="m", ...)to@strictcli.flag("message", short="m", ...); positional argument names and the App name are unaffected. - [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.App(schema_path=...)is removed,project_idnow names the project holding the module that constructed the App (the installed distribution that records it, or in a checkout the nearestpyproject.toml's[project] name) rather than thepyproject.tomlin 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. - [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. - [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
- [strictcli] Early exit.
strictcli.exit_now(code, message)ends a command from anywhere in its call stack through the framework:finallyblocks run, the message is printed aserror: <message>or recorded in the--jsondiagnostics, and a dry run still renders its would-do log.app.call()raisesstrictcli.ExitErrorwith the code and the message, and MCP answersexit code <code>: <message>. - [strictcli] Payload renderers. A command with a
payload_schema=may declarepayload_renderer=; human mode prints the rendering of the supplied payload,--jsonemits only the payload. - [strictcli] Document writer.
ctx.document()gives anowns_stdout=Truecommand a binary writer to the real stdout, unchanged in both modes. - [strictcli] Signals.
app.run()catches SIGINT and SIGTERM while the handler runs and setsctx.canceled; when the handler returns, the command exits 130 or 143 witherror: canceled by signal <SIGNAL>. A second signal gets the default action. - [strictcli] --lint-framework-use. Every app reserves
--lint-framework-use, which scans the program's own package for process exits, direct stdout and stderr writes, argv access, and environment reads, and exits 1 listing each finding as<path>:<line>: <rule>: <message>. - [strictcli]
ExitErrorcarriespayload: what the command supplied throughctx.payloadbefore its early exit,Nonewhen it supplied nothing. - [strictcli] Check values,
failing-checks, and named hooks.app.set_check_value_resolver(fn)lets an app assign each checkstrictcli.CheckValue(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. - [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"}. - [strictcli] Declared runtime requirements.
strictcli.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.
Fix...
go-strictcli@v0.37.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
- [go-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); a child run throughSpawnorRun(Stream(true))under--jsonno longer writes beside it. While a handler runs in machine mode, file descriptor 1 is redirected, and any byte written outside the framework (fmt.Println,os.Stdout, an unmanaged child) fails the run with exit 1 unless the code is already nonzero, plus the error diagnosticstdout written outside the framework: <n> bytes: "<excerpt>". In human modectx.Warnprintswarning: <message>andctx.Errorprintserror: <message>. - [go-strictcli] A child started through
ctx.Effects().Spawnthat is still running when its handler ends is now killed (SIGTERM, then SIGKILL after one second; at once on Windows) 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.Signal(sig)sends any signal. - [go-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 panics 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. - [go-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 exampleStringFlag("m", ..., Short("m"))toStringFlag("message", ..., Short("m")); positional argument names and the App name are unaffected. - [go-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.WithSchemaPathandWithSchemaPathRelativeToRootare removed,project_idis now the main module path from the program's build information rather than thego.modin 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. - [go-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. - [go-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
- [go-strictcli] Early exit, output writers, and signals.
strictcli.ExitNow(code, message)ends a command from anywhere in its call stack through the exit step: deferred functions run, the command exitscode, and the message is printed aserror: <message>(under--json, the last error diagnostic);strictcli.Go(ctx, fn)runs a goroutine whoseExitNowor panic is delivered to the handler,App.Callreturns*strictcli.ExitError, and MCP answersexit code <code>: <message>.ctx.Out(text)writes the command's answer and is never hidden by--quiet;PayloadRenderer(fn)declares a human rendering of the payload;ctx.Document()writes anOwnsStdout()command's document.App.Runcatches SIGINT and SIGTERM while the handler runs and closesctx.Done(); when the handler returns, the command exits 130 or 143 witherror: canceled by signal <SIGNAL>, and a second signal gets the default action. - [go-strictcli] --lint-framework-use. Every app reserves
--lint-framework-use, which scans the program run from its module root (every strictcli main package and the module packages it imports, resolving import aliases) for process exits, direct stdout and stderr writes, argv access, environment reads, andstrictcli.ExitNowinside a rawgofunc literal, and exits 1 listing each finding as<path>:<line>: <rule>: <message>. - [go-strictcli] Under
--jsonthe runtime guard now redirects stdout at the operating-system level on every Unix (including Solaris, illumos, and AIX) and on Windows, so a direct write to the standard output or a child started outside the effects handle is caught there too. The module now requiresgolang.org/x/sys. - [go-strictcli]
ExitErrorgainsPayload: what the command supplied throughctx.Payloadbefore its early exit, nil when it supplied nothing. - [go-strictcli] Check values,
failing-checks, and named hooks.app.SetCheckValueResolver(func(name string) (strictcli.CheckValue, bool))lets an app assign each check aCheckValue{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. - [go-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"}. - [go-strictcli] Declared runtime requirements.
strictcli.NewRequirement(name, help, install, load)declares something a command needs at run time once,WithRequires(...)references it from each command that needs it, andstrictcli.Need(ctx, r)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 `c...