Skip to content

Releases: stricttools/strictcli

go-strictcli@v0.39.2

Choose a tag to compare

@smm-h smm-h released this 11 Oct 11:20

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 than GET, HEAD, or OPTIONS is now a hard error naming the method, returned before any request is sent. Before, a read_only command could declare a DELETE (or any other method) a read, and under --dry-run the request was really sent while the preview said no changes were made.

go-strictcli@v0.39.1

Choose a tag to compare

@smm-h smm-h released this 08 Oct 16:28

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 check and failing-checks commands 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

Choose a tag to compare

@smm-h smm-h released this 08 Oct 11:51

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. Each app.Test(argv) call captures its output in its own streams, including the stdout and stderr of children run through Spawn or a streamed Run, and the state a dispatch writes belongs to that dispatch instead of the App: 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 to os.Stdout directly (fmt.Println) is no longer captured in Result.Stdout, and under --json Test() no longer arms the stray-stdout guard, which stays on Run(); write through ctx.Out, as --lint-framework-use already requires.
  • [go-strictcli] Test coverage is declared by the source-tree root. WithSourceTreeRoot(path) replaces WithTestCoverageDir(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 in shards/ (never committed) and the committed manifest in manifest.json (formerly coverage/ and test-coverage.json). Coverage is on only when that directory exists at construction. WithTestCoverageDir is now refused at registration, naming WithSourceTreeRoot. To migrate, move .strictcli/test-coverage.json to .strictmetadata/.cli-test-coverage/manifest.json (selfdoc layout migrate does this) and declare the root instead of the directory.
  • [go-strictcli] The check context factory receives the dispatch's *Context. SetCheckContext now takes func(*Context) (CheckContext, error): check and failing-checks call 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 *Context and return (context, nil).

Features

  • [go-strictcli] Stdin and Timeout effect options. strictcli.Stdin(data) on Run and Spawn writes 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) on Run and Spawned.Wait kills the child once d has passed and returns an error naming the command and the timeout; a non-positive d is refused, and dry mode renders (timeout: 30s).
  • [go-strictcli] Declared reads, a settable HTTP client, and redaction. strictcli.Read() on HTTP and strictcli.Observe() on Run declare 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); without Read() 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 an httptest server; without it the framework uses its own client with a 60-second timeout instead of http.DefaultClient, and strictcli.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-run output; an HTTP body renders only as its byte count. strictcli.Mode(mode) on Write sets the file's permission bits, rendered as (mode: 0600).
  • [go-strictcli] HTTP bodies streamed from a file. strictcli.BodyFile(path) on HTTP sends a regular file as the request body, read while the request is sent instead of into memory first, with the file size as the Content-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 with Body is refused at the call. Dry mode renders (body: N bytes from <path>), adding at offset N for a range, and the record carries body_file, body_offset, and body_bytes.
  • [go-strictcli] A command can receive the arguments after -- unparsed. WithArgsAfterSeparator(name, help) hands every token after the first bare -- to the handler through ctx.ArgsAfterSeparator(), untouched (a --dry-run or a second -- there included), while the tokens before it parse as before, so a stray word is still refused. Help lists it under Arguments: as -- <name>..., the help document publishes it as args_after_separator (its command default null joins the defaults block), and Call and 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 its Timeout returns an error matching strictcli.ErrTimedOut with errors.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 ask ctx.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-run can run a program that writes for itself (a package manager's cache) there. A command that does not declare one is refused. The check and failing-checks commands 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 with go:embed and passed to WithChecksEmbed; 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-bypass check skips _test.go files. 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

Choose a tag to compare

@smm-h smm-h released this 07 Oct 00:22

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=...) replaces test_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 in shards/ (never committed) and the committed manifest in manifest.json (formerly coverage/ and test-coverage.json). Coverage is on only when that directory exists at construction. test_coverage_dir= is now refused at registration, naming source_tree_root. To migrate, move .strictcli/test-coverage.json to .strictmetadata/.cli-test-coverage/manifest.json (selfdoc layout migrate does 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

Choose a tag to compare

@smm-h smm-h released this 01 Oct 18:02

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.int flag, positional argument, environment value, or config set value now accepts only plain decimal digits with an optional leading minus sign: 30 and -5 parse, while +30, 030, and -030 are refused with expected integer, got '<value>' (naming the variable for an environment value). Write 30 instead of +30 or 030.

py-strictcli@v0.45.0

Choose a tag to compare

@smm-h smm-h released this 01 Oct 18:02

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 int flag, positional argument, environment value, or config set value now accepts only plain ASCII decimal digits with an optional leading minus sign: 30 and -5 parse, while +30, 030, -030, and digits such as ٣٠ are refused with expected integer, got '<value>' (naming the variable for an environment value). Write 30 instead.

go-strictcli@v0.38.0

Choose a tag to compare

@smm-h smm-h released this 01 Oct 18:01

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 int flag, positional argument, environment value, or config set value now accepts only plain decimal digits with an optional leading minus sign: 30 and -5 parse, while +30, 030, and -030 are refused with expected integer, got '<value>' (naming the variable for an environment value). Write 30 instead of +30 or 030.

ts-strictcli@v0.43.0

Choose a tag to compare

@smm-h smm-h released this 29 Sep 17:45

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 cal...
Read more

py-strictcli@v0.44.0

Choose a tag to compare

@smm-h smm-h released this 29 Sep 17:45

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 --json the same bytes go into the document's new output member, and interface_version is now 3. Consumers validating the document's key set must accept output.
  • [strictcli] Prefixed warnings and errors. In human mode ctx.error prints error: <message> and ctx.warn prints warning: <message> on stderr; --json diagnostics stay unprefixed. A handler that spelled its own error: 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, and os._exit ends the run through the framework. Owns-stdout commands must write their document through ctx.document().
  • [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.send_signal(signum) sends any signal.
  • [strictcli] check --ignore-warnings and run_checks(ignore_warnings=) 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.
  • [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 @strictcli.flag("m", short="m", ...) to @strictcli.flag("message", short="m", ...); positional argument names and the App name are unaffected.
  • [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. App(schema_path=...) is removed, project_id now names the project holding the module that constructed the App (the installed distribution that records it, or in a checkout the nearest pyproject.toml's [project] name) rather than the pyproject.toml 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.
  • [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.
  • [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

  • [strictcli] Early exit. strictcli.exit_now(code, message) ends a command from anywhere in its call stack through the framework: finally blocks run, the message is printed as error: <message> or recorded in the --json diagnostics, and a dry run still renders its would-do log. app.call() raises strictcli.ExitError with the code and the message, and MCP answers exit code <code>: <message>.
  • [strictcli] Payload renderers. A command with a payload_schema= may declare payload_renderer=; human mode prints the rendering of the supplied payload, --json emits only the payload.
  • [strictcli] Document writer. ctx.document() gives an owns_stdout=True command a binary writer to the real stdout, unchanged in both modes.
  • [strictcli] Signals. app.run() catches SIGINT and SIGTERM while the handler runs and sets ctx.canceled; when the handler returns, the command exits 130 or 143 with error: 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] ExitError carries payload: what the command supplied through ctx.payload before its early exit, None when it supplied nothing.
  • [strictcli] Check values, failing-checks, and named hooks. app.set_check_value_resolver(fn) lets an app assign each check strictcli.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.
  • [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"}.
  • [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, 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.

Fix...

Read more

go-strictcli@v0.37.0

Choose a tag to compare

@smm-h smm-h released this 29 Sep 17:45

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 `c...
Read more