Skip to content

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

Description

@thecodedrift

--help on a subcommand runs the subcommand

taskless check --help does not print help. It runs check. The same is true of every subcommand — --help is not recognized as a help request anywhere below the root, so it is parsed as an unknown flag and the command body executes.

This was found while quoting real --help output for the README (#130): asking check for help migrated the repo's own .taskless/ scaffold as a side effect.

Mechanism

packages/cli/src/index.ts:125 calls citty's runCommand. citty implements --help only inside runMain (citty@0.1.6/dist/index.mjs:435); runCommand has no help branch at all.

The root command appears to work only because it hand-rolls the behavior — index.ts:88-91 calls showUsage(cmd) when the flags present are not init flags. That is the root reimplementing for itself what runMain would have given the whole tree.

Why this is a defect and not a design choice

citty appends this line to the usage of any command that has subcommands (dist/index.mjs:425):

Use `taskless <command> --help` for more information about a command.

So taskless --help — which does work — instructs the reader to use a flag the CLI does not implement. The CLI advertises its own broken path.

Worth separating two things that are easy to conflate:

  • The agent-facing path is fine and is not what this is about. taskless agent <topic> is the documented way an agent discovers how to run a command, and it works. This is a human-facing gap.
  • The check case is the mild one. The migration it triggers (packages/cli/src/commands/check.ts:324) is deliberate, documented, idempotent, and gated on .taskless/ already existing. Not a bug on its own.

The reason it matters is that the dispatch path is identical for every subcommand, so taskless init --help should launch the installer rather than describe it. (Not verified by running it, for the obvious reason.)

Fix: intercept before dispatch, render with citty's own showUsage

Do not switch to runMain. It is a package deal, and both its exit paths call process.exit()process.exit(0) on help (dist/index.mjs:439) and process.exit(1) in its own catch (:456). process.exit does not run finally blocks, so adopting it silently deletes the telemetry wrapper at index.ts:160-175 — the block whose comment calls cli_run "the per-invocation denominator: emitted exactly once here, on both success and failure, so no command has to remember to." Every failed invocation would stop reporting. It would also swallow the CLIError / reported handling that exists so a throw site which did not print still produces output and a non-zero exit.

Trading the telemetry denominator for a help flag is not a good trade, and it is the kind of regression no test would catch.

Instead, keep runCommand and intercept --help / -h before dispatch, handing off to citty's exported showUsage:

  • showUsage and renderUsage are public (export { createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage }). No help text is hand-written — citty still does the formatting.
  • The only piece that is not public is resolveSubCommand, so resolution has to walk positionals through subCommands locally.
  • Return normally rather than exiting, so the finally block still emits cli_run.

Two details the walk has to get right:

  1. It must hop more than once. auth (commands/auth.ts:158) and rules (commands/rules.ts:681) define their own subCommands, so taskless auth login --help needs to descend twice. Walk positionals while each one resolves to a child, and render the deepest command that resolved, with its parent.
  2. A flag value is not a positional. Every command in the CLI aliases --dir to -d, so the walk must skip the token after -d/--dir — the same rule index.ts:68-73 already applies in hasPositional. Reuse that logic rather than writing a second copy that can drift.

-h is safe to claim: no command defines it. The only alias in the CLI is d (the sole other "h" in the tree is a URL parameter in rules/rule-hash.ts:96, unrelated to argv).

Also

Once --help works at every depth, citty's Use \taskless --help`` line becomes true and can stay. If for any reason this is closed without fixing, that line should be suppressed instead — advertising a flag that runs the command is worse than not having the flag.

Tests

  • --help and -h on a leaf subcommand print usage and run nothing (assert the command's side effects did not happen, not just that output appeared)
  • --help on a nested subcommand (auth login) renders that subcommand's usage, not auth's
  • taskless -d <path> check --help resolves check, not <path>
  • the root's existing --help behavior is unchanged
  • cli_run telemetry is still emitted for a help invocation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions