feat(cli): warn on unrecognized CLI options#1514
Conversation
Yargs does not enable strict option checking by default, so unknown
flags are silently swallowed. This adds a warning when unrecognized
options are detected, helping users catch typos early.
Handles edge cases:
- Skips undefined-value keys (yargs default placeholders from other commands)
- Skips camelCase variants of known kebab-case options
- Skips boolean negation keys (noFoo for --no-foo)
- Skips negativeAlias keys (e.g. R for --no-rollback)
- Skips keys injected by yargs .env('CDK') from CDK_* environment variables
Closes #928 (partial — related to rix0rrr's strict parsing suggestion)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1514 +/- ##
==========================================
+ Coverage 88.10% 88.22% +0.12%
==========================================
Files 75 76 +1
Lines 10723 10816 +93
Branches 1465 1488 +23
==========================================
+ Hits 9447 9542 +95
+ Misses 1248 1246 -2
Partials 28 28
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| expect(findUnknownOptions(argv)).toEqual([]); | ||
| }); | ||
|
|
||
| test('does not report keys injected by yargs .env("CDK") from environment variables', () => { |
There was a problem hiding this comment.
I dug into it and the test was actually wrong. I ran the actual CDK parser and yargs doesn't create noFoo keys from --no-foo flags, it just sets foo: false. The only way a noFoo key ends up in argv is if someone sets CDK_NO_FOO as an env var (via .env('CDK')), which the env prefix check already covers. Removed.
| */ | ||
| export function findUnknownOptions(argv: any): string[] { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const config = require('../cli-type-registry.json'); |
There was a problem hiding this comment.
nit: this doesn't change, so can be outside the function as a "global" variable. In practice this doesn't matter much because findUnknownOptions is only called once and require is cached, but you should start to thinking about these kind of things.
The same applies to much of the "setup" code later on: sorting global and command options etc.
A good question to ask is: Which parts of the code are actually dependent on the argv input? Which parts only depend on the static cli-type-registry.json? This will give you an idea how to split it.
There was a problem hiding this comment.
That's a good point, thank you! Moved the registry load and global options set to module scope since they never change. Only the command-specific options are computed inside the function now since those depend on which command was invoked.
| if (argv[key] === undefined) continue; | ||
| if (yargsInternals.has(key)) continue; | ||
| if (globalOptions.has(key)) continue; | ||
| if (commandOptions.has(key)) continue; |
There was a problem hiding this comment.
I actually removed this entirely since it's the same issue as the first comment. The negation check was solving a problem that doesn't exist with how yargs works.
| if (globalOptions.has(positiveKey) || commandOptions.has(positiveKey) || | ||
| globalOptions.has(positiveKebab) || commandOptions.has(positiveKebab)) continue; |
There was a problem hiding this comment.
we should enforce this as a linter rule, but imho these lines are hard to parse and read. Prefer always using {}.
There was a problem hiding this comment.
got it, changed it
| function camelToKebab(str: string): string { | ||
| return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); | ||
| } |
There was a problem hiding this comment.
This probably happens to work in this case, but is likely not working in the general case. For your code, it would be much easier to go the other way: From kebab to camel.
There was a problem hiding this comment.
ah okay, switched it
- Move static config and global options set to module scope - Replace camelToKebab with kebabToCamel (applied at set-construction time) - Remove unnecessary isNegationOfKnownOption — verified that yargs does NOT create "noFoo" keys from --no-foo flags; the only noFoo keys come from CDK_NO_FOO env vars which isFromEnvPrefix already handles - Always use braces for if/continue statements - Combine the noFoo negation check into isFromEnvPrefix coverage - Add explanatory comment on env var test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
.env('CDK')), undefined defaults, camelCase variants, boolean negations, negativeAliasesThis implements a non-breaking warning approach. Unrecognized options are still processed by yargs as before, but users now get feedback that their flag was not recognized.
Changes
check-unknown-options.tscli.tsfindUnknownOptionsand emits a warningcheck-unknown-options.test.tsTest plan
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
🤖 Generated with Claude Code