Skip to content

fix: improve ax error handling around missing commands or unknown flags#8116

Merged
ndhoule merged 8 commits intomainfrom
sr/error-improvements
Mar 27, 2026
Merged

fix: improve ax error handling around missing commands or unknown flags#8116
ndhoule merged 8 commits intomainfrom
sr/error-improvements

Conversation

@sean-roberts
Copy link
Copy Markdown
Contributor

🎉 Thanks for submitting a pull request! 🎉

Summary

Today:

  • missing commands try to do a closest match check and put the user in an interactive mode to confirm if that's ok, causing issues in CI or agent work
  • invalid command flags return a flat error saying it's unknown and tells the user to use the command --help flag to figure out what to do. This is fine for humans but during CI or when agents use it, it's better to immediately print out the help information for the command to inform the user not only of the invalid flag but what the valid flags are. For agents this saves a few round trips and tokens to run the help command and then fix things, they can immedately fix things.
Screenshot 2026-03-27 at 11 56 14 AM

With this change:

  • if the user is non-interactive, don't put them in an interactive mode, just give a flat error for unknown commands with a suggestion. In this mode we could show all commands.
  • show the netlify CLI help or the command help, whichever is relevant.
Screenshot 2026-03-27 at 11 59 30 AM Screenshot 2026-03-27 at 11 59 06 AM

For us to review and ship your PR efficiently, please perform the following steps:

  • Open a bug/issue before writing your code 🧑‍💻. This ensures we can discuss the changes and get feedback from everyone that should be involved. If you`re fixing a typo or something that`s on fire 🔥 (e.g. incident related), you can skip this step.
  • Read the contribution guidelines 📖. This ensures your code follows our style guide and
    passes our tests.
  • Update or add tests (if any source code was changed or added) 🧪
  • Update or add documentation (if features were changed or added) 📝
  • Make sure the status checks below are successful ✅

A picture of a cute animal (not mandatory, but encouraged)

@sean-roberts sean-roberts self-assigned this Mar 27, 2026
@sean-roberts sean-roberts requested a review from a team as a code owner March 27, 2026 16:58
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aced35be-3077-4306-9d23-3c262b346b70

📥 Commits

Reviewing files that changed from the base of the PR and between 3f12a30 and b5d55c4.

⛔ Files ignored due to path filters (1)
  • tests/integration/commands/didyoumean/__snapshots__/didyoumean.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • src/commands/base-command.ts
  • src/commands/main.ts
  • src/utils/command-error-handler.ts
  • tests/integration/commands/error-handling/error-handling.test.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/integration/commands/error-handling/error-handling.test.ts

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved error messages and guidance for invalid CLI options, unknown commands, and missing arguments
    • Enhanced error handling in non-interactive environments with clearer help output

Walkthrough

The PR adds centralized handling for Commander option-related errors and makes command-name optional. BaseCommand.createCommand now accepts an optional name and wraps registered actions to set Commander’s exitOverride at action-registration time. An exitOverride handler checks for option errors via new utilities (isOptionError, handleOptionError) and invokes help/output behavior for non-interactive contexts. runProgram now catches CommanderError and exits with its exitCode. main command creation also installs an exitOverride and skips interactive confirmation in non-interactive environments. A new integration test suite exercises these error scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: improving CLI error handling for missing commands and unknown flags in non-interactive environments.
Description check ✅ Passed The description is well-related to the changeset, detailing the problem being solved (interactive mode issues in CI/agents, flat errors for invalid flags) and the proposed solutions implemented in the code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 sr/error-improvements

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 27, 2026

📊 Benchmark results

Comparing with 3cefa0e

  • Dependency count: 1,059 (no change)
  • Package size: 354 MB (no change)
  • Number of ts-expect-error directives: 356 (no change)

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

🧹 Nitpick comments (1)
src/commands/base-command.ts (1)

68-69: Extract the Commander option-error policy into one shared helper.

The handled code list and the log()/outputHelp()/log() branch now live in both this file and src/commands/main.ts. Centralizing that logic will keep root and subcommand behavior aligned and lets the flow stay self-explanatory without the new block comments.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Never write comments on what the code does; make the code clean and self-explanatory instead.

Also applies to: 283-303

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

In `@src/commands/base-command.ts` around lines 68 - 69, Extract the Commander
option-error handling into a shared helper instead of duplicating
OPTION_ERROR_CODES and the log()/outputHelp()/log() branch; create a single
exported function (e.g., handleCommanderOptionError or
isCommanderOptionErrorPolicy) that encapsulates the OPTION_ERROR_CODES array and
the logic that logs the error, calls outputHelp(), and logs again, then replace
the duplicated code in both base-command.ts (where OPTION_ERROR_CODES is
declared) and main.ts with calls to this new helper so both root and subcommand
flows use the same implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/main.ts`:
- Around line 188-193: The help output in non-interactive error branches is
being written to stdout; change the calls to outputHelp() to route to stderr by
calling outputHelp({ error: true }) in the non-interactive/error paths —
specifically replace command.outputHelp() in the non-interactive branch (where
isInteractive() is false) and the this.outputHelp() calls referenced in the
other error branches (methods containing this.outputHelp()) so they pass the {
error: true } option to ensure help text goes to stderr.

In `@tests/integration/commands/error-handling/error-handling.test.ts`:
- Around line 7-19: The test currently calls
callCli(['status','--invalid-option']) twice and only inspects the stderr from
the second run; change the test (and the similar cases in lines 22-48) to call
callCli once and capture the thrown ExecaError (e.g., assign the rejected value
to a variable via await expect(...).rejects.toThrow() replacement pattern or
try/catch) and then assert both the brief error banner and that the same error
object's stderr contains the full USAGE/OPTIONS help block (use normalize to
compare), referencing the existing callCli and normalize helpers and the
'unknown option shows error message' test so all assertions are made against the
single captured error instance.

---

Nitpick comments:
In `@src/commands/base-command.ts`:
- Around line 68-69: Extract the Commander option-error handling into a shared
helper instead of duplicating OPTION_ERROR_CODES and the
log()/outputHelp()/log() branch; create a single exported function (e.g.,
handleCommanderOptionError or isCommanderOptionErrorPolicy) that encapsulates
the OPTION_ERROR_CODES array and the logic that logs the error, calls
outputHelp(), and logs again, then replace the duplicated code in both
base-command.ts (where OPTION_ERROR_CODES is declared) and main.ts with calls to
this new helper so both root and subcommand flows use the same implementation.
🪄 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: CHILL

Plan: Pro

Run ID: e86b4a59-ef43-43d6-8cab-53e52a70169b

📥 Commits

Reviewing files that changed from the base of the PR and between 3fc2aec and 3f12a30.

📒 Files selected for processing (4)
  • src/commands/base-command.ts
  • src/commands/main.ts
  • src/utils/run-program.ts
  • tests/integration/commands/error-handling/error-handling.test.ts

Comment on lines +7 to +19
test('unknown option shows error message', async (t) => {
await expect(callCli(['status', '--invalid-option'])).rejects.toThrow()

try {
await callCli(['status', '--invalid-option'])
} catch (error) {
const stderr = (error as { stderr: string }).stderr
const normalized = normalize(stderr)

// In interactive mode (test environment), shows brief error
t.expect(normalized).toContain('Error: unknown option')
t.expect(normalized).toContain('See more help with --help')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert the new help-on-error contract from the same failed run.

These cases only check the generic error banner and then rerun the CLI, so a regression where the extra help text disappears or lands on the wrong stream would still pass. Capture the rejected ExecaError once and assert the USAGE/OPTIONS block from that same failure.

Also applies to: 22-48

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

In `@tests/integration/commands/error-handling/error-handling.test.ts` around
lines 7 - 19, The test currently calls callCli(['status','--invalid-option'])
twice and only inspects the stderr from the second run; change the test (and the
similar cases in lines 22-48) to call callCli once and capture the thrown
ExecaError (e.g., assign the rejected value to a variable via await
expect(...).rejects.toThrow() replacement pattern or try/catch) and then assert
both the brief error banner and that the same error object's stderr contains the
full USAGE/OPTIONS help block (use normalize to compare), referencing the
existing callCli and normalize helpers and the 'unknown option shows error
message' test so all assertions are made against the single captured error
instance.

Copy link
Copy Markdown
Contributor

@ndhoule ndhoule left a comment

Choose a reason for hiding this comment

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

I agree with both of the Coderabbit comments, but otherwise looks good.

eduardoboucas
eduardoboucas previously approved these changes Mar 27, 2026
const HELP_SEPARATOR_WIDTH = 5

/** Commander error codes for option-related errors */
const OPTION_ERROR_CODES = ['commander.unknownOption', 'commander.missingArgument', 'commander.excessArguments']
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Making this a Set would be neat.

@ndhoule ndhoule merged commit 0715310 into main Mar 27, 2026
71 checks passed
@ndhoule ndhoule deleted the sr/error-improvements branch March 27, 2026 19:40
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.

3 participants