From 0dc69bd36420d43c8e7ac0d113dff82c13449bb3 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 27 Jan 2026 22:27:42 +0900 Subject: [PATCH] feat(cli): add `gemini extensions list --output-format=json` (#14479) Signed-off-by: Akihiro Suda Co-authored-by: Bryan Morgan --- .../cli/src/commands/extensions/list.test.ts | 57 ++++++++++++++++++- packages/cli/src/commands/extensions/list.ts | 42 ++++++++++---- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/commands/extensions/list.test.ts b/packages/cli/src/commands/extensions/list.test.ts index cc17e6410b..6967719be8 100644 --- a/packages/cli/src/commands/extensions/list.test.ts +++ b/packages/cli/src/commands/extensions/list.test.ts @@ -78,6 +78,17 @@ describe('extensions list command', () => { mockCwd.mockRestore(); }); + it('should output empty JSON array if no extensions are installed and output-format is json', async () => { + const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); + mockExtensionManager.prototype.loadExtensions = vi + .fn() + .mockResolvedValue([]); + await handleList({ outputFormat: 'json' }); + + expect(emitConsoleLog).toHaveBeenCalledWith('log', '[]'); + mockCwd.mockRestore(); + }); + it('should list all installed extensions', async () => { const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); const extensions = [ @@ -99,6 +110,24 @@ describe('extensions list command', () => { mockCwd.mockRestore(); }); + it('should list all installed extensions in JSON format', async () => { + const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir'); + const extensions = [ + { name: 'ext1', version: '1.0.0' }, + { name: 'ext2', version: '2.0.0' }, + ]; + mockExtensionManager.prototype.loadExtensions = vi + .fn() + .mockResolvedValue(extensions); + await handleList({ outputFormat: 'json' }); + + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', + JSON.stringify(extensions, null, 2), + ); + mockCwd.mockRestore(); + }); + it('should log an error message and exit with code 1 when listing fails', async () => { const mockProcessExit = vi .spyOn(process, 'exit') @@ -130,11 +159,35 @@ describe('extensions list command', () => { expect(command.describe).toBe('Lists installed extensions.'); }); - it('handler should call handleList', async () => { + it('builder should have output-format option', () => { + const mockYargs = { + option: vi.fn().mockReturnThis(), + }; + ( + command.builder as unknown as ( + yargs: typeof mockYargs, + ) => typeof mockYargs + )(mockYargs); + expect(mockYargs.option).toHaveBeenCalledWith('output-format', { + alias: 'o', + type: 'string', + describe: 'The format of the CLI output.', + choices: ['text', 'json'], + default: 'text', + }); + }); + + it('handler should call handleList with parsed arguments', async () => { mockExtensionManager.prototype.loadExtensions = vi .fn() .mockResolvedValue([]); - await (command.handler as () => Promise)(); + await ( + command.handler as unknown as (args: { + 'output-format': string; + }) => Promise + )({ + 'output-format': 'json', + }); expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled(); }); }); diff --git a/packages/cli/src/commands/extensions/list.ts b/packages/cli/src/commands/extensions/list.ts index 6faa795bd7..39a8a3f108 100644 --- a/packages/cli/src/commands/extensions/list.ts +++ b/packages/cli/src/commands/extensions/list.ts @@ -13,7 +13,7 @@ import { loadSettings } from '../../config/settings.js'; import { promptForSetting } from '../../config/extensions/extensionSettings.js'; import { exitCli } from '../utils.js'; -export async function handleList() { +export async function handleList(options?: { outputFormat?: 'text' | 'json' }) { try { const workspaceDir = process.cwd(); const extensionManager = new ExtensionManager({ @@ -24,16 +24,25 @@ export async function handleList() { }); const extensions = await extensionManager.loadExtensions(); if (extensions.length === 0) { - debugLogger.log('No extensions installed.'); + if (options?.outputFormat === 'json') { + debugLogger.log('[]'); + } else { + debugLogger.log('No extensions installed.'); + } return; } - debugLogger.log( - extensions - .map((extension, _): string => - extensionManager.toOutputString(extension), - ) - .join('\n\n'), - ); + + if (options?.outputFormat === 'json') { + debugLogger.log(JSON.stringify(extensions, null, 2)); + } else { + debugLogger.log( + extensions + .map((extension, _): string => + extensionManager.toOutputString(extension), + ) + .join('\n\n'), + ); + } } catch (error) { debugLogger.error(getErrorMessage(error)); process.exit(1); @@ -43,9 +52,18 @@ export async function handleList() { export const listCommand: CommandModule = { command: 'list', describe: 'Lists installed extensions.', - builder: (yargs) => yargs, - handler: async () => { - await handleList(); + builder: (yargs) => + yargs.option('output-format', { + alias: 'o', + type: 'string', + describe: 'The format of the CLI output.', + choices: ['text', 'json'], + default: 'text', + }), + handler: async (argv) => { + await handleList({ + outputFormat: argv['output-format'] as 'text' | 'json', + }); await exitCli(); }, };