Umbrella issue tracking design decisions that should be made (or revisited) before the 1.0 release. Items in this list typically involve a breaking change to defaults, so they are deferred until v1.0 where breaking changes are acceptable.
Open decisions
(Add more items here as they come up.)
Boolean negation: default visibility
Today, when a boolean field does not set negation, the auto-generated --no-<name> (and camelCase --no<Name>) form is accepted by the parser but never advertised in --help, generated Markdown docs, or shell completions. It is effectively a hidden feature.
PR #393 adds explicit control via negation:
negation: "custom-name" — replace --no-X with a custom name (rendered in help/docs/completion).
negation: true — opt-in to render the default --no-X in help/docs/completion.
negation: false — disable negation entirely.
The default for unset negation is still the current behavior (parser accepts --no-X, but it is hidden from help/docs/completion).
Decision to make for v1
Should the default change so that --no-X is shown in help/docs/completion automatically for every boolean field, with negation: false to opt out?
Pros
- Higher discoverability: users see the negation form without reading documentation.
- More consistent: the parser already accepts
--no-X, so advertising it matches reality.
- Less ceremony: no need to set
negation: true to surface a feature that already works.
Cons (why this is deferred to v1)
- Breaking change for help/docs output: every existing CLI that uses politty would see new lines appear in its help and Markdown docs for every boolean field.
- All golden doc snapshots in downstream projects (and in this repo's playground) would need regeneration.
- Users who deliberately keep their help short would have to add
negation: false to many fields.
If we change the default, ship it as part of the 1.0 release with a migration note (e.g. "set negation: false on boolean fields whose negation should remain hidden").
Command-level defaults API
There are policies — currently negation, potentially others later (placeholders, env-var prefixes, alias casing, etc.) — that users may want to set once at the command level instead of repeating on every arg() call. We need a single, consistent way to express those defaults so that help, generated docs, and shell completions all observe the same policy.
Constraints
- Whatever shape we pick must apply uniformly across
runMain / runCommand (help, completion) and generateDoc / renderArgsTable (docs). Otherwise docs drift from the help users actually see at runtime.
- Per-field meta on
arg() must always win over command-level defaults.
- The shape must scale as more policies are added in the future (placeholders, env prefixes, ...).
- Runtime and tooling code paths must stay tree-shake-able.
generateDoc (and other build/test-time tooling) must not be reachable from the runtime entry point graph, even after the defaults API is in place.
Candidate placements
Three candidates for where the defaults are declared. Each example uses a nested defaults object for illustration, but the placement decision is independent of that surface detail.
A. Attach to the command via defineCommand
const cmd = defineCommand({
name: "my-cli",
defaults: { negation: (cliName) => `disable-${cliName}` },
args: z.object({ ... }),
run: ({ ... }) => { ... },
});
- Pros: defaults travel with the command, so
runMain(cmd) and generateDoc({ command: cmd }) both see them without extra plumbing. Subcommand-level overrides are natural. Runtime and tooling stay in separate import graphs.
- Cons: mixes "what the command is" with "how to render it". Multiple commands sharing the same defaults need either inheritance or duplication.
B. Factory pattern (split: runtime vs. tooling)
A single factory that returns both runtime (runMain) and tooling (generateDoc) would force the tooling module into the runtime bundle — undesirable for tree-shaking. The factory must therefore be split into separate runtime / tooling entry points sharing the same defaults config.
// runtime (in bin/cli.ts)
import { configurePolittyRuntime } from "politty";
const { defineCommand, runMain } = configurePolittyRuntime({
defaults: { negation: (cliName) => `disable-${cliName}` },
});
// tooling (in scripts/gen-docs.ts)
import { configurePolittyDocs } from "politty/docs";
const { generateDoc } = configurePolittyDocs({
defaults: { negation: (cliName) => `disable-${cliName}` },
});
- Pros: FP-pure, no module-global state. Each factory statically guarantees that all entry points it returns share the same defaults.
- Cons: most invasive — breaking change to all imports.
defaults must be declared twice (once per factory), so the source of truth has to be a shared constant. Forces a decision on what happens when a command defined under one factory is run by another. Two factories cannot easily share types.
C. Pass at every entry point (runMain, runCommand, generateDoc, ...)
runMain(cmd, { defaults: { negation: (cliName) => `disable-${cliName}` } });
generateDoc({ command: cmd, defaults: { negation: (cliName) => `disable-${cliName}` } });
- Pros: explicit, no hidden state, no factory ceremony. Runtime and tooling import graphs stay independent.
- Cons: easy for
runMain's and generateDoc's defaults to drift out of sync; users have to remember to pass them at every call site.
Rejected alternatives
- Module-level setter (
setDefaults(...)): implicit ordering, race-prone in tests, hard to reason about.
- Single factory returning both runtime and tooling entry points: pulls
generateDoc into the runtime bundle. See option B for the split-factory variant that avoids this.
Open sub-questions
- Subcommand inheritance: do defaults cascade from the root command into subcommands automatically, or must each subcommand re-declare? (Probably cascade with per-level override.)
- Validation: what happens when a default produces a name that conflicts with another field's
cliName / alias / negation? (Probably reuse the existing validateDuplicateNegations pass — it already runs after resolution.)
- Type-level: should the defaults object be statically typed against the keys of
BaseArgMeta so adding a new policy is a single-point change?
Decision to make for v1
- Pick one of A / B / C (or a hybrid) as the placement.
- Confirm the inheritance rule and the validation contract.
- Decide which policies are supported from day one (
negation is the motivating case; others can be added later as long as the surface is stable).
Umbrella issue tracking design decisions that should be made (or revisited) before the 1.0 release. Items in this list typically involve a breaking change to defaults, so they are deferred until v1.0 where breaking changes are acceptable.
Open decisions
--no-Xin help/docs/completion for boolean fields (context below)(Add more items here as they come up.)
Boolean negation: default visibility
Today, when a boolean field does not set
negation, the auto-generated--no-<name>(and camelCase--no<Name>) form is accepted by the parser but never advertised in--help, generated Markdown docs, or shell completions. It is effectively a hidden feature.PR #393 adds explicit control via
negation:negation: "custom-name"— replace--no-Xwith a custom name (rendered in help/docs/completion).negation: true— opt-in to render the default--no-Xin help/docs/completion.negation: false— disable negation entirely.The default for unset
negationis still the current behavior (parser accepts--no-X, but it is hidden from help/docs/completion).Decision to make for v1
Should the default change so that
--no-Xis shown in help/docs/completion automatically for every boolean field, withnegation: falseto opt out?Pros
--no-X, so advertising it matches reality.negation: trueto surface a feature that already works.Cons (why this is deferred to v1)
negation: falseto many fields.If we change the default, ship it as part of the 1.0 release with a migration note (e.g. "set
negation: falseon boolean fields whose negation should remain hidden").Command-level defaults API
There are policies — currently
negation, potentially others later (placeholders, env-var prefixes, alias casing, etc.) — that users may want to set once at the command level instead of repeating on everyarg()call. We need a single, consistent way to express those defaults so that help, generated docs, and shell completions all observe the same policy.Constraints
runMain/runCommand(help, completion) andgenerateDoc/renderArgsTable(docs). Otherwise docs drift from the help users actually see at runtime.arg()must always win over command-level defaults.generateDoc(and other build/test-time tooling) must not be reachable from the runtime entry point graph, even after the defaults API is in place.Candidate placements
Three candidates for where the defaults are declared. Each example uses a nested
defaultsobject for illustration, but the placement decision is independent of that surface detail.A. Attach to the command via
defineCommandrunMain(cmd)andgenerateDoc({ command: cmd })both see them without extra plumbing. Subcommand-level overrides are natural. Runtime and tooling stay in separate import graphs.B. Factory pattern (split: runtime vs. tooling)
A single factory that returns both runtime (
runMain) and tooling (generateDoc) would force the tooling module into the runtime bundle — undesirable for tree-shaking. The factory must therefore be split into separate runtime / tooling entry points sharing the samedefaultsconfig.defaultsmust be declared twice (once per factory), so the source of truth has to be a shared constant. Forces a decision on what happens when a command defined under one factory is run by another. Two factories cannot easily share types.C. Pass at every entry point (
runMain,runCommand,generateDoc, ...)runMain's andgenerateDoc's defaults to drift out of sync; users have to remember to pass them at every call site.Rejected alternatives
setDefaults(...)): implicit ordering, race-prone in tests, hard to reason about.generateDocinto the runtime bundle. See option B for the split-factory variant that avoids this.Open sub-questions
cliName/ alias / negation? (Probably reuse the existingvalidateDuplicateNegationspass — it already runs after resolution.)BaseArgMetaso adding a new policy is a single-point change?Decision to make for v1
negationis the motivating case; others can be added later as long as the surface is stable).