From 3487c1117d6a40665eda23bc910f8c6ba9c1e10b Mon Sep 17 00:00:00 2001 From: Edu Wass Date: Thu, 26 Mar 2026 20:01:30 +0100 Subject: [PATCH] fix: --llms scoped to group doubles namespace in command names When running `cli group --llms`, the `prefix` array was passed both to build `scopedName` (e.g. "cli group") AND to the collect helper functions which also prepend it to each command name. This caused output like: `cli group group login` instead of `cli group login` The fix passes an empty prefix to collect helpers when scoped, since `scopedName` already carries the group path for display. Affects --llms, --llms-full in md/json/yaml formats. --- src/Cli.test.ts | 9 ++++++--- src/Cli.ts | 14 ++++++++++---- src/e2e.test.ts | 32 ++++++++++++++++---------------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/Cli.test.ts b/src/Cli.test.ts index 729b717..dc6a603 100644 --- a/src/Cli.test.ts +++ b/src/Cli.test.ts @@ -1274,8 +1274,9 @@ describe('--llms', () => { cli.command('ping', { description: 'Health check', run: () => ({}) }) const { output } = await serve(cli, ['auth', '--llms']) - expect(output).toContain('test auth auth login') - expect(output).toContain('test auth auth logout') + expect(output).toContain('test auth login') + expect(output).toContain('test auth logout') + expect(output).not.toContain('test auth auth') // no doubled namespace expect(output).not.toContain('ping') }) }) @@ -3326,7 +3327,9 @@ test('--llms scoped to group', async () => { const { output } = await serve(cli, ['--llms-full', '--format', 'json', 'pr']) const manifest = JSON.parse(output) expect(manifest.commands).toHaveLength(2) - expect(manifest.commands.every((c: any) => c.name.startsWith('pr '))).toBe(true) + // When scoped to a group, command names should NOT include the group prefix + // (the scope context already establishes it) + expect(manifest.commands.map((c: any) => c.name).sort()).toEqual(['create', 'list']) }) test('--help on root with rootCommand shows command help with subcommands', async () => { diff --git a/src/Cli.ts b/src/Cli.ts index f7a2f82..18e859f 100644 --- a/src/Cli.ts +++ b/src/Cli.ts @@ -587,26 +587,32 @@ async function serveImpl( } } + // When scoped into a subtree, prefix has already been consumed to narrow + // scopedCommands and is captured in scopedName. Passing it again to the + // collect helpers would double the group segment in command names + // (e.g. "cli auth auth login" instead of "cli auth login"). + const collectPrefix = prefix.length > 0 ? [] as string[] : prefix + if (llmsFull) { if (!formatExplicit || formatFlag === 'md') { const groups = new Map() - const cmds = collectSkillCommands(scopedCommands, prefix, groups) + const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups) const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name writeln(Skill.generate(scopedName, cmds, groups)) return } - writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag)) + writeln(Formatter.format(buildManifest(scopedCommands, collectPrefix), formatFlag)) return } if (!formatExplicit || formatFlag === 'md') { const groups = new Map() - const cmds = collectSkillCommands(scopedCommands, prefix, groups) + const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups) const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name writeln(Skill.index(scopedName, cmds, scopedDescription)) return } - writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix), formatFlag)) + writeln(Formatter.format(buildIndexManifest(scopedCommands, collectPrefix), formatFlag)) return } diff --git a/src/e2e.test.ts b/src/e2e.test.ts index b8a6a84..948c708 100644 --- a/src/e2e.test.ts +++ b/src/e2e.test.ts @@ -1248,9 +1248,9 @@ describe('--llms-full', () => { const names = json(output).commands.map((c: any) => c.name) expect(names).toMatchInlineSnapshot(` [ - "auth login", - "auth logout", - "auth status", + "login", + "logout", + "status", ] `) }) @@ -1266,9 +1266,9 @@ describe('--llms-full', () => { const names = json(output).commands.map((c: any) => c.name) expect(names).toMatchInlineSnapshot(` [ - "project deploy create", - "project deploy rollback", - "project deploy status", + "create", + "rollback", + "status", ] `) }) @@ -1300,15 +1300,15 @@ describe('--llms-full', () => { '--format', 'json', ]) - const deployCreate = json(output).commands.find((c: any) => c.name === 'project deploy create') + const deployCreate = json(output).commands.find((c: any) => c.name === 'create') expect(deployCreate.examples).toMatchInlineSnapshot(` [ { - "command": "project deploy create staging", + "command": "create staging", "description": "Deploy staging from main", }, { - "command": "project deploy create production --branch release --dryRun true", + "command": "create production --branch release --dryRun true", "description": "Dry run a production deploy", }, ] @@ -1495,9 +1495,9 @@ describe('--llms', () => { | Command | Description | |---------|-------------| - | \`app auth auth login\` | Log in to the service | - | \`app auth auth logout\` | Log out of the service | - | \`app auth auth status\` | Show authentication status | + | \`app auth login\` | Log in to the service | + | \`app auth logout\` | Log out of the service | + | \`app auth status\` | Show authentication status | Run \`app auth --llms-full\` for full manifest. Run \`app auth --schema\` for argument details. " @@ -1513,9 +1513,9 @@ describe('--llms', () => { | Command | Description | |---------|-------------| - | \`app project deploy project deploy create \` | Create a deployment | - | \`app project deploy project deploy rollback \` | Rollback a deployment | - | \`app project deploy project deploy status \` | Check deployment status | + | \`app project deploy create \` | Create a deployment | + | \`app project deploy rollback \` | Rollback a deployment | + | \`app project deploy status \` | Check deployment status | Run \`app project deploy --llms-full\` for full manifest. Run \`app project deploy --schema\` for argument details. " @@ -1941,7 +1941,7 @@ describe('env', () => { test('--llms-full json includes schema.env', async () => { const { output } = await serve(createApp(), ['auth', '--llms-full', '--format', 'json']) - const login = json(output).commands.find((c: any) => c.name === 'auth login') + const login = json(output).commands.find((c: any) => c.name === 'login') expect(login.schema.env.properties).toMatchInlineSnapshot(` { "AUTH_HOST": {