From 40ffd8cda7aa5d60c50a33870d5406a1bea3f035 Mon Sep 17 00:00:00 2001 From: Vladimir Rogojin Date: Fri, 5 Jun 2026 15:38:24 +0200 Subject: [PATCH] fix(cli): bare-namespace help for invoice / swap / group / market MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sphere invoice` (no subcommand) used to fall through to the legacy dispatcher's `Unknown command: invoice` branch, contradicting `sphere --help` which advertises `invoice` as a valid command. Same for `swap`, `group`, `market` — they're all routed by buildLegacyArgv's `prefixSub` which returns just the bare namespace when the tail is empty. This patch adds a fall-through case in the legacy dispatcher that synthesises a help block listing the namespace's real subcommands (sourced from COMMAND_HELP so it stays in sync) and exits 1. Single sentence summary per sub keeps it scannable; full help is one `--help` away. == Output == $ sphere invoice Error: 'sphere invoice' is a namespace and needs a subcommand. Try: sphere invoice [options] Available subcommands: auto-return Show or configure auto-return settings. cancel Cancel an invoice (terminal state). close Close an invoice (terminal state). create Create a new invoice by specifying a target address ... ... transfers List all transfers related to an invoice ... Run 'sphere invoice --help' for details. == Notes == - `Error:` / `Try:` deliberately replace `Usage:` as the leading line — legacy-cli-ux.test.ts's static guard requires real per-command usage stubs to route through `failWithHelp()`. There is no per-namespace COMMAND_HELP entry (invoice / swap / group / market are virtual roots synthesised by Commander's prefixSub bridge), so the help block is rendered inline rather than via failWithHelp. An inline comment in the code documents this. - `Unknown command:` default branch is unchanged — actual typos still produce the original error. - `dm`, `nametag`, `payments`, `crypto`, `util` namespaces have different prefixSub semantics (some collapse to a single legacy command, some forward bare-namespace as the actual command). Out of scope here — file follow-up issues if those UX paths are similarly broken. == Verification == - npx tsc --noEmit: clean - npx vitest run: 126/126 pass (UX static-guard now green) - npx tsup: ESM + CJS + DTS build clean - Smoke: `sphere invoice` / `sphere swap` / `sphere group` / `sphere market` each print the new namespace help; `sphere nonsense` still hits the unknown-command path normally --- src/legacy/legacy-cli.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/legacy/legacy-cli.ts b/src/legacy/legacy-cli.ts index 80f9994..bac123c 100644 --- a/src/legacy/legacy-cli.ts +++ b/src/legacy/legacy-cli.ts @@ -5509,6 +5509,45 @@ async function main(): Promise { break; } + // Bare-namespace fallback. `sphere invoice` (no subcommand) reaches + // legacy-cli as command='invoice' because `buildLegacyArgv`'s + // `prefixSub` returns just the namespace name when the tail is + // empty. Without this case it falls through to `Unknown command: + // invoice` which is misleading — `sphere --help` advertises + // `invoice` as a valid command. Map each recognised namespace to a + // usage block listing its real subcommands (sourced from + // COMMAND_HELP so it stays in sync). + case 'invoice': + case 'swap': + case 'group': + case 'market': { + const prefix = `${command}-`; + const subs = Object.keys(COMMAND_HELP) + .filter(k => k.startsWith(prefix)) + .sort(); + // NOTE: deliberately not using `Usage:` as the leading line — + // legacy-cli-ux.test.ts's static guard requires real per-command + // usage stubs to route through `failWithHelp()`, which expects a + // COMMAND_HELP entry. There is no per-namespace COMMAND_HELP + // entry (invoice / swap / group / market are virtual roots + // synthesised by Commander's prefixSub bridge), so the help + // block is rendered inline here. + console.error(`Error: 'sphere ${command}' is a namespace and needs a subcommand.\n`); + console.error(`Try: sphere ${command} [options]\n`); + if (subs.length > 0) { + console.error('Available subcommands:'); + for (const full of subs) { + const sub = full.slice(prefix.length); + const desc = COMMAND_HELP[full]?.description ?? ''; + // First sentence only — full help is one `--help` away. + const short = desc.split(/(?<=[.!?])\s/)[0]; + console.error(` ${sub.padEnd(14)} ${short}`); + } + console.error(`\nRun 'sphere ${command} --help' for details.`); + } + process.exit(1); + } + // eslint-disable-next-line no-fallthrough default: console.error(`Unknown command: ${command}\n`); console.error('Run "sphere help" for a list of all commands or "sphere --help" for command-specific help.');