Fix: print usage for a bare adk invocation instead of only the version - #597
Open
AmaadMartin wants to merge 2 commits into
Open
Fix: print usage for a bare adk invocation instead of only the version#597AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
added 2 commits
August 3, 2026 16:39
The root commander command had an unconditional action handler that printed the version, so bare `adk` answered with a version string and never listed its subcommands. Branch the handler on the --version flag and fall through to the root help, matching adk-python, where the click group defaults to no_args_is_help. Also rename the program from 'ADK CLI' to 'adk' so the usage line the user now sees names the actual binary (and every subcommand's usage line reads 'adk web ...' rather than 'ADK CLI web ...').
…rError
Replace the hand-rolled {code?, exitCode?} structural type with the class
commander exports for exactly this. Both fields are required on the real
type, so the unknown-subcommand assertions no longer need optional
chaining; narrow the possibly-undefined result with expect.fail so a
missing throw reports as a real assertion failure.
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Closes: #issue_number
Related: #issue_number
Problem: A bare
adkprints only the version string and exits, so a user who has just installed the CLI learns nothing aboutweb,api_server,create,run,deployorintegration, and has to guess that--helpexists.The cause is in
dev/src/cli/cli.ts: the root commander command carried an unconditional action handler.In commander, when the root command has an action handler and no subcommand matches, that handler runs — so it fired for both
adk -vand bareadk, and printed the version in both cases. Commander's own "no subcommand given, show help" path (Command._parseCommandonly callsthis.help({error: true})when!this._actionHandler) was therefore unreachable, and the subcommand list was never shown. This also diverges fromadk-python, whose root is aclick.Group;click.Groupdefaultsno_args_is_help=True, so bareadkprints usage there.Solution: Branch the root action on the flag rather than printing unconditionally, and print the root help when the flag is absent (one source hunk):
Why these specific choices:
program.outputHelp(), notconsole.log(program.helpInformation()).outputHelp()writes through commander's configured output writer, so no newconsole.logis introduced (the repo guideline forbids adding one) and the output stays redirectable/testable viaconfigureOutput.program.help(). That additionally requests an exit, and underexitOverride()it throws aCommanderErrorwith codecommander.helpDisplayed, which the existing test harness does not swallow.outputHelp()returns normally, so node exits 0 on its own — noprocess.exit()is called.dev/src/cli_entrypoint.tscalls.parse(), not.parseAsync().adk(from'ADK CLI') is required for the output to be usable:Usage: ADK CLI [options] [command]is not a runnable command, and this text is now what a user sees for a bare invocation. It also fixes every subcommand's usage line (Usage: adk web ..., previouslyADK CLI web ...).adk-pythonprintsUsage: adk [OPTIONS] COMMAND [ARGS].... The identically spelledAdkLogger({label: 'ADK CLI'})two lines above is a separate value and is left alone.Resulting contract, all verified against the built binary (see E2E below):
adk(no args)adk --helpadk -v/adk --versionadk --help/-hadk <unknown>Exit code 0 for the bare invocation is deliberate, so shell wrappers running under
set -ethat probe for the binary are unaffected. A reviewer comparing SDKs should note this matchesadk-pythononclick < 8.2; click 8.2.0 changed it ("If help is shown becauseno_args_is_helpis enabled ... the exit code is 2 instead of 0") andadk-pythonpinsclick>=8.1.8,<9, so bareadkthere exits 0 or 2 depending on the resolved click version. That inconsistency is being handled separately on the Python side and is intentionally not imported here — the JS contract is exit 0.Breaking change analysis: exactly one invocation changes behaviour — bare
adknow prints usage instead of the version. A script doingVERSION=$(adk)would break; assessed as negligible and acceptable, since the documented way to get the version isadk --version(unchanged) and the repo contains no such usage. A repo-wide search for binary invocations found onlynpx adk run/npx adk webinREADME.md,npx adk ${adkCommand} ...in the generated Dockerfile (dev/src/cli/deploy/deploy_utils.ts), and an explicit-args spawn ofdev/dist/esm/cli_entrypoint.jsintests/integration/test_api_server.ts. Nothing invokesadkbare or parses its output. Exit codes are unchanged for every invocation. No API, export or type is added, removed or changed;export function createProgram(): Commandis unchanged. No dependency added, removed or bumped —package.jsonandpackage-lock.jsonare untouched.Deliberately out of scope: the unknown-subcommand message is still
error: too many arguments. Expected 0 arguments but got 1.rather than naming the command. Improving it means loosening excess-argument handling on the root and re-implementing the check by hand, which is a separate change; the test added here therefore asserts on the exit code and error code, not on that message string.Collision check (required before implementing):
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000returned 496 open PRs. Filtering titles/branches forcli|usage|help|version|bare|invocation|command|subcommand|no-argsand then diffing every candidate that touchesdev/src/cli/cli.ts(#595, #592, #587, #585, #451, #385, #358, #433, #289) showed none of them touch the root program construction (lines 199–215) — their hunks are at lines 13–29, 95–160, 336–520. Greps of those diffs fornew Command('ADK CLI'),outputHelp,options.versionandconsole.log(version)found onlyhelpInformation()in unrelated tests (#592, #358) and anoutputHelpmention in a comment aboutaddHelpTexton thedeploy cloud_runsubcommand (#587). No PR lands or overlaps this change, so this branches frommain.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
Added a new
describe('bare invocation', ...)block in the existingdev/test/cli/cli_test.ts. No existing test, mock or hook was modified or deleted — in particular the existingdescribe('command: version')block is byte-for-byte unchanged, since it is the regression signal that--versionstill works. The new block installs capturingwriteOut/writeErrwriters viaprogram.configureOutput()in a nestedbeforeEach(nested hooks run after the outer one, soprogramis already built andexitOverride()already applied), becauseoutputHelp()writes through commander's output writer rather thanconsole.log.should print usage listing every subcommand and request no exit— asserts stdout containsUsage: adk,Commands:and each ofweb,api_server,create,run,deploy,integration; that stderr stayed empty; thatconsole.logwas not called with the version; and that nothing was thrown. "Nothing thrown" is a sound proxy for exit 0: withexitOverride()installed commander requests every non-zero exit by throwing, so no throw means no exit requested, and the real process exit code is additionally confirmed 0 by the E2E run below.should print the version without usage for -v and --version— for both flags, assertsconsole.logwas called with the version exactly twice and that the commander writers stayed empty, i.e. no usage block leaked into the--versionpath.should exit non-zero for an unknown subcommand— assertserr.code === 'commander.excessArguments',err.exitCode === 1, and that no help was dumped to stdout on the error path.All 24 pre-existing cases still pass unmodified.
Coverage of the new code. Measured with
--coverage.include=dev/src/cli/cli.tsand read out ofcoverage-final.jsonfor the changed region: every new statement is hit, and both sides of the one new branch are executed — theif (options.version)true-arm 4 times (cases 2 and the untouched existing version test) and theprogram.outputHelp()arm once (case 1). That is 100% line and branch coverage of the new code. Whole-file coverage is 95.36% statements / 74.41% branches, unchanged in substance from before — the uncovered remainder is pre-existing subcommand wiring this change does not touch.Proof the tests can fail (each mutation applied to
dev/src/cli/cli.tsalone, with the new tests left in place, then reverted):.action(() => { console.log(version); })AssertionError: expected '' to contain 'Usage: adk'(26 passed, 1 failed)new Command('adk')→new Command('ADK CLI')AssertionError: expected 'Usage: ADK CLI [options] [command]\n\…' to contain 'Usage: adk'if (options.version) { console.log(version); return; }), leaving onlyoutputHelp()command: versiontest FAILS —expected "log" to be called with arguments: [ '1.0.0-test' ](25 passed, 2 failed)Case 3 (unknown subcommand) is a guard-rail test: it passes before and after this change by design, so it is not claimed as new signal for the fix. It is not vacuous, though — it pins that the root command's excess-argument handling was not widened, and it fails if that invariant is broken: adding
program.allowExcessArguments()to the root makes it fail withexpected commander to reject an unknown subcommand(26 passed, 1 failed).The helper driving cases 1–3 is typed with commander's exported
CommanderErrorrather than a hand-rolled{code?: string; exitCode?: number}shape, socodeandexitCodeare the real required fields and the assertions need no optional chaining; the possibly-undefined result is narrowed withexpect.fail(...), which also turns "commander did not throw at all" into a proper assertion failure rather than anundefinedmismatch.No integration test was added, deliberately: the behaviour is fully determined by
createProgram(), which these unit tests drive against the real commander instance (only leaf action targets such asAdkApiServerare mocked). A build-dependent spawn test for a help string would add no signal.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
From the repo root,
npm run build, then invoke the exact filedev/package.jsonmapsbin.adkto:Postcondition check — bare output is byte-identical to
--help:Also run on the pushed commit:
npm run build(succeeds),npm run lint(clean),npx prettier --checkon both changed files (clean).npm run ts:checkreports 281 errors, but the identical 281 are present on the base commit with the change stashed (error sets diffed and identical) — this branch adds none.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.