Skip to content

fix(cli): global flags visible, position-tolerant, and surface usage errors#10

Merged
bradfair merged 6 commits into
mainfrom
fix/global-flags-help-visibility
Apr 21, 2026
Merged

fix(cli): global flags visible, position-tolerant, and surface usage errors#10
bradfair merged 6 commits into
mainfrom
fix/global-flags-help-visibility

Conversation

@bradfair
Copy link
Copy Markdown
Contributor

@bradfair bradfair commented Apr 21, 2026

Summary

Three related global-flag bugs:

  • ana org show --no-such-flag exited 1 silently — ErrUsage errors were suppressed on stderr. Now printed.
  • ana org show --json silently succeeded with empty output because flag.FlagSet.Parse stopped at the first positional, so post-verb globals never reached ParseGlobal. New StripGlobals walks argv linearly and recognizes globals regardless of position, with -- terminator support, =value / space-form, and unknown-flag passthrough to the leaf.
  • ana --help / ana <leaf> --help did not list --json / --endpoint / --token-file / --profile. A single globalFlagsHelp() block is now appended to both.

Independent of the feat/group-inheritable-flags branch; can merge in either order.

Summary by CodeRabbit

  • New Features

    • Global flags (--profile, --endpoint, --token-file, --json, --token-file) are recognized anywhere on the command line and help now includes a consistent "Global Flags:" section.
  • Bug Fixes

    • Leaf usage errors are correctly surfaced to stderr; help/error reporting avoids double-printing diagnostics.
    • Duplicate global flags resolve with “last occurrence wins.”
    • Unknown-profile and unknown-command errors are reported consistently.
  • Tests

    • Added comprehensive unit and end-to-end tests for global-flag parsing, help output, and usage/error behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b62c4be1-dab6-4f77-af68-06984006d0c0

📥 Commits

Reviewing files that changed from the base of the PR and between 1dd59f4 and 9112c90.

📒 Files selected for processing (2)
  • cmd/ana/main.go
  • internal/cli/dispatch.go

📝 Walkthrough

Walkthrough

Adds a global-flag registry and exported StripGlobals to extract globals from anywhere in argv; switches Dispatch and main to use it; narrows stderr suppression so leaf usage errors are not silently swallowed; appends a canonical "Global Flags:" section to root and leaf help output.

Changes

Cohort / File(s) Summary
CLI runtime
cmd/ana/main.go, cmd/ana/main_test.go
run() now uses cli.StripGlobals(args) and only suppresses cli.ErrHelp/cli.ErrReported for stderr printing; added TestRun_LeafUsageErrorReturned and updated TestRun_UnknownProfile to assert cli.ErrReported.
Global flag registry & extraction
internal/cli/root.go
Introduce globalFlagSpec/globalFlagRegistry, globalFlagsHelp(), and exported StripGlobals(args []string) (Global, []string, error) to parse/remove known globals from any argv position, handle --name, --name=value, --name value, -- terminator, validate/take values, and apply last-wins semantics.
Dispatch, help & error reporting
internal/cli/dispatch.go, internal/cli/cli.go, internal/cli/errors.go
Dispatch/cli switched to StripGlobals; usage/unknown-command errors are now joined with cli.ErrReported so callers can detect already-reported diagnostics; leaf -h/--help and root help now append a blank line and the canonical globalFlagsHelp() after command help/list. Added exported sentinel cli.ErrReported.
Tests: StripGlobals & help behavior
internal/cli/cli_test.go
Extensive tests added for StripGlobals (positions, formats, -- handling, duplicates, passthrough unknowns, parse error mapping), registry sync checks, and help-output assertions ensuring a single Global Flags: section and proper error mapping to ErrUsage/ErrReported.

Sequence Diagram

sequenceDiagram
    participant Client as Client/Main
    participant Dispatch as CLI Dispatch
    participant Strip as StripGlobals
    participant Cmd as Command
    participant FlagSet as Leaf FlagSet

    rect rgba(100,200,150,0.5)
    Note over Client,Cmd: Globals may be anywhere in argv
    Client->>Dispatch: Dispatch(args)
    Dispatch->>Strip: StripGlobals(args)
    Strip->>Strip: Scan tokens, set known globals, preserve rest
    Strip-->>Dispatch: (Global, restArgs, error)
    Dispatch->>Cmd: Route using restArgs
    Cmd->>FlagSet: Parse restArgs
    FlagSet-->>Cmd: (flags, usage/error)
    Cmd-->>Client: Execute or return error
    end

    rect rgba(200,150,100,0.5)
    Note over Dispatch,Cmd: Help emission includes globals
    Client->>Dispatch: Dispatch(args with -h/--help)
    Dispatch->>Cmd: Route to command help
    Cmd->>Cmd: cmd.Help() (+ inherited Flags block)
    Cmd->>Dispatch: Request globalFlagsHelp()
    Dispatch-->>Client: Combined help output + "Global Flags:"
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hopped through argv, twitching my nose,
Found globals hiding where the verb goes.
I stripped them tidy, left the rest to run,
Help now shows flags and usage greets the sun.
Tiny paws, tidy flags — job nicely done.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the summary and type, but is missing Conventional Commits confirmation, make lint/cover assertions, and docs update confirmation. Complete the checklist items in the description: confirm PR title follows Conventional Commits, assert make lint and make cover pass, and confirm docs updates if needed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: fixing global flags visibility, position tolerance, and error surfacing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/global-flags-help-visibility

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 21, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 21, 2026
main() skipped stderr emission for any error wrapping ErrUsage on the
theory the error had already been reported. Leaf flag errors bubble up
through ParseFlags wrapped in ErrUsage, but every leaf's FlagSet uses
io.Discard output — so nothing was ever printed. Result: misplaced or
unknown leaf flags exited 1 with no output.

Print every non-ErrHelp error to stderr. ErrHelp stays skipped since its
text is already written to stdout by the help-rendering code path.
Stdlib flag.FlagSet.Parse stops at the first positional, so ParseGlobal
only saw flags placed before the verb. `ana --json org show` worked;
`ana org show --json` fell through to the leaf FlagSet, which rejected
the unknown flag and (since 1a) now surfaces the error properly but is
still the wrong outcome.

Add StripGlobals: a single linear walk over argv that consumes every
known global wherever it appears (before, after, or interleaved with
the verb path and its positionals), with `--` as a hard terminator.
Everything unknown passes through in original order so the leaf's
FlagSet still reports precise unknown-flag errors.

Dispatch uses StripGlobals instead of ParseGlobal; cmd/ana/main's
config-resolution pre-pass uses it too so a trailing --profile/--endpoint
/--token-file still influences config load. ParseGlobal stays because
it's still the reference FlagSet shape for the registry-sync test and a
few existing callers.

TestGlobalFlagsRegistrySync prevents drift: any new global added to
ParseGlobal must also land in globalFlagRegistry.
Globals (--json/--endpoint/--token-file/--profile) were invisible in
help output — only discoverable by reading the source. Add a canonical
Global Flags section rendered by globalFlagsHelp (one source of truth
for naming/usage) and append it to:

- RootHelp (ana --help / ana with no args)
- Leaf --help (via dispatchChild's --help short-circuit)

Group.Help is left alone: children are already listed, and adding a
global block at every Group level would duplicate noise without
discovery value. The Global Flags block renders once at the root and
once on each leaf's help — the surfaces users actually land on.
gofmt complained about a stray blank line between globalFlagsHelp and the
Global type doc. Same pass drops the two string-flag fs.Set error paths in
StripGlobals — stdlib stringValue.Set cannot fail, so those branches were
dead code that kept internal/cli at 97.9% and broke the 100% gate. Keep the
bool-flag fs.Set error path (reachable via --json=notbool) and add tests
for it plus the Dispatch StripGlobals-error surface path.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmd/ana/main.go (1)

57-105: ⚠️ Potential issue | 🟠 Major

Skip config resolution for help paths.

run() now resolves global.Profile before cli.Dispatch gets a chance to short-circuit help. That makes ana help --profile ghost and ana org show --help --profile ghost fail with unknown profile instead of rendering help, which is the opposite of the current dispatch contract. Use the stripped rest to detect help requests and bypass the config-resolution pre-pass for them.

Based on learnings: The --help / -h scan and rendering must occur in dispatchChild BEFORE Command.Run is called; Groups are skipped so help reaches the deepest leaf, and bare positional help is left alone so leaves can receive it as an argument.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ana/main.go` around lines 57 - 105, The pre-dispatch config Resolve in
run() is happening even for help requests, causing unknown-profile errors when
users ask for help; update the logic that follows cli.StripGlobals(args) to
inspect the returned rest slice for help indicators (e.g. "-h", "--help", or the
literal "help" as the first positional) and, if a help request is detected, skip
loading cfgPath/config.Load and calling config.Resolve(env, loaded,
global.Profile) so cli.Dispatch/dispatchChild can short-circuit to render help;
keep the existing behavior for non-help paths (still use global.TokenFile,
config.DefaultPath, and Resolve) and reference the existing symbols global,
rest, cfgPath, loaded, config.Load, and config.Resolve when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/ana/main.go`:
- Around line 35-40: The main() post-dispatch error handler is double-reporting
usage/dispatch diagnostics; update the check around fmt.Fprintln(stdio.Stderr,
err) to also skip cli.ErrUsage in addition to cli.ErrHelp so errors already
written by cli.Dispatch or run() aren't printed again. Locate the error handling
in main() (the block that currently checks errors.Is(err, cli.ErrHelp) and calls
fmt.Fprintln) and add a guard that returns/does not print when errors.Is(err,
cli.ErrUsage) (and keep the existing cli.ErrHelp check) so only truly unreported
errors are emitted.

---

Outside diff comments:
In `@cmd/ana/main.go`:
- Around line 57-105: The pre-dispatch config Resolve in run() is happening even
for help requests, causing unknown-profile errors when users ask for help;
update the logic that follows cli.StripGlobals(args) to inspect the returned
rest slice for help indicators (e.g. "-h", "--help", or the literal "help" as
the first positional) and, if a help request is detected, skip loading
cfgPath/config.Load and calling config.Resolve(env, loaded, global.Profile) so
cli.Dispatch/dispatchChild can short-circuit to render help; keep the existing
behavior for non-help paths (still use global.TokenFile, config.DefaultPath, and
Resolve) and reference the existing symbols global, rest, cfgPath, loaded,
config.Load, and config.Resolve when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e312a924-43f7-4f2c-a2ac-8c3c5dab7faa

📥 Commits

Reviewing files that changed from the base of the PR and between 8a1c33f and 4ac73ce.

⛔ Files ignored due to path filters (1)
  • internal/cli/CLAUDE.md is excluded by !**/*.md
📒 Files selected for processing (6)
  • cmd/ana/main.go
  • cmd/ana/main_test.go
  • internal/cli/cli.go
  • internal/cli/cli_test.go
  • internal/cli/dispatch.go
  • internal/cli/root.go

Comment thread cmd/ana/main.go Outdated
Comment thread internal/cli/root.go
…print

CodeRabbit found two issues post-rebase:

1. Dispatch and run() both write diagnostics to stderr, and main now also
   prints on any non-ErrHelp error (introduced in 1a so leaf FlagSets whose
   output is io.Discard get their errors surfaced). Result: bad globals,
   unknown commands, and unknown profiles produce two stderr lines — the
   callee's clean message plus main's wrapped repeat.

   Fix: add cli.ErrReported sentinel. Dispatch + run wrap their errors with
   errors.Join(err, ErrReported) after emitting the diagnostic themselves.
   main's fallback stderr print skips any error carrying ErrReported.
   Leaf flag errors still flow through main's print path unchanged.

2. StripGlobals only recognized --name; stdlib flag.FlagSet.Parse treats
   -name and --name as equivalent (per its docs: "One or two dashes may
   be used; they are equivalent"). A user running `ana -json org show`
   would have -json silently passed through to the org-show FlagSet,
   which then fails with "flag provided but not defined".

   Fix: parseFlagToken now accepts both single- and double-dash forms.
   Bare -, --, ---, -=val noise tokens are left in rest untouched so the
   leaf receives them as-is.
@bradfair bradfair enabled auto-merge April 21, 2026 16:34
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/ana/main.go`:
- Line 104: Wrap and join the existing errors so rerr's chain is preserved while
still wrapping cli.ErrUsage for exit-code mapping: replace the current return
with a join that includes a wrapped cli.ErrUsage plus rerr and cli.ErrReported
(e.g. use errors.Join(fmt.Errorf("%w", cli.ErrUsage), rerr, cli.ErrReported)),
referencing rerr, cli.ErrUsage, cli.ErrReported, errors.Join and fmt.Errorf to
locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c68d762d-566b-44e3-8c7b-c556869ba6f1

📥 Commits

Reviewing files that changed from the base of the PR and between 4ac73ce and 1dd59f4.

⛔ Files ignored due to path filters (1)
  • internal/cli/CLAUDE.md is excluded by !**/*.md
📒 Files selected for processing (6)
  • cmd/ana/main.go
  • cmd/ana/main_test.go
  • internal/cli/cli_test.go
  • internal/cli/dispatch.go
  • internal/cli/errors.go
  • internal/cli/root.go

Comment thread cmd/ana/main.go Outdated
CodeRabbit nit + golangci-lint errorlint: formatting the inner error with
%s in fmt.Errorf loses its chain, so downstream errors.Is against the
inner sentinel fails. ErrUnknownProfile is checked upstream so behavior
was fine today, but the pattern is brittle.

Go 1.20+ fmt.Errorf accepts multiple %w verbs; switching "%w: %s" →
"%w: %w" preserves both sentinels (ErrUsage for exit-code mapping plus
the inner error for future errors.Is callers) while keeping the rendered
string identical.
@bradfair bradfair added this pull request to the merge queue Apr 21, 2026
Merged via the queue into main with commit 33bb9c3 Apr 21, 2026
10 checks passed
@bradfair bradfair deleted the fix/global-flags-help-visibility branch April 21, 2026 16:43
@hpt-bot hpt-bot Bot mentioned this pull request Apr 21, 2026
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