Skip to content

feat(cli): generate a --no- negation for boolean inputs - #27

Merged
bdelanghe merged 1 commit into
mainfrom
claude/next-jybxja
Aug 18, 2026
Merged

feat(cli): generate a --no- negation for boolean inputs#27
bdelanghe merged 1 commit into
mainfrom
claude/next-jybxja

Conversation

@bdelanghe

@bdelanghe bdelanghe commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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

parseArgs accepts --no-{field} for every boolean input:

input: z.object({ changedOnly: z.boolean().default(true) }),
// `check`                  →  changedOnly: true   (the default)
// `check --no-changedOnly` →  changedOnly: false

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 known set was load-bearing for two different questions, so it is split rather than widened in place:

set answers contents
known "is this a declared input field?" input keys — gates positional names, supplies negation targets
knownFlags "is this an accepted flag name?" known ∪ the negation names

So 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.
  • an input field that shadows a generated negation (a boolean loud beside a field literally named no-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.
  • a negation of anything not boolean or not declared (--no-slug, --no-bogus) — an unknown flag, as before.

A negation consumes no token, so myverb --no-loud alice still reads alice as a positional. Booleans stay scalar: --loud --no-loud is last-wins, the same rule every other scalar flag follows.

One adjacent fix, in the same renderer

toHelp now renders a boolean as --loud / --no-loud and prints each field's default. That surfaced an existing mislabel: z.toJSONSchema lists a defaulted field in required (the parsed output always carries it), so help marked --limit as (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.md requests a minor bump.

Verification

Ran the repo's own CI command locally:

bun install --frozen-lockfile && bun run build && bun test && bunx jsr publish --dry-run --allow-slow-types

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.

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
bdelanghe marked this pull request as ready for review August 18, 2026 02:36
@bdelanghe bdelanghe changed the title feat(cli): generate --no-<field> for boolean inputs feat(cli): generate a --no- negation for boolean inputs Aug 18, 2026
@bdelanghe
bdelanghe merged commit 860d3bd into main Aug 18, 2026
9 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Front Desk Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

CLI: no negation for a boolean input with default(true)

2 participants