From ae51bbdae811e8930a703a7b4411f6baeb2e1cdf Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Mon, 29 Sep 2025 16:39:47 -0700 Subject: [PATCH] Add extension name auto-complete to `/extensions update` (#10198) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../src/ui/commands/extensionsCommand.test.ts | 76 +++++++++++++++++++ .../cli/src/ui/commands/extensionsCommand.ts | 13 ++++ 2 files changed, 89 insertions(+) diff --git a/packages/cli/src/ui/commands/extensionsCommand.test.ts b/packages/cli/src/ui/commands/extensionsCommand.test.ts index 1b71f8c4a5..b51e5a826f 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.test.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.test.ts @@ -251,5 +251,81 @@ describe('extensionsCommand', () => { expect.any(Number), ); }); + + describe('completion', () => { + const updateCompletion = extensionsCommand.subCommands?.find( + (cmd) => cmd.name === 'update', + )?.completion; + + if (!updateCompletion) { + throw new Error('Update completion not found'); + } + + const extensionOne: GeminiCLIExtension = { + name: 'ext-one', + version: '1.0.0', + isActive: true, + path: '/test/dir/ext-one', + installMetadata: { + type: 'git', + autoUpdate: false, + source: 'https://github.com/some/extension.git', + }, + }; + const extensionTwo: GeminiCLIExtension = { + name: 'another-ext', + version: '1.0.0', + isActive: true, + path: '/test/dir/another-ext', + installMetadata: { + type: 'git', + autoUpdate: false, + source: 'https://github.com/some/extension.git', + }, + }; + const allExt: GeminiCLIExtension = { + name: 'all-ext', + version: '1.0.0', + isActive: true, + path: '/test/dir/all-ext', + installMetadata: { + type: 'git', + autoUpdate: false, + source: 'https://github.com/some/extension.git', + }, + }; + + it.each([ + { + description: 'should return matching extension names', + extensions: [extensionOne, extensionTwo], + partialArg: 'ext', + expected: ['ext-one'], + }, + { + description: 'should return --all when partialArg matches', + extensions: [], + partialArg: '--al', + expected: ['--all'], + }, + { + description: + 'should return both extension names and --all when both match', + extensions: [allExt], + partialArg: 'all', + expected: ['--all', 'all-ext'], + }, + { + description: 'should return an empty array if no matches', + extensions: [extensionOne], + partialArg: 'nomatch', + expected: [], + }, + ])('$description', async ({ extensions, partialArg, expected }) => { + mockGetExtensions.mockReturnValue(extensions); + const suggestions = await updateCompletion(mockContext, partialArg); + expect(suggestions).toEqual(expected); + }); + }); }); }); diff --git a/packages/cli/src/ui/commands/extensionsCommand.ts b/packages/cli/src/ui/commands/extensionsCommand.ts index 037ae1f94e..fe9e6e5c49 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.ts @@ -142,6 +142,19 @@ const updateExtensionsCommand: SlashCommand = { description: 'Update extensions. Usage: update |--all', kind: CommandKind.BUILT_IN, action: updateAction, + completion: async (context, partialArg) => { + const extensions = context.services.config?.getExtensions() ?? []; + const extensionNames = extensions.map((ext) => ext.name); + const suggestions = extensionNames.filter((name) => + name.startsWith(partialArg), + ); + + if ('--all'.startsWith(partialArg) || 'all'.startsWith(partialArg)) { + suggestions.unshift('--all'); + } + + return suggestions; + }, }; export const extensionsCommand: SlashCommand = {