feat(cli): generate a --no- negation for boolean inputs - #27
Merged
Conversation
A `z.boolean().default(true)` could not be turned off from the CLI: only presence-is-true worked, so "on by default, opt out on the command line" forced authors to declare a SECOND override flag and compute the effective value by hand — two flags controlling one boolean dimension purely to work around the missing negation. `parseArgs` now accepts `--no-<field>` for every boolean input. Every boolean gets one, not only the `default(true)` ones: which spelling an author reaches for follows from the default, but a default is a value the verb may change, and a conditionally-generated flag would vanish from every script using it the moment someone flipped one. The negation is a DERIVED CLI name, not an input key, so the one set that answered both "is this an accepted flag?" and "is this a legal positional?" is split in two. `known` keeps its old meaning (input fields — the positional check and the negation targets); a new `knownFlags` carries the `--no-` names and gates the flag check alone. Widening the CLI vocabulary therefore cannot quietly make `positionals: ["no-changedOnly"]` legal, which it should not be. Holding the package's strict-mapping line, three things halt rather than resolve to something surprising: `--no-<field>=value` (the negation IS the value), an input field that would shadow a generated negation (a boolean `loud` beside a field named `no-loud` — a spec error, checked up front like an undeclared positional), and `--no-<field>` for anything not boolean or not declared (an unknown flag, as before). A negation consumes no token, so `myverb --no-loud alice` still reads `alice` as a positional, and booleans stay scalar — `--loud --no-loud` is last-wins. `toHelp` renders a boolean as `--loud / --no-loud` and prints each field's default. That surfaced an existing mislabel worth correcting in the same renderer: `z.toJSONSchema` lists a DEFAULTED field in `required` (the parsed output always carries it), so help said `--limit <integer> (required)` for a field that is precisely the omittable one — and beside a printed default it would have read as a flat contradiction. A field with a default is no longer also marked required. Closes #12 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cHeA3WaJ6Gc9DYvzbicAf
bdelanghe
marked this pull request as ready for review
August 18, 2026 02:36
--no-<field> for boolean inputs--no- negation for boolean inputs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12.
(Placeholders below are written
{field}rather than in angle brackets: the first revision of this description was created with angle brackets and they were silently stripped, leaving--no-and--limit (required). The code and commit message use the normal angle-bracket spelling.)The problem
A
z.boolean().default(true)had no way to be turned off from the CLI — only presence-is-true worked. Expressing "on by default, opt out on the command line" forced authors to declare a second override flag and compute the effective value by hand: two flags controlling one boolean dimension, purely to work around the missing negation.What this does
parseArgsaccepts--no-{field}for every boolean input:Every boolean gets one, not only the
default(true)ones. Which spelling an author reaches for follows from the default, but a default is a value the verb may change — a conditionally-generated flag would vanish from every script using it the moment someone flipped one.Two sets, deliberately not one
Per the design note on #12: the negation is the first derived CLI name — a valid flag that is deliberately not an input key. The single
knownset was load-bearing for two different questions, so it is split rather than widened in place:knownknownFlagsknown∪ the negation namesSo widening the CLI vocabulary cannot quietly make
positionals: ["no-changedOnly"]legal. There is a test holding that line specifically.Strict mapping is preserved
Three things halt rather than resolving to something surprising:
--no-changedOnly=false— the negation is the value, so it accepts none.loudbeside a field literally namedno-loud) — a spec error, checked up front like an undeclared positional, so it fails on every invocation rather than only the ones that pass the flag.--no-slug,--no-bogus) — an unknown flag, as before.A negation consumes no token, so
myverb --no-loud alicestill readsaliceas a positional. Booleans stay scalar:--loud --no-loudis last-wins, the same rule every other scalar flag follows.One adjacent fix, in the same renderer
toHelpnow renders a boolean as--loud / --no-loudand prints each field's default. That surfaced an existing mislabel:z.toJSONSchemalists a defaulted field inrequired(the parsed output always carries it), so help marked--limitas(required)for a field that is precisely the omittable one. Beside a printed default it would have read as a flat contradiction —(required) (default: 20)— so a field with a default is no longer also marked required.This is included rather than split out because it is one line in the function this PR already rewrites, and leaving it would mean shipping visibly self-contradicting help text.
Scope
Additive for existing specs — no currently-valid invocation changes meaning. Help text changes for defaulted and boolean fields.
.release/cli-boolean-negation.mdrequests a minor bump.Verification
Ran the repo's own CI command locally:
66 pass, 0 fail (17 new), build clean, JSR dry-run
Success. All 9 checks are green on CI.No
[settings]or[org]toggles are required by this change.