Skip to content

fix(cli): make --help work on every subcommand - #135

Merged
thecodedrift merged 2 commits into
mainfrom
fix/help-flag-on-subcommands
Aug 21, 2026
Merged

fix(cli): make --help work on every subcommand#135
thecodedrift merged 2 commits into
mainfrom
fix/help-flag-on-subcommands

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Aug 21, 2026

Copy link
Copy Markdown
Member

taskless check --help did not print help — it ran check. The same held for every subcommand: citty implements --help only inside runMain, and the entry dispatches with runCommand, so --help fell through as an unrecognized flag and the command body executed. taskless init --help installed skills; check --help migrated the repo's .taskless/ scaffold. Meanwhile the root's own usage — which works, via a hand-rolled showUsage — tells the reader to run taskless <command> --help.

Mechanism

--help/-h is intercepted in src/index.ts before dispatch, and rendered with citty's exported showUsage, so no usage text is hand-written:

  • src/util/help.ts walks positionals through subCommands, hopping as many times as they resolve, and renders the deepest command that resolved with its parent — so taskless auth login --help prints login's usage, not auth's. citty's own resolveSubCommand is not exported, and its walk takes the first non-- token, which would resolve taskless -d /tmp check --help to /tmp.
  • src/util/argv.ts holds the one copy of the argv rules that make that correct: a token following -d/--dir is a flag value, not a positional, and -- ends the options so everything after it is a positional (which is how taskless check -- -h still scans a path literally named -h). The root run handler, resolveCommandName (telemetry), and check's extractPositionalPaths all now go through it; check passes its own value-taking flag --timeout.
  • -h is safe to claim — the only alias defined anywhere in the CLI is d.

Why not runMain

It looks like a one-line fix and it is not. Both of runMain's exit paths call process.exit()process.exit(0) on help, process.exit(1) in its catch — and process.exit does not run finally blocks. The finally in src/index.ts is what emits cli_run, the per-invocation telemetry denominator, "on both success and failure, so no command has to remember to." Adopting runMain would silently stop reporting every help and every failed invocation, and would swallow the CLIError/reported handling. The interception here returns normally instead, so the finally still runs. The test named as a regression guard pins exactly that.

Root help

The root's hand-rolled branch stays. It is not redundant: it also covers taskless --json, taskless --version, a bare taskless --, and any unknown flag with no subcommand. Only --help/-h now bypass it, and they render identically (showUsage(main) with no parent). Verified unchanged, along with the init-flags delegation and the non-TTY preamble.

Tests

packages/cli/test/help-flag.test.ts — spawns the built CLI, matching cli.test.ts, plus unit coverage for the scanner:

  • --help and -h on a leaf assert the side effects did not happen: init --help -d <tmp> leaves the temp directory empty. Confirmed this fails against the pre-fix build, which wrote .agents/ and .taskless/ there.
  • auth login --help renders login, not auth.
  • -d <tmp> check --help resolves check.
  • check -d <tmp> --json -- -h runs the scan rather than printing usage — the -- escape hatch. Also confirmed against the build without this fix, which printed usage.
  • the root's --help is unchanged.
  • cli_run is still captured for a help invocation (in-process, entry imported with mocked telemetry) — the runMain guard, commented as such.

Verification

pnpm lint, pnpm typecheck, pnpm test (647 tests, 52 files), node --test .github/scripts/*.test.cjs (156 pass), pnpm openspec validate --all --strict (24 passed, 0 failed) — all green locally, on the rebased branch.

No OpenSpec change: nothing under openspec/specs/ is contradicted. cli-detect already requires that taskless detect --help "be recognized and print its usage", which this makes true.

Fixes #134

Copilot AI lite review requested due to automatic review settings August 21, 2026 05:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a long-standing CLI defect where --help/-h on subcommands was treated as an unknown flag and the subcommand executed (including side effects), by intercepting help requests before citty’s runCommand dispatch and rendering usage via citty’s showUsage without process.exit() (preserving the entrypoint finally telemetry emission).

Changes:

  • Add pre-dispatch help interception in src/index.ts, resolving nested subcommands and rendering usage via a new helper (showResolvedUsage).
  • Centralize raw-argv scanning (splitRawArguments, hasHelpFlag) and reuse it for both subcommand resolution and telemetry command-name derivation.
  • Add regression tests asserting both “no side effects on help” and that cli_run telemetry is still emitted for help invocations.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/cli/src/index.ts Intercepts --help/-h before runCommand to render usage without exiting (preserves telemetry finally).
packages/cli/src/util/help.ts Implements nested subcommand resolution and delegates rendering to citty’s showUsage.
packages/cli/src/util/argv.ts Introduces shared raw-argv scanning utilities used by help resolution, root logic, and telemetry.
packages/cli/src/telemetry-run.ts Refactors command-name extraction to reuse the shared argv scanner.
packages/cli/test/help-flag.test.ts Adds integration + in-process regression tests for help behavior and telemetry emission.
.changeset/help-flag-on-subcommands.md Documents the behavioral fix as a patch changeset for @taskless/cli.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/cli/src/util/argv.ts Outdated
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 4m 18s —— View job


Review: fix(cli): make --help work on every subcommand

Read the full diff (gh pr diff 135), the entire src/index.ts (including the unchanged parts of the run handler the diff sits inside), src/telemetry-run.ts, src/util/argv.ts, src/util/help.ts, the new test file, and cross-checked against commands/check.ts's own argv parsing. The core mechanism is sound: intercepting --help/-h before runCommand and returning normally (instead of adopting citty's runMain, which process.exit()s and would silently drop the cli_run telemetry denominator) is the right call, and the regression test explicitly pinning that is a good guard. The nested-subcommand resolution via the shared argv scanner correctly fixes the -d /tmp check --help mis-resolution bug this PR set out to fix.

Two issues found, posted inline:

1. packages/cli/src/util/argv.ts (line 25) — real regression, not just theoretical. splitRawArguments/hasHelpFlag don't honor the POSIX -- end-of-options marker. check has its own extractPositionalPaths (commands/check.ts:69-103) that explicitly does honor --, specifically so users can pass paths beginning with -. Before this PR, taskless check -- -h ran check against a literal path -h; after this PR, the new pre-dispatch interception sees -h in flags (since it doesn't know -- ended the flags) and shows usage instead of running check at all — silently breaking the one documented escape hatch this exact command supports. Same Copilot review comment landed on this independently; I confirmed it's reachable and traced the concrete before/after behavior change. Worth fixing before merge, or at minimum a follow-up issue if the maintainer judges this edge case acceptable for now.

2. packages/cli/src/index.ts (line 79) — minor reuse gap. The onlyInitFlags scan still hand-rolls its own -d/--dir value-skipping logic instead of using the new shared splitRawArguments, so the "one copy of this rule" goal the PR states isn't fully achieved — this is a third, uncentralized copy. Not a behavior bug (the two implementations currently agree), just a missed simplification.

Everything else checked out:

  • showResolvedUsage's walk-and-render logic in util/help.ts is correct for the cases it's designed for; the "stop at first non-child token" behavior is an accepted, documented tradeoff mirroring citty's own (unexported) resolver.
  • resolveCommandName's pre-existing lack of -- handling is untouched by this PR (same gap existed before), so it's not a new regression — only the new hasHelpFlag path introduces a fresh, reachable break.
  • The root's hand-rolled help/usage branch and the finally-based telemetry emission are correctly left alone; control flow confirmed help never reaches main's own run() handler.
  • Test file: good coverage for the leaf/nested/-d cases and the runMain regression guard, but has no case for the -- scenario above — which would have caught issue taskless rules create: spec version upgrade loop with init #1.
  • No CLAUDE.md violations found (direct imports, no barrel exports, exported types stay exported for declaration emit).
    · branch fix/help-flag-on-subcommands

Comment thread packages/cli/src/util/argv.ts
Comment thread packages/cli/src/index.ts Outdated
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task in 4m 18s —— Review: fix(cli): make --help work on every subcommand…"
#135 (comment)

Both findings addressed in 24e54de; the two inline threads carry the details and are resolved.

  1. -- end-of-options (real regression — confirmed). I reproduced it against the previous build: taskless check -d <tmp> --json -- -h printed usage; it now runs the scan. splitRawArguments honors -- and will not consume it as the value of -d. Taking the rest of your suggestion, check.ts's extractPositionalPaths is deleted — it delegates to the shared scanner with its own value-taking flag --timeout, which was the only command-specific part. New CLI test for the -- case (it fails against the previous commit's build) plus unit tests for the scanner and hasHelpFlag.
  2. onlyInitFlags reuse. Converted, with one adjustment: the scanner keeps -- in flags and the check is flags.every((flag) => DIR_FLAGS.has(flag)) against the exported set. A ---stripped flag list with inline literals would have silently turned taskless -- from usage into the install wizard.

Copilot's review summary is a description of the PR with no finding in it, so there is nothing to action there; its one inline finding is the same -- issue and is resolved.

pnpm lint, pnpm typecheck, pnpm test (647 tests), node --test .github/scripts/*.test.cjs (145), and pnpm openspec validate --all --strict (24/0) all pass locally.

— AI Coding Agent

@thecodedrift
thecodedrift force-pushed the fix/help-flag-on-subcommands branch from 24e54de to 900ae6f Compare August 21, 2026 15:52
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @copilot-pull-request-reviewer — "## Pull request overview — Fixes a long-standing CLI defect where --help/-h on subcommands was treated as an unknown flag and the subcommand executed…"

Acknowledged — the overview matches the change, and it carries no finding to action. Copilot's one inline finding (the missing -- end-of-options handling) was real: fixed, replied to, and that thread is resolved.

— AI Coding Agent

thecodedrift and others added 2 commits August 21, 2026 10:43
citty implements --help only inside runMain; the CLI dispatches with
runCommand, so --help fell through as an unrecognized flag and the command
body ran (`taskless init --help` installed skills; `check --help` migrated
the scaffold). Help is now intercepted before dispatch and rendered with
citty's exported showUsage, walking positionals through subCommands so
nested commands render their own usage.

runMain was rejected deliberately: both of its exit paths call process.exit,
which skips finally blocks — including the one that emits cli_run, the
per-invocation telemetry denominator. The interception returns normally.

The positional scan (a value after -d/--dir is not a positional) moved to
src/util/argv.ts, shared by the help walk, the root run handler, and
resolveCommandName, which each had their own copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Review found a real regression in the pre-dispatch help interception: the
shared scanner did not know about the POSIX end-of-options marker, so
`taskless check -- -h` was read as a help request and printed usage instead
of scanning a path literally named `-h` — the escape hatch check.ts was
built to support. splitRawArguments now treats everything after `--` as a
positional and will not swallow `--` as the value of `-d`.

check.ts's extractPositionalPaths was the second copy of that rule (the
complete one). It now calls the shared scanner, passing its own value-taking
flag `--timeout`, and the root's onlyInitFlags scan — a third copy — goes
through the same scanner's flags.

Tests: unit coverage for the scanner and hasHelpFlag, plus a CLI case
asserting `check --json -- -h` still runs check. Verified the case fails
against the previous commit's build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift
thecodedrift force-pushed the fix/help-flag-on-subcommands branch from 900ae6f to 308b7cc Compare August 21, 2026 17:43
@thecodedrift
thecodedrift merged commit b0dd139 into main Aug 21, 2026
2 checks passed
@thecodedrift
thecodedrift deleted the fix/help-flag-on-subcommands branch August 21, 2026 17:50
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.

--help on any subcommand runs the command instead of printing usage

2 participants