Skip to content

fix(cli): compose --usage schemas from the runtime command tree - #118

Open
2ynn wants to merge 10 commits into
mainfrom
fix/cli-usage-compose-runtime-commands
Open

2ynn wants to merge 10 commits into
mainfrom
fix/cli-usage-compose-runtime-commands

Conversation

@2ynn

@2ynn 2ynn commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Why

Generated CLIs serve --usage (the KDL machine schema) from strings rendered at generation time, one full string per command path. Anything attached to the command tree after generation is invisible to every ancestor's schema:

  • custom: true commands filled in by custom.Register appear in root --usage as bare cmd "<name>" help="…" nodes with no args or flags;
  • leaves that custom.Register mounts under a generated group are missing from root --usage and from the group's --usage;
  • flags added to a generated command at runtime are missing from root --usage.

Each such command's own --usage was correct, because a command marked with usage.MarkDynamic is rendered live from cobra. That live rendering was itself lossy next to the generated text: no suggestions, no default=, flags in alphabetical rather than declared order, generic <flag_name> placeholders, and arg help/requiredness taken from the prompt annotation instead of the generated node.

Tools and agents that discover a CLI through root --usage therefore miss hand-written commands and flags entirely.

What changed

  • templates/cli/includes/usage.ts: templateUsageSchemaMap (one complete KDL string per path, each ancestor repeating its whole subtree, plus dead alias-path keys) is replaced by templateUsageNodeMap. It emits usageNodes: for each command path, the node's head line, aliases, args, flags (keyed by long name, with settings / hidden markers) and declared child names, each rendered once at indent 0.
  • templates/cli/usage.go.stmpl: EmitSchema composes every schema, root and subtrees alike, from the assembled cobra tree (renderUsageNode):
    • generated nodes render verbatim;
    • children keep their declared order; children removed or hidden at runtime are dropped; available children added at runtime are appended in cobra order;
    • NewRootCommand calls usage.MarkGenerated on the tree right before custom.Register, and usage.InitDefaultCommands marks only the help/completion commands cobra itself creates (a custom completion stays live). The mark records the command's flag-set identity. Only marked commands with unchanged flag sets use their table node; anything attached later, including a same-named replacement, and a command whose flags were replaced with ResetFlags, renders live from cobra. Cobra metadata cannot tell an unchanged command from a replacement (generated intents register zero-value cobra defaults while the table documents the effective default), so identity is the signal;
    • a marked command flagged with usage.MarkDynamic (porcelain extending it in place) takes help from its runtime Short, keeps generated arg lines while the speakeasy_prompt_args names match, keeps generated alias and flag lines for those it still has (flags generated visible but hidden at runtime are dropped), and renders runtime-added aliases and visible local flags live, so generated suggestions, default= and declared order survive;
    • settings flags stay on a command's own schema only, as before;
    • only commands under the generated CLI root (matched by name) use the table; a detached command or group renders entirely from cobra.
  • Commands that are unchanged at runtime render byte-identically to the previous output. Each node is stored once, so the generated internal/usage/schema.go is roughly half its previous size.
  • Known limits, documented in the README: on a marked dynamic command, generated arg/flag help text wins over an in-place rewrite of a flag's Usage; commands added in the root custom-commands region run before the mark and count as generated.
  • templates/cli/usage_test.go.stmpl: new tests for runtime commands composed into ancestors (with byte-identity of the rest of the root), a dynamic command with runtime flags (own schema and root), a dynamic root (unchanged output; exact output after a runtime root flag), a generated command hidden at runtime, a detached leaf and group, same-named replacements with and without MarkDynamic (exact own and root blocks), a replaced intent with cobra.NoArgs, the generated mark on built-in and cobra commands, a custom completion registered before cobra's defaults, and a marked command after ResetFlags.
  • pkg/generate/snapshots/cli_empty_body_go_test.go: the schema.go pattern follows the usageNodes entry shape.
  • templates/cli/README.md: --usage section describes the composition rules.
  • Review SDK zSDKs/sdk-cli: internal/usage/schema.go and tests/usage_test.go regenerated. Root --usage now lists the --greeting flag that custom.Register attaches to hello; every other path is unchanged.
  • Changeset: core, cli, fix, patch.

Testing

  • TARGET=primary make test-cli: 2185 tests pass (after merging current main).
  • go test ./pkg/generate/snapshots -run TestSnapCLI: pass.
  • make lint: pass.
  • TARGET=review make build-cli: diff limited to internal/usage/schema.go, tests/usage_test.go and gen.lock.
  • The new tests fail with the previous schema.go and pass with this change.
  • Old vs new --usage output compared for every command path:
    • review SDK: only root differs, by the runtime --greeting flag;
    • a generated CLI whose custom.Register adds top-level commands, group leaves and flags on generated commands: unchanged commands are byte-identical; root and the affected groups gain the runtime commands and flags; dynamic commands keep their generated suggestions, default= and declared order.
  • All emitted schemas parse as KDL v2, including multi-line help values after re-indentation.

Public-safety check

  • This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
  • Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (.agents/skills/public-repo-communication/SKILL.md).
  • Generated fixtures and review SDK changes are public-safe.
  • I reviewed git diff --check.

2ynn added 2 commits October 16, 2026 19:58
Root and group `--usage` were served from KDL strings rendered at generation
time, one full string per command path. Commands attached after generation
(`custom: true` placeholders filled in by `custom.Register`, leaves mounted
under generated groups, flags added to generated commands) were missing from
every ancestor's schema or appeared as bare nodes, while their own `--usage`
was correct. A command marked dynamic was rendered live from cobra, which
dropped `suggestions`, `default=`, declared flag order and placeholders, and
took arg help/requiredness from the prompt annotation.

The generator now emits `usageNodes`: for each command path, its head line,
aliases, args, flags (keyed by long name, with settings/hidden markers) and
declared child names, rendered once at indent 0. `EmitSchema` composes every
schema from the assembled cobra tree:

- generated nodes render verbatim; children keep declared order, children
  removed at runtime are dropped, available children added at runtime are
  appended;
- a command marked with `usage.MarkDynamic`, or absent from the table, takes
  `help` from the runtime `Short`, keeps the generated alias/arg/flag lines
  for everything it still has (flags hidden at runtime are dropped) and
  renders only runtime-added aliases and local flags live;
- settings flags stay on a command's own schema only, as before.

Commands unchanged at runtime render byte-identically to the previous
output. Each node is stored once instead of being repeated in every
ancestor's string, roughly halving the generated schema source.

Tests: `TestUsageSchema_ComposesRuntimeCommands`,
`TestUsageSchema_DynamicCommandKeepsGeneratedLines` and
`TestUsageSchema_DynamicRootUnchanged`; the empty-body snapshot test follows
the new table shape. README `--usage` section updated; review SDK
regenerated.
- Live-rendered aliases, args and flags take the node's item prefix, so a
  dynamic root lists runtime root flags at indent 0 instead of 2.
- A generated child hidden at runtime is dropped from its parent's schema,
  matching children added at runtime, which already required an available
  command.
- The root node is a parentless command with subcommands; any other
  parentless command renders itself from cobra instead of the root schema.

Tests: `TestUsageSchema_DynamicRoot` (replaces
`TestUsageSchema_DynamicRootUnchanged` and adds the runtime root flag),
`TestUsageSchema_HiddenGeneratedCommandDropped`,
`TestUsageSchema_DetachedDynamicCommand`. Review SDK regenerated.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread templates/templates/cli/README.md Outdated
Comment thread templates/templates/cli/usage.go.stmpl Outdated
2ynn added 2 commits October 16, 2026 20:35
A parentless command group was treated as the CLI root because root
detection only checked `Parent() == nil && HasSubCommands()`, so
`EmitSchema` on a detached group printed the generated root's metadata and
dropped the group's own `cmd` node. Only commands whose `Root()` is the
generated CLI root (`usageRootName`) now use the generated `usageNodes`
table; a detached command and its children render from cobra.

README: state precisely what a dynamic command keeps (generated arg lines;
alias and flag lines it still has; generated `hide=#true` flags) and that
detached commands render entirely from cobra.

`TestUsageSchema_DetachedCommand` replaces
`TestUsageSchema_DetachedDynamicCommand` and adds a detached group whose
child shares a generated command's name. Review SDK regenerated.
A dynamic command reused every generated arg line whenever the node had
args, and every generated flag line whose long name still existed. A
same-named replacement registered with `cobra.NoArgs` and
`BoolP("id", "x", ...)` therefore still advertised `arg "id"` and
`-i --id <id>`, in its own schema and in every ancestor's.

`usageNodes` now records, per generated flag, its shorthand and whether it
takes an operand or repeats, and each arg line carries its name. For a
dynamic command:

- generated arg lines are kept only while the runtime
  `speakeasy_prompt_args` names match them; otherwise args render live;
- a generated flag line is kept only while cobra's flag has the same
  shorthand, operand and repeatability; otherwise that flag renders live in
  its generated position.

Generated `default=` values are not compared: they are the documented
effective defaults, which intents may apply outside cobra, so cobra's
`DefValue` differs for unchanged flags.

Tests: `TestUsageSchema_ReplacedCommandDropsStaleFlagSyntax` (own and root
schema), `TestUsageSchema_ReplacedIntentDropsStaleArgs`;
`TestUsageSchema_DynamicRoot` now asserts the exact root output after a
runtime root flag is added. README and review SDK updated.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread templates/templates/cli/usage.go.stmpl Outdated
Comment thread zSDKs/sdk-cli/tests/usage_test.go
`MarkDynamic` merges a command's generated lines with its runtime state,
and nothing else rendered fully live, so a same-named replacement of a
generated command could still advertise generated `default=`,
`suggestions`, `env` and help text it no longer has. Cobra metadata cannot
tell an unchanged command from a replacement: generated intents register
flags with zero-value defaults while the table documents the effective
default the intent applies itself.

Use command identity instead. `NewRootCommand` calls `usage.MarkGenerated`
on the tree right before `custom.Register`, and on cobra's `help` and
`completion` once they are created. Only marked commands use their
`usageNodes` entry; anything attached later, including a same-named
replacement, renders live from cobra while keeping the node's declared
child order. `MarkDynamic` keeps its role for porcelain that extends a
generated command in place.

This supersedes the per-flag shorthand/operand/repeat signature: pflag
cannot remove a flag, so on a marked command every table flag is the
generated flag object. The `short`/`value`/`repeat` table fields and
`usageFlagUnchanged` are removed.

Tests: `TestUsageSchema_ReplacedCommandRendersLive` (exact own and root
block for an unmarked replacement), `TestUsageSchema_GeneratedCommandsMarked`;
`TestUsageSchema_ReplacedIntentDropsStaleArgs` no longer marks the
replacement dynamic. README and review SDK updated.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread templates/templates/cli/README.md Outdated
…e validation docs

- `TestUsageSchema_ReplacedIntentDropsStaleArgs` now asserts the exact own
  schema of the replacement and that root `--usage` renders it as a bare
  node at its depth, without the generated `arg` lines.
- README: under `--usage`, required-flag validation cannot fail
  (`output.InstallErrorHandling` clears the required-flag annotations via
  `usage.DisarmRequiredFlags` and re-arms them after `RunE`); only Cobra's
  flag-group validators can still win.

Review SDK regenerated.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 4 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would require human review. Refactors generated CLI --usage to compose schemas from the runtime tree so runtime-added commands and flags appear, with tests; it changes the public --usage contract and makes compatibility tradeoffs requiring human sign-off.

View guided diff | Turn on auto-fix | Re-trigger cubic

@2ynn
2ynn marked this pull request as ready for review October 19, 2026 16:39
@2ynn
2ynn requested a review from a team as a code owner October 19, 2026 16:39
@2ynn
2ynn requested a review from speakeasybot October 19, 2026 16:39
…ets in --usage

- `NewRootCommand` marked every child named `help` or `completion` as
  generated after `custom.Register`. Cobra keeps an existing `completion`
  command, so a custom one was rendered from the built-in table entry and
  lost its help and flags. `usage.InitDefaultCommands` now wraps
  `InitDefaultHelpCmd`/`InitDefaultCompletionCmd` and marks only the
  commands cobra created.
- The generated mark now records the identity of the command's flag sets.
  A marked command whose flags are replaced with `ResetFlags` renders live
  instead of reusing generated flag lines for flags it no longer has.
- `MarkDynamic`'s doc comment describes the merge it performs.

Tests: `TestUsageSchema_CustomCompletionRendersLive`,
`TestUsageSchema_ResetFlagsRendersLive`. README and review SDK updated.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 8 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would require human review. Refactors generated CLI --usage to compose schemas from the runtime tree so runtime-added commands and flags appear; this deliberately changes a machine-readable public contract, and the dismissed hidden-flag fix is not visible in the truncated diff.

View guided diff | Turn on auto-fix | Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant