diff --git a/.changeset/did-you-mean-root-fallback.md b/.changeset/did-you-mean-root-fallback.md new file mode 100644 index 0000000..7b3089c --- /dev/null +++ b/.changeset/did-you-mean-root-fallback.md @@ -0,0 +1,5 @@ +--- +'incur': patch +--- + +Fixed root fetch/command fallback bypassing "Did you mean?" suggestions when the input is a typo of a known command. diff --git a/src/Cli.test.ts b/src/Cli.test.ts index c4174f9..083df23 100644 --- a/src/Cli.test.ts +++ b/src/Cli.test.ts @@ -3732,6 +3732,15 @@ describe('fetch', async () => { `) }) + test('root-level fetch with typo of known command → did you mean', async () => { + const cli = Cli.create('api', { description: 'API', fetch: app.fetch }).command('upgrade', { + run: () => ({ upgraded: true }), + }) + const { output, exitCode } = await serve(cli, ['upgra']) + expect(exitCode).toBe(1) + expect(output).toContain("Did you mean 'upgrade'?") + }) + test('root-level fetch with no args → root path', async () => { const cli = Cli.create('api', { description: 'API', fetch: app.fetch }) // Hono returns 404 for / since we don't have a root route diff --git a/src/Cli.ts b/src/Cli.ts index 252b7b4..414626f 100644 --- a/src/Cli.ts +++ b/src/Cli.ts @@ -1079,9 +1079,18 @@ async function serveImpl( const resolvedFormat = 'command' in resolved && (resolved as any).command.format const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon' - // Fall back to root fetch when no subcommand matches + // Fall back to root fetch/command when no subcommand matches, + // but only if the token doesn't look like a typo of a known command. + const rootFallbackBlocked = + 'error' in resolved && + !resolved.path && + (() => { + const candidates = [...resolved.commands.keys()] + for (const b of builtinCommands) candidates.push(b.name) + return suggest(resolved.error, candidates) !== undefined + })() const effective = - 'error' in resolved && options.rootFetch && !resolved.path + 'error' in resolved && options.rootFetch && !resolved.path && !rootFallbackBlocked ? { fetchGateway: { _fetch: true as const, @@ -1092,7 +1101,7 @@ async function serveImpl( path: name, rest: filtered, } - : 'error' in resolved && options.rootCommand && !resolved.path + : 'error' in resolved && options.rootCommand && !resolved.path && !rootFallbackBlocked ? { command: options.rootCommand, path: name, rest: filtered } : resolved