Skip to content

go-strictcli@v0.37.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

  • [go-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); a child run through Spawn or Run(Stream(true)) under --json no 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 diagnostic stdout written outside the framework: <n> bytes: "<excerpt>". In human mode ctx.Warn prints warning: <message> and ctx.Error prints error: <message>.
  • [go-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; at once on Windows) 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.Signal(sig) sends any signal.
  • [go-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 panics 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.
  • [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 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 StringFlag("m", ..., Short("m")) to StringFlag("message", ..., Short("m")); positional argument names and the App name are unaffected.
  • [go-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. WithSchemaPath and WithSchemaPathRelativeToRoot are removed, project_id is now the main module path from the program's build information rather than the go.mod 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.
  • [go-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.
  • [go-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

  • [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 exits code, and the message is printed as error: <message> (under --json, the last error diagnostic); strictcli.Go(ctx, fn) runs a goroutine whose ExitNow or panic is delivered to the handler, App.Call returns *strictcli.ExitError, and MCP answers exit 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 an OwnsStdout() command's document. App.Run catches SIGINT and SIGTERM while the handler runs and closes ctx.Done(); when the handler returns, the command exits 130 or 143 with error: 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, and strictcli.ExitNow inside a raw go func literal, and exits 1 listing each finding as <path>:<line>: <rule>: <message>.
  • [go-strictcli] Under --json the 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 requires golang.org/x/sys.
  • [go-strictcli] ExitError gains Payload: what the command supplied through ctx.Payload before 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 a CheckValue{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.
  • [go-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"}.
  • [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, and strictcli.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 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

  • [go-strictcli] The config set, config init, and config edit commands report their own errors through the error writer: printed once as error: <message> and carried as error diagnostics under --json instead of bare stderr lines; config init's refusal now reads config init: config file already exists: <path> as in the other implementations.
  • [go-strictcli] Programs built with strictcli compile for targets where int is 32 bits wide (GOARCH=386, arm, mips, and mipsle) instead of failing with choiceMaxMagnitude ... overflows int.
  • [go-strictcli] check --list on a checks.toml that declares no checks prints No checks defined. instead of a bare table header, as the Python and TypeScript implementations do.