From cc0b95474e44b3a1300920c386823a72fcf1072c Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:33:20 +0800 Subject: [PATCH] fix(cli): honor list extensions flag --- packages/cli/src/gemini.test.tsx | 51 ++++++++++++++++++++++++++++++++ packages/cli/src/gemini.tsx | 15 ++++------ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 28e5337851..10b991267d 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -27,6 +27,7 @@ import type { Config } from '@qwen-code/qwen-code-core'; import { ApprovalMode, OutputFormat } from '@qwen-code/qwen-code-core'; const mockWriteStderrLine = vi.hoisted(() => vi.fn()); +const mockHandleListExtensions = vi.hoisted(() => vi.fn()); // Custom error to identify mock process.exit calls class MockProcessExitError extends Error { @@ -110,6 +111,10 @@ vi.mock('./core/initializer.js', () => ({ }), })); +vi.mock('./commands/extensions/list.js', () => ({ + handleList: mockHandleListExtensions, +})); + describe('gemini.tsx main function', () => { let originalEnvGeminiSandbox: string | undefined; let originalEnvSandbox: string | undefined; @@ -229,6 +234,52 @@ describe('gemini.tsx main function', () => { processExitSpy.mockRestore(); }); + it('handles --list-extensions before sandbox and app config startup', async () => { + vi.clearAllMocks(); + const processExitSpy = vi + .spyOn(process, 'exit') + .mockImplementation((code) => { + throw new MockProcessExitError(code); + }); + + const { loadCliConfig, parseArguments } = await import( + './config/config.js' + ); + const { loadSettings } = await import('./config/settings.js'); + const { loadSandboxConfig } = await import('./config/sandboxConfig.js'); + + vi.mocked(parseArguments).mockResolvedValue({ + listExtensions: true, + } as unknown as CliArgs); + vi.mocked(loadSettings).mockReturnValue({ + errors: [], + merged: { + advanced: {}, + security: { auth: {} }, + ui: {}, + }, + setValue: vi.fn(), + forScope: () => ({ settings: {}, originalSettings: {}, path: '' }), + migrationWarnings: [], + getUserHooks: () => undefined, + getProjectHooks: () => undefined, + } as never); + mockHandleListExtensions.mockResolvedValue(undefined); + + try { + await main(); + } catch (e) { + if (!(e instanceof MockProcessExitError)) throw e; + } + + expect(mockHandleListExtensions).toHaveBeenCalledOnce(); + expect(processExitSpy).toHaveBeenCalledWith(0); + expect(loadSandboxConfig).not.toHaveBeenCalled(); + expect(loadCliConfig).not.toHaveBeenCalled(); + + processExitSpy.mockRestore(); + }); + it('should skip full settings discovery in bare mode', async () => { const originalArgv = process.argv; process.argv = ['node', 'script.js', '--bare']; diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 83712872d8..a6616ef001 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -40,6 +40,7 @@ import { initializeApp, type InitializationResult, } from './core/initializer.js'; +import { handleList as handleListExtensions } from './commands/extensions/list.js'; import { runNonInteractive } from './nonInteractiveCli.js'; import { setupStartupWorktree, @@ -466,6 +467,11 @@ export async function main() { ); } + if (argv.listExtensions) { + await handleListExtensions(); + process.exit(0); + } + // Check for invalid input combinations early to prevent crashes if (argv.promptInteractive && !process.stdin.isTTY) { writeStderrLine( @@ -841,15 +847,6 @@ export async function main() { ); } - // FIXME: list extensions after the config initialize - // if (config.getListExtensions()) { - // console.log('Installed extensions:'); - // for (const extension of extensions) { - // console.log(`- ${extension.config.name}`); - // } - // process.exit(0); - // } - const wasRaw = process.stdin.isRaw; let kittyProtocolDetectionComplete: Promise | undefined; let themeAutoDetectionComplete: Promise | undefined;