From 906651b813445d58106e7a8a2c4a8dd75f44548a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 9 Oct 2026 00:54:50 -0700 Subject: [PATCH 1/2] fix(sim-cli): an unknown option lists the command's own flags Commander suggests only a near spelling, so a guessed flag such as `tables rows batch-delete --row-ids` (the flag is --row) got a bare "unknown option" and the caller had to look the command up before retrying. The parse-error formatter now appends the command's flags; an id that opens with a dash keeps its `--` example instead. --- packages/sim-cli/src/embed-isolation.test.ts | 20 ++++++++++++++++++ packages/sim-cli/src/runtime/build.ts | 22 +++++++++++++------- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/sim-cli/src/embed-isolation.test.ts b/packages/sim-cli/src/embed-isolation.test.ts index 5854a049ac3..69eb120ba16 100644 --- a/packages/sim-cli/src/embed-isolation.test.ts +++ b/packages/sim-cli/src/embed-isolation.test.ts @@ -87,4 +87,24 @@ describe('embedded CLI output ownership', () => { expect(invalid.stdout).toBe('') expect(invalid.stderr).toContain('missing required argument') }) + + it('answers a guessed flag with the command flags and an id that opens with a dash with the escape', async () => { + const transport = async () => { + throw new Error('a parse error must not reach the API') + } + const [guessed, dashedId] = await Promise.all([ + runEmbeddedCli(['tables', 'rows', 'batch-delete', 'tbl_1', '--row-ids', 'row_1', '--yes'], { + ...identity, + transport, + }), + runEmbeddedCli(['audit-logs', 'get', '-X9abc'], { ...identity, transport }), + ]) + expect(guessed.exitCode).toBe(1) + expect(guessed.stderr).toContain("unknown option '--row-ids'") + expect(guessed.stderr).toContain('Options for sim tables rows batch-delete:') + expect(guessed.stderr).toContain('--row ') + expect(dashedId.exitCode).toBe(1) + expect(dashedId.stderr).toContain('Example: sim audit-logs get -- -X9abc') + expect(dashedId.stderr).not.toContain('Options for') + }) }) diff --git a/packages/sim-cli/src/runtime/build.ts b/packages/sim-cli/src/runtime/build.ts index dd5b1a9681a..135b7ec0808 100644 --- a/packages/sim-cli/src/runtime/build.ts +++ b/packages/sim-cli/src/runtime/build.ts @@ -92,13 +92,16 @@ function looksLikeAnId(token: string): boolean { } /** - * Appends a worked example to the parse errors a positional argument causes. + * Appends a worked example to the parse errors a positional argument causes, + * and the command's own flags to an unknown option. * * Covers the argument being absent and the argument being swallowed as an * option because its id opens with a dash; the second needs the `--` escape, - * which commander never mentions. + * which commander never mentions. A guessed flag gets the real list: commander + * suggests only a near spelling, so `--row-ids` for `--row` got a bare error + * and a caller had to look the command up before retrying. */ -function addArgumentExamples(command: Command): Command { +function addParseErrorGuidance(command: Command): Command { const outputError = command.configureOutput().outputError if (!outputError) throw new Error('Commander output formatter is not configured') @@ -113,10 +116,15 @@ function addArgumentExamples(command: Command): Command { return } - if (command.registeredArguments.length === 0) return const token = UNKNOWN_OPTION_TOKEN.exec(message)?.[1] - if (!token || !looksLikeAnId(token)) return - write(`Example: ${commandPath(command)} -- ${token}\n`) + if (!token) return + if (looksLikeAnId(token)) { + if (command.registeredArguments.length > 0) + write(`Example: ${commandPath(command)} -- ${token}\n`) + return + } + const flags = command.options.filter((option) => !option.hidden).map((option) => option.flags) + write(`Options for ${commandPath(command)}: ${flags.join(', ')}\n`) }, }) return command @@ -339,7 +347,7 @@ function configureOperation( } function buildLeaf(operation: V2OperationName, spec: CommandSpec, leafName: string): Command { - return addArgumentExamples(configureOperation(new Command(leafName), operation, spec)) + return addParseErrorGuidance(configureOperation(new Command(leafName), operation, spec)) } /** From 0b2eef2e72c064cb9211ce5cd77e49cffe0f00bc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 9 Oct 2026 01:07:23 -0700 Subject: [PATCH 2/2] fix(sim-cli): an id-shaped token lists the flags on a command with no positional --- packages/sim-cli/src/embed-isolation.test.ts | 9 +++++++-- packages/sim-cli/src/runtime/build.ts | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/sim-cli/src/embed-isolation.test.ts b/packages/sim-cli/src/embed-isolation.test.ts index 69eb120ba16..89626b908ad 100644 --- a/packages/sim-cli/src/embed-isolation.test.ts +++ b/packages/sim-cli/src/embed-isolation.test.ts @@ -88,16 +88,17 @@ describe('embedded CLI output ownership', () => { expect(invalid.stderr).toContain('missing required argument') }) - it('answers a guessed flag with the command flags and an id that opens with a dash with the escape', async () => { + it('answers a guessed flag with the command flags, and an id that opens with a dash with the escape', async () => { const transport = async () => { throw new Error('a parse error must not reach the API') } - const [guessed, dashedId] = await Promise.all([ + const [guessed, dashedId, noArguments] = await Promise.all([ runEmbeddedCli(['tables', 'rows', 'batch-delete', 'tbl_1', '--row-ids', 'row_1', '--yes'], { ...identity, transport, }), runEmbeddedCli(['audit-logs', 'get', '-X9abc'], { ...identity, transport }), + runEmbeddedCli(['logs', 'list', '-X9abc'], { ...identity, transport }), ]) expect(guessed.exitCode).toBe(1) expect(guessed.stderr).toContain("unknown option '--row-ids'") @@ -106,5 +107,9 @@ describe('embedded CLI output ownership', () => { expect(dashedId.exitCode).toBe(1) expect(dashedId.stderr).toContain('Example: sim audit-logs get -- -X9abc') expect(dashedId.stderr).not.toContain('Options for') + // With no positional to escape into, an id-shaped token is just an unknown option. + expect(noArguments.exitCode).toBe(1) + expect(noArguments.stderr).toContain('Options for sim logs list:') + expect(noArguments.stderr).not.toContain('Example:') }) }) diff --git a/packages/sim-cli/src/runtime/build.ts b/packages/sim-cli/src/runtime/build.ts index 135b7ec0808..950c7d3b4b8 100644 --- a/packages/sim-cli/src/runtime/build.ts +++ b/packages/sim-cli/src/runtime/build.ts @@ -118,9 +118,8 @@ function addParseErrorGuidance(command: Command): Command { const token = UNKNOWN_OPTION_TOKEN.exec(message)?.[1] if (!token) return - if (looksLikeAnId(token)) { - if (command.registeredArguments.length > 0) - write(`Example: ${commandPath(command)} -- ${token}\n`) + if (looksLikeAnId(token) && command.registeredArguments.length > 0) { + write(`Example: ${commandPath(command)} -- ${token}\n`) return } const flags = command.options.filter((option) => !option.hidden).map((option) => option.flags)