Skip to content

fix(cli): register own flags on commands that have sub-commands - #10622

Draft
luvkapur wants to merge 2 commits into
masterfrom
fix-schema-json-flag
Draft

fix(cli): register own flags on commands that have sub-commands#10622
luvkapur wants to merge 2 commits into
masterfrom
fix-schema-json-flag

Conversation

@luvkapur

@luvkapur luvkapur commented Aug 14, 2026

Copy link
Copy Markdown
Member

What

Running a command that has sub-commands with any of its own flags failed as an unknown argument and printed help instead of running:

$ bit schema "ui/tooltip" --json
bit.js schema <pattern>
...
Unknown argument: json

Same for bit lane -d, bit lane --merged, bit lane --not-merged, bit capsule flags, and so on. The flags were also missing from the command's --help output.

Cause

CLIParser.parseCommandWithSubCommands must override the yargs builder in order to register the sub-commands. That override discards YargsAdapter.builder, which is what declares the command's own flags, positionals and examples — so under yargs.strict() those flags are unknown arguments.

The fix delegates to the adapter's builder instead of re-implementing part of it. That also removes the need for the explicit getGlobalOptions call, since the adapter's builder already includes the global options.

Scoping the flags to the parent (second commit)

The first commit registered the parent's flags as yargs globals (.option() defaults to global: true), which propagate into sub-command contexts. Where a sub-command declares the same flag name with a different arity, the parent's definition shadowed it:

$ bit lane remove <lane> --remote --silent --force
Not enough arguments following: remote

LaneCmd has --remote <remote-scope-name> (takes a value) while LaneRemoveCmd has a boolean --remote. CI's lane-export-skip-main-history e2e caught this.

A command's own flags are now registered with global: false when it has sub-commands. Sub-commands never inherited these flags before — they were not registered at all — so this restores their previous behaviour exactly, while the parent gains its own. The global options (log, safe-mode, token, pager) stay global, which is how bit lane list --log=error keeps working.

Why -h kept working

Worth stating, since it makes the bug look narrower than it is. --help/-h is registered in configureGlobalFlags() on the root parser with yargs' default global: true, so it propagates into every command context and never passes through the per-command builder that the override discards. --log, --safe-mode and --token survive for the same reason (re-added inside the override by #5585).

On the unfixed build, bit lane -h shows only those four global flags — the whole Options group with -d/--details, -j/--json, -r/--remote, --merged, --not-merged is absent — while bit lane -d fails with Unknown argument: d.

That globality is the mechanism is confirmed by the counter-example: version is registered with an explicit global: false, and bit lane --version is rejected as unknown while bit --version works at top level. -h also escapes strict validation because setHelpMiddleware is registered with applyBeforeValidation = true, so it prints and exits before validation runs.

When it broke

I confirmed the latency empirically: with strict() commented out on unfixed master, bit schema --json returns valid JSON.

Tests

New cli-parser.spec.ts, 6 cases: the parent's own flags (long form and alias), a parent flag that takes a value, sub-command routing with its own flags, global flags on the parent, and a parent/sub-command flag-name collision with mismatched arity (the CI failure above, reproduced as a unit test).

Verified the spec fails only for the right reasons — against unfixed code the two parent-flag cases fail while sub-command routing and global flags still pass.

The spec puts the logger in daemon mode, because a parse failure otherwise calls process.exit(1) and would kill the whole mocha run instead of reporting a failing test.

Verification

npm run lint clean. After bit compile teambit.harmony/cli, against the real CLI:

  • bit schema "ui/tooltip" --json → valid JSON
  • bit lane -d, bit lane --not-merged → run instead of printing help
  • bit lane --help now lists -d, --details, -j, --json, --merged, --not-merged
  • sub-commands still route: bit lane list, bit capsule list, bit aspect list, bit app list, bit envs list, bit schema diff --help
  • bit lane --remote some-scope → the parent's value-taking flag still works
  • bit lane list --log=error → global flags still reach sub-commands
  • bit lane remove <lane> --remote --silent --force → no longer a parse error
  • strict mode still rejects real typos: bit lane --definitely-not-a-flagUnknown arguments

The e2e that caught the sub-command regression, lane-export-skip-main-history (forked-from lane deleted upstream), passes locally: 2 passing.

Note

bit lane --json now parses the flag and reaches the command, which then reports command "lane" doesn't implement "json" methodLaneCmd declares a json flag but only implements report. That is a separate pre-existing gap in LaneCmd, left alone here.

🤖 Generated with Claude Code

luvkapur and others added 2 commits August 14, 2026 15:46
Running a command that has sub-commands with any of its own flags failed as an
unknown argument and printed help instead, e.g. `bit schema <pattern> --json`,
`bit lane -d`, `bit lane --not-merged`. The flags were also missing from the
command's `--help` output.

`parseCommandWithSubCommands` has to override the yargs builder to register the
sub-commands, which discards the adapter's builder — the one that declares the
command's own flags, positionals and examples. Delegate to it instead of
re-implementing part of it.

Introduced in #4359 (Commander -> Yargs), where the override dropped the
parent's options. It stayed invisible because the adapter's handler reads every
key off argv, so unregistered flags still reached the command, until #4429 added
`yargs.strict()` two weeks later and they became unknown arguments. #5585 later
patched the same override for the global flags only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Registering the parent's flags made them yargs globals, which propagate into
sub-command contexts. Where a sub-command declares the same flag name with a
different arity the parent's definition shadowed it, so
`bit lane remove <lane> --remote --silent --force` failed with
"Not enough arguments following: remote" - the parent's `--remote <scope-name>`
takes a value while `lane remove --remote` is a boolean.

Register a command's own flags with `global: false` when it has sub-commands.
Sub-commands never inherited these flags before, since they were not registered
at all, so this restores their previous behavior. The global options (log,
safe-mode, token, pager) stay global.

Caught by e2e lane-export-skip-main-history, which passes locally with this
change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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