Repository navigation
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 andcommand '<path>' needs <name> (<help>), which is not available: <reason>; install it: <install>.--helpshows aRequirements:section and the help document arequireslist.
Fixes
- [go-strictcli] The
config set,config init, andconfig editcommands report their own errors through the error writer: printed once aserror: <message>and carried aserrordiagnostics under--jsoninstead of bare stderr lines;config init's refusal now readsconfig init: config file already exists: <path>as in the other implementations. - [go-strictcli] Programs built with strictcli compile for targets where
intis 32 bits wide (GOARCH=386,arm,mips, andmipsle) instead of failing withchoiceMaxMagnitude ... overflows int. - [go-strictcli]
check --liston a checks.toml that declares no checks printsNo checks defined.instead of a bare table header, as the Python and TypeScript implementations do.