fix(cli): make --help work on every subcommand - #135
Conversation
There was a problem hiding this comment.
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_runtelemetry 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.
|
@claude /review |
|
Claude finished @thecodedrift's task in 4m 18s —— View job Review:
|
Both findings addressed in 24e54de; the two inline threads carry the details and are resolved.
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
— AI Coding Agent |
24e54de to
900ae6f
Compare
Acknowledged — the overview matches the change, and it carries no finding to action. Copilot's one inline finding (the missing — AI Coding Agent |
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
900ae6f to
308b7cc
Compare
taskless check --helpdid not print help — it rancheck. The same held for every subcommand: citty implements--helponly insiderunMain, and the entry dispatches withrunCommand, so--helpfell through as an unrecognized flag and the command body executed.taskless init --helpinstalled skills;check --helpmigrated the repo's.taskless/scaffold. Meanwhile the root's own usage — which works, via a hand-rolledshowUsage— tells the reader to runtaskless <command> --help.Mechanism
--help/-his intercepted insrc/index.tsbefore dispatch, and rendered with citty's exportedshowUsage, so no usage text is hand-written:src/util/help.tswalks positionals throughsubCommands, hopping as many times as they resolve, and renders the deepest command that resolved with its parent — sotaskless auth login --helpprintslogin's usage, notauth's. citty's ownresolveSubCommandis not exported, and its walk takes the first non--token, which would resolvetaskless -d /tmp check --helpto/tmp.src/util/argv.tsholds the one copy of the argv rules that make that correct: a token following-d/--diris a flag value, not a positional, and--ends the options so everything after it is a positional (which is howtaskless check -- -hstill scans a path literally named-h). The rootrunhandler,resolveCommandName(telemetry), andcheck'sextractPositionalPathsall now go through it;checkpasses its own value-taking flag--timeout.-his safe to claim — the only alias defined anywhere in the CLI isd.Why not
runMainIt looks like a one-line fix and it is not. Both of
runMain's exit paths callprocess.exit()—process.exit(0)on help,process.exit(1)in its catch — andprocess.exitdoes not runfinallyblocks. Thefinallyinsrc/index.tsis what emitscli_run, the per-invocation telemetry denominator, "on both success and failure, so no command has to remember to." AdoptingrunMainwould silently stop reporting every help and every failed invocation, and would swallow theCLIError/reportedhandling. The interception here returns normally instead, so thefinallystill 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 baretaskless --, and any unknown flag with no subcommand. Only--help/-hnow 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, matchingcli.test.ts, plus unit coverage for the scanner:--helpand-hon 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 --helprenderslogin, notauth.-d <tmp> check --helpresolvescheck.check -d <tmp> --json -- -hruns the scan rather than printing usage — the--escape hatch. Also confirmed against the build without this fix, which printed usage.--helpis unchanged.cli_runis still captured for a help invocation (in-process, entry imported with mocked telemetry) — therunMainguard, 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-detectalready requires thattaskless detect --help"be recognized and print its usage", which this makes true.Fixes #134