From ea061f52b0af014e70df607832cf8df9cd0f77df Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Mon, 29 Sep 2025 06:53:19 -0700 Subject: [PATCH] Fix `-e ` for disabled extensions (#9994) --- .../cli/src/commands/extensions/update.ts | 12 +- packages/cli/src/commands/mcp/list.test.ts | 9 +- packages/cli/src/commands/mcp/list.ts | 7 +- packages/cli/src/config/config.test.ts | 1159 +++++++++++++++-- packages/cli/src/config/config.ts | 4 +- packages/cli/src/config/extension.test.ts | 113 +- packages/cli/src/config/extension.ts | 73 +- .../config/extensions/extensionEnablement.ts | 51 +- .../cli/src/config/extensions/update.test.ts | 18 +- packages/cli/src/gemini.tsx | 11 +- .../src/ui/hooks/useExtensionUpdates.test.ts | 4 +- .../cli/src/zed-integration/zedIntegration.ts | 7 +- 12 files changed, 1260 insertions(+), 208 deletions(-) diff --git a/packages/cli/src/commands/extensions/update.ts b/packages/cli/src/commands/extensions/update.ts index 4c3e0efc85..60ed6b771d 100644 --- a/packages/cli/src/commands/extensions/update.ts +++ b/packages/cli/src/commands/extensions/update.ts @@ -8,6 +8,7 @@ import type { CommandModule } from 'yargs'; import { loadExtensions, annotateActiveExtensions, + ExtensionStorage, requestConsentNonInteractive, } from '../../config/extension.js'; import { @@ -19,6 +20,7 @@ import { import { checkForExtensionUpdate } from '../../config/extensions/github.js'; import { getErrorMessage } from '../../utils/errors.js'; import { ExtensionUpdateState } from '../../ui/state/extensions.js'; +import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; interface UpdateArgs { name?: string; @@ -30,11 +32,17 @@ const updateOutput = (info: ExtensionUpdateInfo) => export async function handleUpdate(args: UpdateArgs) { const workingDir = process.cwd(); - const allExtensions = loadExtensions(); + const extensionEnablementManager = new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + // Force enable named extensions, otherwise we will only update the enabled + // ones. + args.name ? [args.name] : [], + ); + const allExtensions = loadExtensions(extensionEnablementManager); const extensions = annotateActiveExtensions( allExtensions, - allExtensions.map((e) => e.config.name), workingDir, + extensionEnablementManager, ); if (args.name) { try { diff --git a/packages/cli/src/commands/mcp/list.test.ts b/packages/cli/src/commands/mcp/list.test.ts index f3eb72ea22..31376ee5ee 100644 --- a/packages/cli/src/commands/mcp/list.test.ts +++ b/packages/cli/src/commands/mcp/list.test.ts @@ -7,7 +7,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { listMcpServers } from './list.js'; import { loadSettings } from '../../config/settings.js'; -import { loadExtensions } from '../../config/extension.js'; +import { ExtensionStorage, loadExtensions } from '../../config/extension.js'; import { createTransport } from '@google/gemini-cli-core'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -16,6 +16,9 @@ vi.mock('../../config/settings.js', () => ({ })); vi.mock('../../config/extension.js', () => ({ loadExtensions: vi.fn(), + ExtensionStorage: { + getUserExtensionsDir: vi.fn(), + }, })); vi.mock('@google/gemini-cli-core', () => ({ createTransport: vi.fn(), @@ -34,6 +37,7 @@ vi.mock('@google/gemini-cli-core', () => ({ })); vi.mock('@modelcontextprotocol/sdk/client/index.js'); +const mockedExtensionStorage = ExtensionStorage as vi.Mock; const mockedLoadSettings = loadSettings as vi.Mock; const mockedLoadExtensions = loadExtensions as vi.Mock; const mockedCreateTransport = createTransport as vi.Mock; @@ -69,6 +73,9 @@ describe('mcp list command', () => { MockedClient.mockImplementation(() => mockClient); mockedCreateTransport.mockResolvedValue(mockTransport); mockedLoadExtensions.mockReturnValue([]); + mockedExtensionStorage.getUserExtensionsDir.mockReturnValue( + '/mocked/extensions/dir', + ); }); afterEach(() => { diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts index 547f8631dd..da6457d10e 100644 --- a/packages/cli/src/commands/mcp/list.ts +++ b/packages/cli/src/commands/mcp/list.ts @@ -10,7 +10,8 @@ import { loadSettings } from '../../config/settings.js'; import type { MCPServerConfig } from '@google/gemini-cli-core'; import { MCPServerStatus, createTransport } from '@google/gemini-cli-core'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { loadExtensions } from '../../config/extension.js'; +import { ExtensionStorage, loadExtensions } from '../../config/extension.js'; +import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; const COLOR_GREEN = '\u001b[32m'; const COLOR_YELLOW = '\u001b[33m'; @@ -21,7 +22,9 @@ async function getMcpServersFromConfig(): Promise< Record > { const settings = loadSettings(); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); const mcpServers = { ...(settings.merged.mcpServers || {}) }; for (const extension of extensions) { Object.entries(extension.config.mcpServers || {}).forEach( diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 3f8aff414b..ea7b9d6b98 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -17,9 +17,10 @@ import { } from '@google/gemini-cli-core'; import { loadCliConfig, parseArguments, type CliArgs } from './config.js'; import type { Settings } from './settings.js'; -import type { Extension } from './extension.js'; +import { ExtensionStorage, type Extension } from './extension.js'; import * as ServerConfig from '@google/gemini-cli-core'; import { isWorkspaceTrusted } from './trustedFolders.js'; +import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; vi.mock('./trustedFolders.js', () => ({ isWorkspaceTrusted: vi @@ -347,7 +348,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js', '--show-memory-usage']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getShowMemoryUsage()).toBe(true); }); @@ -355,7 +365,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getShowMemoryUsage()).toBe(false); }); @@ -363,7 +382,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { ui: { showMemoryUsage: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getShowMemoryUsage()).toBe(false); }); @@ -371,7 +399,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js', '--show-memory-usage']; const argv = await parseArguments({} as Settings); const settings: Settings = { ui: { showMemoryUsage: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getShowMemoryUsage()).toBe(true); }); @@ -405,7 +442,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getProxy()).toBeFalsy(); }); @@ -446,7 +492,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getProxy()).toBe(expected); }); }); @@ -455,7 +510,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getProxy()).toBe('http://localhost:7890'); }); @@ -464,7 +528,16 @@ describe('loadCliConfig', () => { process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getProxy()).toBe('http://localhost:7890'); }); }); @@ -489,7 +562,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(false); }); @@ -497,7 +579,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js', '--telemetry']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -505,7 +596,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js', '--no-telemetry']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(false); }); @@ -513,7 +613,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -521,7 +630,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(false); }); @@ -529,7 +647,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js', '--telemetry']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -537,7 +664,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js', '--no-telemetry']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(false); }); @@ -547,7 +683,16 @@ describe('loadCliConfig telemetry', () => { const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpEndpoint()).toBe( 'http://settings.example.com', ); @@ -564,7 +709,16 @@ describe('loadCliConfig telemetry', () => { const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpEndpoint()).toBe('http://cli.example.com'); }); @@ -572,7 +726,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317'); }); @@ -582,7 +745,16 @@ describe('loadCliConfig telemetry', () => { const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryTarget()).toBe( ServerConfig.DEFAULT_TELEMETRY_TARGET, ); @@ -594,7 +766,16 @@ describe('loadCliConfig telemetry', () => { const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryTarget()).toBe('gcp'); }); @@ -602,7 +783,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryTarget()).toBe( ServerConfig.DEFAULT_TELEMETRY_TARGET, ); @@ -612,7 +802,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); @@ -620,7 +819,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js', '--telemetry-log-prompts']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); @@ -628,7 +836,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js', '--no-telemetry-log-prompts']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); @@ -636,7 +853,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); @@ -646,7 +872,16 @@ describe('loadCliConfig telemetry', () => { const settings: Settings = { telemetry: { otlpProtocol: 'http' }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpProtocol()).toBe('http'); }); @@ -656,7 +891,16 @@ describe('loadCliConfig telemetry', () => { const settings: Settings = { telemetry: { otlpProtocol: 'grpc' }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpProtocol()).toBe('http'); }); @@ -664,7 +908,16 @@ describe('loadCliConfig telemetry', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpProtocol()).toBe('grpc'); }); @@ -741,7 +994,17 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { }, ]; const argv = await parseArguments({} as Settings); - await loadCliConfig(settings, extensions, 'session-id', argv); + await loadCliConfig( + settings, + extensions, + + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'session-id', + argv, + ); expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( expect.any(String), [], @@ -816,7 +1079,16 @@ describe('mergeMcpServers', () => { const originalSettings = JSON.parse(JSON.stringify(settings)); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - await loadCliConfig(settings, extensions, 'test-session', argv); + await loadCliConfig( + settings, + extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(settings).toEqual(originalSettings); }); }); @@ -860,6 +1132,10 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -887,6 +1163,10 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -923,6 +1203,10 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -941,6 +1225,10 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -956,6 +1244,10 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -970,6 +1262,10 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -997,6 +1293,10 @@ describe('mergeExcludeTools', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1022,7 +1322,16 @@ describe('mergeExcludeTools', () => { const originalSettings = JSON.parse(JSON.stringify(settings)); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - await loadCliConfig(settings, extensions, 'test-session', argv); + await loadCliConfig( + settings, + extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(settings).toEqual(originalSettings); }); }); @@ -1048,6 +1357,10 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1074,6 +1387,10 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1100,6 +1417,10 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1126,6 +1447,10 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1145,6 +1470,10 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1175,6 +1504,10 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1202,6 +1535,10 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1224,11 +1561,14 @@ describe('Approval mode tool exclusion logic', () => { const settings: Settings = {}; const extensions: Extension[] = []; - await expect( loadCliConfig( settings, extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + invalidArgv.extensions, + ), 'test-session', invalidArgv as CliArgs, ), @@ -1264,7 +1604,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow all MCP servers if the flag is not provided', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig(baseSettings, [], 'test-session', argv); + const config = await loadCliConfig( + baseSettings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual(baseSettings.mcpServers); }); @@ -1276,7 +1625,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { 'server1', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig(baseSettings, [], 'test-session', argv); + const config = await loadCliConfig( + baseSettings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -1292,7 +1650,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { 'server3', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig(baseSettings, [], 'test-session', argv); + const config = await loadCliConfig( + baseSettings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, server3: { url: 'http://localhost:8082' }, @@ -1309,7 +1676,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { 'server4', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig(baseSettings, [], 'test-session', argv); + const config = await loadCliConfig( + baseSettings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -1318,7 +1694,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow no MCP servers if the flag is provided but empty', async () => { process.argv = ['node', 'script.js', '--allowed-mcp-server-names', '']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig(baseSettings, [], 'test-session', argv); + const config = await loadCliConfig( + baseSettings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({}); }); @@ -1329,7 +1714,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { ...baseSettings, mcp: { allowed: ['server1', 'server2'] }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, server2: { url: 'http://localhost:8081' }, @@ -1343,7 +1737,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { ...baseSettings, mcp: { excluded: ['server1', 'server2'] }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server3: { url: 'http://localhost:8082' }, }); @@ -1359,7 +1762,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { allowed: ['server1', 'server2'], }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server2: { url: 'http://localhost:8081' }, }); @@ -1380,7 +1792,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { allowed: ['server2'], }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -1403,7 +1824,16 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { excluded: ['server3'], // Should be ignored }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getMcpServers()).toEqual({ server2: { url: 'http://localhost:8081' }, server3: { url: 'http://localhost:8082' }, @@ -1432,6 +1862,10 @@ describe('loadCliConfig extensions', () => { const config = await loadCliConfig( settings, mockExtensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1448,6 +1882,10 @@ describe('loadCliConfig extensions', () => { const config = await loadCliConfig( settings, mockExtensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1466,6 +1904,10 @@ describe('loadCliConfig model selection', () => { }, }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1481,6 +1923,10 @@ describe('loadCliConfig model selection', () => { // No model set. }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1498,6 +1944,10 @@ describe('loadCliConfig model selection', () => { }, }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1513,6 +1963,10 @@ describe('loadCliConfig model selection', () => { // No model provided via settings. }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1532,6 +1986,10 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1549,6 +2007,10 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1566,6 +2028,10 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1586,6 +2052,10 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1604,6 +2074,10 @@ describe('loadCliConfig model selection with model router', () => { }, }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -1637,7 +2111,16 @@ describe('loadCliConfig folderTrust', () => { }, }; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getFolderTrust()).toBe(false); }); @@ -1651,7 +2134,16 @@ describe('loadCliConfig folderTrust', () => { }, }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getFolderTrust()).toBe(true); }); @@ -1659,7 +2151,16 @@ describe('loadCliConfig folderTrust', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getFolderTrust()).toBe(false); }); }); @@ -1700,7 +2201,16 @@ describe('loadCliConfig with includeDirectories', () => { ], }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); const expected = [ mockCwd, path.resolve(path.sep, 'cli', 'path1'), @@ -1743,7 +2253,16 @@ describe('loadCliConfig chatCompression', () => { }, }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getChatCompression()).toEqual({ contextPercentageThreshold: 0.5, }); @@ -1753,7 +2272,16 @@ describe('loadCliConfig chatCompression', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getChatCompression()).toBeUndefined(); }); }); @@ -1777,7 +2305,16 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getUseRipgrep()).toBe(true); }); @@ -1785,7 +2322,16 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { tools: { useRipgrep: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getUseRipgrep()).toBe(false); }); @@ -1793,7 +2339,16 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { tools: { useRipgrep: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getUseRipgrep()).toBe(true); }); @@ -1802,7 +2357,16 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getUseModelRouter()).toBe(true); }); @@ -1810,7 +2374,16 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { experimental: { useModelRouter: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getUseModelRouter()).toBe(true); }); @@ -1818,7 +2391,16 @@ describe('loadCliConfig useRipgrep', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { experimental: { useModelRouter: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getUseModelRouter()).toBe(false); }); }); @@ -1845,7 +2427,16 @@ describe('screenReader configuration', () => { const settings: Settings = { ui: { accessibility: { screenReader: true } }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getScreenReader()).toBe(true); }); @@ -1855,7 +2446,16 @@ describe('screenReader configuration', () => { const settings: Settings = { ui: { accessibility: { screenReader: false } }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getScreenReader()).toBe(false); }); @@ -1865,7 +2465,16 @@ describe('screenReader configuration', () => { const settings: Settings = { ui: { accessibility: { screenReader: false } }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getScreenReader()).toBe(true); }); @@ -1873,7 +2482,16 @@ describe('screenReader configuration', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getScreenReader()).toBe(false); }); }); @@ -1901,7 +2519,16 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getExcludeTools()).not.toContain('run_shell_command'); expect(config.getExcludeTools()).not.toContain('replace'); expect(config.getExcludeTools()).not.toContain('write_file'); @@ -1911,7 +2538,16 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getExcludeTools()).not.toContain('run_shell_command'); expect(config.getExcludeTools()).not.toContain('replace'); expect(config.getExcludeTools()).not.toContain('write_file'); @@ -1921,7 +2557,16 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '-p', 'test']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getExcludeTools()).toContain('run_shell_command'); expect(config.getExcludeTools()).toContain('replace'); expect(config.getExcludeTools()).toContain('write_file'); @@ -1931,7 +2576,16 @@ describe('loadCliConfig tool exclusions', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '-p', 'test', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getExcludeTools()).not.toContain('run_shell_command'); expect(config.getExcludeTools()).not.toContain('replace'); expect(config.getExcludeTools()).not.toContain('write_file'); @@ -1960,7 +2614,16 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.isInteractive()).toBe(true); }); @@ -1968,7 +2631,16 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '--prompt-interactive', 'test']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.isInteractive()).toBe(true); }); @@ -1976,7 +2648,16 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.isInteractive()).toBe(false); }); @@ -1984,7 +2665,16 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--prompt', 'test']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.isInteractive()).toBe(false); }); @@ -1992,7 +2682,16 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--model', 'gemini-1.5-pro', 'Hello']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.isInteractive()).toBe(false); }); @@ -2007,7 +2706,16 @@ describe('loadCliConfig interactive', () => { 'Hello world', ]; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.isInteractive()).toBe(false); }); @@ -2015,7 +2723,16 @@ describe('loadCliConfig interactive', () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--model', 'gemini-1.5-pro']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.isInteractive()).toBe(true); }); }); @@ -2040,42 +2757,96 @@ describe('loadCliConfig approval mode', () => { it('should default to DEFAULT approval mode when no flags are set', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set YOLO approval mode when --yolo flag is used', async () => { process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); it('should set YOLO approval mode when -y flag is used', async () => { process.argv = ['node', 'script.js', '-y']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); it('should set DEFAULT approval mode when --approval-mode=default', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'default']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT); }); it('should set YOLO approval mode when --approval-mode=yolo', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); @@ -2086,14 +2857,32 @@ describe('loadCliConfig approval mode', () => { const argv = await parseArguments({} as Settings); // Manually set yolo to true to simulate what would happen if validation didn't prevent it argv.yolo = true; - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should fall back to --yolo behavior when --approval-mode is not set', async () => { process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); @@ -2109,28 +2898,64 @@ describe('loadCliConfig approval mode', () => { it('should override --approval-mode=yolo to DEFAULT', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should override --approval-mode=auto_edit to DEFAULT', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should override --yolo flag to DEFAULT', async () => { process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should remain DEFAULT when --approval-mode=default', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'default']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); }); @@ -2208,7 +3033,16 @@ describe('loadCliConfig fileFiltering', () => { }, }; const argv = await parseArguments(settings); - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(getter(config)).toBe(value); }, ); @@ -2218,7 +3052,16 @@ describe('Output format', () => { it('should default to TEXT', async () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getOutputFormat()).toBe(OutputFormat.TEXT); }); @@ -2228,6 +3071,10 @@ describe('Output format', () => { const config = await loadCliConfig( { output: { format: OutputFormat.JSON } }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -2240,6 +3087,10 @@ describe('Output format', () => { const config = await loadCliConfig( { output: { format: OutputFormat.JSON } }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -2323,7 +3174,16 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -2332,7 +3192,16 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { target: 'local' } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryTarget()).toBe('gcp'); }); @@ -2342,7 +3211,16 @@ describe('Telemetry configuration via environment variables', () => { const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { target: 'gcp' } }; await expect( - loadCliConfig(settings, [], 'test-session', argv), + loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ), ).rejects.toThrow( /Invalid telemetry configuration: .*Invalid telemetry target/i, ); @@ -2357,7 +3235,16 @@ describe('Telemetry configuration via environment variables', () => { const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.com' }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpEndpoint()).toBe('http://gemini.env.com'); }); @@ -2366,7 +3253,16 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { otlpProtocol: 'grpc' } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOtlpProtocol()).toBe('http'); }); @@ -2375,7 +3271,16 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); @@ -2386,7 +3291,16 @@ describe('Telemetry configuration via environment variables', () => { const settings: Settings = { telemetry: { outfile: '/settings/telemetry.log' }, }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryOutfile()).toBe('/gemini/env/telemetry.log'); }); @@ -2395,7 +3309,16 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { useCollector: false } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryUseCollector()).toBe(true); }); @@ -2404,7 +3327,16 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -2413,7 +3345,16 @@ describe('Telemetry configuration via environment variables', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { target: 'local' } }; - const config = await loadCliConfig(settings, [], 'test-session', argv); + const config = await loadCliConfig( + settings, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryTarget()).toBe('local'); }); @@ -2421,7 +3362,16 @@ describe('Telemetry configuration via environment variables', () => { vi.stubEnv('GEMINI_TELEMETRY_ENABLED', '1'); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryEnabled()).toBe(true); }); @@ -2432,6 +3382,10 @@ describe('Telemetry configuration via environment variables', () => { const config = await loadCliConfig( { telemetry: { enabled: true } }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); @@ -2442,7 +3396,16 @@ describe('Telemetry configuration via environment variables', () => { vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', '1'); process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); - const config = await loadCliConfig({}, [], 'test-session', argv); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); @@ -2453,6 +3416,10 @@ describe('Telemetry configuration via environment variables', () => { const config = await loadCliConfig( { telemetry: { logPrompts: true } }, [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), 'test-session', argv, ); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index c74a503c34..a3a294a00b 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -45,6 +45,7 @@ import { appEvents } from '../utils/events.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { createPolicyEngineConfig } from './policy.js'; +import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; // Simple console logger for now - replace with actual logger if available const logger = { @@ -408,6 +409,7 @@ export function isDebugMode(argv: CliArgs): boolean { export async function loadCliConfig( settings: Settings, extensions: Extension[], + extensionEnablementManager: ExtensionEnablementManager, sessionId: string, argv: CliArgs, cwd: string = process.cwd(), @@ -423,8 +425,8 @@ export async function loadCliConfig( const allExtensions = annotateActiveExtensions( extensions, - argv.extensions || [], cwd, + extensionEnablementManager, ); const activeExtensions = extensions.filter( diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 19d22a5002..932524d3ac 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -10,6 +10,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { EXTENSIONS_CONFIG_FILENAME, + ExtensionStorage, INSTALL_METADATA_FILENAME, annotateActiveExtensions, disableExtension, @@ -152,7 +153,9 @@ describe('extension tests', () => { version: '1.0.0', }); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); expect(extensions[0].path).toBe(extensionDir); expect(extensions[0].config.name).toBe('test-extension'); @@ -171,7 +174,9 @@ describe('extension tests', () => { version: '2.0.0', }); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(2); const ext1 = extensions.find((e) => e.config.name === 'ext1'); @@ -191,7 +196,9 @@ describe('extension tests', () => { contextFileName: 'my-context-file.md', }); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); const ext1 = extensions.find((e) => e.config.name === 'ext1'); @@ -216,11 +223,14 @@ describe('extension tests', () => { SettingScope.User, tempWorkspaceDir, ); - const extensions = loadExtensions(); + const manager = new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ); + const extensions = loadExtensions(manager); const activeExtensions = annotateActiveExtensions( extensions, - [], tempWorkspaceDir, + manager, ).filter((e) => e.isActive); expect(activeExtensions).toHaveLength(1); expect(activeExtensions[0].name).toBe('enabled-extension'); @@ -240,7 +250,9 @@ describe('extension tests', () => { }, }); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); const loadedConfig = extensions[0].config; const expectedCwd = path.join( @@ -269,7 +281,9 @@ describe('extension tests', () => { ); expect(extensionName).toEqual('my-linked-extension'); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); const linkedExt = extensions[0]; @@ -318,7 +332,11 @@ describe('extension tests', () => { }; fs.writeFileSync(configPath, JSON.stringify(extensionConfig)); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ), + ); expect(extensions).toHaveLength(1); const extension = extensions[0]; @@ -369,7 +387,9 @@ describe('extension tests', () => { JSON.stringify(extensionConfig), ); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); const extension = extensions[0]; @@ -397,7 +417,9 @@ describe('extension tests', () => { const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME); fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); expect(extensions[0].config.name).toBe('good-ext'); @@ -429,7 +451,9 @@ describe('extension tests', () => { const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME); fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' })); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); expect(extensions[0].config.name).toBe('good-ext'); @@ -457,7 +481,9 @@ describe('extension tests', () => { }, }); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(1); const loadedConfig = extensions[0].config; expect(loadedConfig.mcpServers?.['test-server'].trust).toBeUndefined(); @@ -508,8 +534,8 @@ describe('extension tests', () => { it('should mark all extensions as active if no enabled extensions are provided', () => { const activeExtensions = annotateActiveExtensions( extensions, - [], '/path/to/workspace', + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), ); expect(activeExtensions).toHaveLength(3); expect(activeExtensions.every((e) => e.isActive)).toBe(true); @@ -518,8 +544,11 @@ describe('extension tests', () => { it('should mark only the enabled extensions as active', () => { const activeExtensions = annotateActiveExtensions( extensions, - ['ext1', 'ext3'], '/path/to/workspace', + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ['ext1', 'ext3'], + ), ); expect(activeExtensions).toHaveLength(3); expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe( @@ -536,8 +565,11 @@ describe('extension tests', () => { it('should mark all extensions as inactive when "none" is provided', () => { const activeExtensions = annotateActiveExtensions( extensions, - ['none'], '/path/to/workspace', + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ['none'], + ), ); expect(activeExtensions).toHaveLength(3); expect(activeExtensions.every((e) => !e.isActive)).toBe(true); @@ -546,8 +578,11 @@ describe('extension tests', () => { it('should handle case-insensitivity', () => { const activeExtensions = annotateActiveExtensions( extensions, - ['EXT1'], '/path/to/workspace', + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ['EXT1'], + ), ); expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe( true, @@ -558,7 +593,14 @@ describe('extension tests', () => { const consoleSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); - annotateActiveExtensions(extensions, ['ext4'], '/path/to/workspace'); + annotateActiveExtensions( + extensions, + '/path/to/workspace', + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ['ext4'], + ), + ); expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4'); consoleSpy.mockRestore(); }); @@ -567,8 +609,10 @@ describe('extension tests', () => { it('should be false if autoUpdate is not set in install metadata', () => { const activeExtensions = annotateActiveExtensions( extensions, - [], tempHomeDir, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ), ); expect( activeExtensions.every( @@ -587,8 +631,10 @@ describe('extension tests', () => { })); const activeExtensions = annotateActiveExtensions( extensionsWithAutoUpdate, - [], tempHomeDir, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ), ); expect( activeExtensions.every((e) => e.installMetadata?.autoUpdate === true), @@ -625,8 +671,10 @@ describe('extension tests', () => { ]; const activeExtensions = annotateActiveExtensions( extensionsWithAutoUpdate, - [], tempHomeDir, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ), ); expect( activeExtensions.find((e) => e.name === 'ext1')?.installMetadata @@ -1015,7 +1063,13 @@ This extension will run the following MCP servers: await uninstallExtension('my-local-extension'); expect(fs.existsSync(sourceExtDir)).toBe(false); - expect(loadExtensions()).toHaveLength(1); + expect( + loadExtensions( + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ), + ), + ).toHaveLength(1); expect(fs.existsSync(otherExtDir)).toBe(true); }); @@ -1154,7 +1208,11 @@ This extension will run the following MCP servers: ], async (_) => true, ); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ), + ); expect(extensions).toEqual([]); }); @@ -1194,7 +1252,9 @@ This extension will run the following MCP servers: 'extensions', ); const userExt1Path = path.join(userExtensionsDir, 'ext1'); - const extensions = loadExtensions(); + const extensions = loadExtensions( + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), + ); expect(extensions).toHaveLength(2); const metadataPath = path.join(userExt1Path, INSTALL_METADATA_FILENAME); @@ -1326,11 +1386,14 @@ This extension will run the following MCP servers: }); const getActiveExtensions = (): GeminiCLIExtension[] => { - const extensions = loadExtensions(); + const manager = new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + ); + const extensions = loadExtensions(manager); const activeExtensions = annotateActiveExtensions( extensions, - [], tempWorkspaceDir, + manager, ); return activeExtensions.filter((e) => e.isActive); }; diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 2effae1d9f..39204a317c 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -146,6 +146,7 @@ function getTelemetryConfig(cwd: string) { } export function loadExtensions( + extensionEnablementManager: ExtensionEnablementManager, workspaceDir: string = process.cwd(), ): Extension[] { const settings = loadSettings(workspaceDir).merged; @@ -160,14 +161,11 @@ export function loadExtensions( } const uniqueExtensions = new Map(); - const manager = new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - ); for (const extension of allExtensions) { if ( !uniqueExtensions.has(extension.config.name) && - manager.isEnabled(extension.config.name, workspaceDir) + extensionEnablementManager.isEnabled(extension.config.name, workspaceDir) ) { uniqueExtensions.set(extension.config.name, extension); } @@ -323,64 +321,17 @@ function getContextFileNames(config: ExtensionConfig): string[] { */ export function annotateActiveExtensions( extensions: Extension[], - enabledExtensionNames: string[], workspaceDir: string, + manager: ExtensionEnablementManager, ): GeminiCLIExtension[] { - const manager = new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - ); - const annotatedExtensions: GeminiCLIExtension[] = []; - if (enabledExtensionNames.length === 0) { - return extensions.map((extension) => ({ - name: extension.config.name, - version: extension.config.version, - isActive: manager.isEnabled(extension.config.name, workspaceDir), - path: extension.path, - installMetadata: extension.installMetadata, - })); - } - - const lowerCaseEnabledExtensions = new Set( - enabledExtensionNames.map((e) => e.trim().toLowerCase()), - ); - - if ( - lowerCaseEnabledExtensions.size === 1 && - lowerCaseEnabledExtensions.has('none') - ) { - return extensions.map((extension) => ({ - name: extension.config.name, - version: extension.config.version, - isActive: false, - path: extension.path, - installMetadata: extension.installMetadata, - })); - } - - const notFoundNames = new Set(lowerCaseEnabledExtensions); - - for (const extension of extensions) { - const lowerCaseName = extension.config.name.toLowerCase(); - const isActive = lowerCaseEnabledExtensions.has(lowerCaseName); - - if (isActive) { - notFoundNames.delete(lowerCaseName); - } - - annotatedExtensions.push({ - name: extension.config.name, - version: extension.config.version, - isActive, - path: extension.path, - installMetadata: extension.installMetadata, - }); - } - - for (const requestedName of notFoundNames) { - console.error(`Extension not found: ${requestedName}`); - } - - return annotatedExtensions; + manager.validateExtensionOverrides(extensions); + return extensions.map((extension) => ({ + name: extension.config.name, + version: extension.config.version, + isActive: manager.isEnabled(extension.config.name, workspaceDir), + path: extension.path, + installMetadata: extension.installMetadata, + })); } /** @@ -711,6 +662,7 @@ export async function uninstallExtension( } const manager = new ExtensionEnablementManager( ExtensionStorage.getUserExtensionsDir(), + [extensionName], ); manager.remove(extensionName); const storage = new ExtensionStorage(extensionName); @@ -789,6 +741,7 @@ export function disableExtension( const manager = new ExtensionEnablementManager( ExtensionStorage.getUserExtensionsDir(), + [name], ); const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir(); manager.disable(name, true, scopePath); diff --git a/packages/cli/src/config/extensions/extensionEnablement.ts b/packages/cli/src/config/extensions/extensionEnablement.ts index 967b6381e9..79ce5ac2d6 100644 --- a/packages/cli/src/config/extensions/extensionEnablement.ts +++ b/packages/cli/src/config/extensions/extensionEnablement.ts @@ -6,6 +6,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { type Extension } from '../extension.js'; export interface ExtensionEnablementConfig { overrides: string[]; @@ -104,24 +105,56 @@ function globToRegex(glob: string): RegExp { return new RegExp(`^${regexString}$`); } -/** - * Determines if an extension is enabled based on the configuration and current path. - * The last matching rule in the overrides list wins. - * - * @param config The enablement configuration for a single extension. - * @param currentPath The absolute path of the current working directory. - * @returns True if the extension is enabled, false otherwise. - */ export class ExtensionEnablementManager { private configFilePath: string; private configDir: string; + // If non-empty, this overrides all other extension configuration and enables + // only the ones in this list. + private enabledExtensionNamesOverride: string[]; - constructor(configDir: string) { + constructor(configDir: string, enabledExtensionNames?: string[]) { this.configDir = configDir; this.configFilePath = path.join(configDir, 'extension-enablement.json'); + this.enabledExtensionNamesOverride = + enabledExtensionNames?.map((name) => name.toLowerCase()) ?? []; } + validateExtensionOverrides(extensions: Extension[]) { + for (const name of this.enabledExtensionNamesOverride) { + if ( + !extensions.some( + (ext) => ext.config.name.toLowerCase() === name.toLowerCase(), + ) + ) { + console.error(`Extension not found: ${name}`); + } + } + } + + /** + * Determines if an extension is enabled based on its name and the current + * path. The last matching rule in the overrides list wins. + * + * @param extensionName The name of the extension. + * @param currentPath The absolute path of the current working directory. + * @returns True if the extension is enabled, false otherwise. + */ isEnabled(extensionName: string, currentPath: string): boolean { + // If we have a single override called 'none', this disables all extensions. + // Typically, this comes from the user passing `-e none`. + if ( + this.enabledExtensionNamesOverride.length === 1 && + this.enabledExtensionNamesOverride[0] === 'none' + ) { + return false; + } + + // If we have explicit overrides, only enable those extensions. + if (this.enabledExtensionNamesOverride.length > 0) { + return this.enabledExtensionNamesOverride.includes(extensionName); + } + + // Otherwise, we use the configuration settings const config = this.readConfig(); const extensionConfig = config[extensionName]; // Extensions are enabled by default. diff --git a/packages/cli/src/config/extensions/update.test.ts b/packages/cli/src/config/extensions/update.test.ts index 8e28fb9883..d542109d46 100644 --- a/packages/cli/src/config/extensions/update.test.ts +++ b/packages/cli/src/config/extensions/update.test.ts @@ -10,6 +10,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { EXTENSIONS_CONFIG_FILENAME, + ExtensionStorage, INSTALL_METADATA_FILENAME, annotateActiveExtensions, loadExtension, @@ -19,6 +20,7 @@ import { GEMINI_DIR } from '@google/gemini-cli-core'; import { isWorkspaceTrusted } from '../trustedFolders.js'; import { ExtensionUpdateState } from '../../ui/state/extensions.js'; import { createExtension } from '../../test-utils/createExtension.js'; +import { ExtensionEnablementManager } from './extensionEnablement.js'; const mockGit = { clone: vi.fn(), @@ -134,8 +136,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; const updateInfo = await updateExtension( extension, @@ -192,8 +194,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; await updateExtension( extension, @@ -234,8 +236,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; await expect( updateExtension( @@ -274,8 +276,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; mockGit.getRemotes.mockResolvedValue([ @@ -317,8 +319,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; mockGit.getRemotes.mockResolvedValue([ @@ -364,8 +366,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; let extensionState = new Map(); const results = await checkForAllExtensionUpdates( @@ -405,8 +407,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; let extensionState = new Map(); const results = await checkForAllExtensionUpdates( @@ -442,8 +444,8 @@ describe('update tests', () => { workspaceDir: tempWorkspaceDir, })!, ], - [], process.cwd(), + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; mockGit.getRemotes.mockRejectedValue(new Error('Git error')); diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 2e18c8f08d..a2cea7c18d 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -26,7 +26,7 @@ import { getStartupWarnings } from './utils/startupWarnings.js'; import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { runNonInteractive } from './nonInteractiveCli.js'; -import { loadExtensions } from './config/extension.js'; +import { ExtensionStorage, loadExtensions } from './config/extension.js'; import { cleanupCheckpoints, registerCleanup, @@ -113,6 +113,7 @@ function getNodeMemoryArgs(isDebugMode: boolean): string[] { import { runZedIntegration } from './zed-integration/zedIntegration.js'; import { loadSandboxConfig } from './config/sandboxConfig.js'; +import { ExtensionEnablementManager } from './config/extensions/extensionEnablement.js'; export function setupUnhandledRejectionHandler() { let unhandledRejectionOccurred = false; @@ -266,6 +267,7 @@ export async function main() { const partialConfig = await loadCliConfig( settings.merged, [], + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), sessionId, argv, ); @@ -336,10 +338,15 @@ export async function main() { // to run Gemini CLI. It is now safe to perform expensive initialization that // may have side effects. { - const extensions = loadExtensions(); + const extensionEnablementManager = new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ); + const extensions = loadExtensions(extensionEnablementManager); const config = await loadCliConfig( settings.merged, extensions, + extensionEnablementManager, sessionId, argv, ); diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts index ed783c7954..da30ac1c5e 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts @@ -10,6 +10,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { EXTENSIONS_CONFIG_FILENAME, + ExtensionStorage, annotateActiveExtensions, loadExtension, } from '../../config/extension.js'; @@ -19,6 +20,7 @@ import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core'; import { isWorkspaceTrusted } from '../../config/trustedFolders.js'; import { renderHook, waitFor } from '@testing-library/react'; import { MessageType } from '../types.js'; +import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; const mockGit = { clone: vi.fn(), @@ -163,8 +165,8 @@ describe('useExtensionUpdates', () => { }); const extension = annotateActiveExtensions( [loadExtension({ extensionDir, workspaceDir: tempHomeDir })!], - [], tempHomeDir, + new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), )[0]; const addItem = vi.fn(); diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index 868fbe68d2..bf6a50f641 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -40,9 +40,10 @@ import * as path from 'node:path'; import { z } from 'zod'; import { randomUUID } from 'node:crypto'; -import type { Extension } from '../config/extension.js'; +import { ExtensionStorage, type Extension } from '../config/extension.js'; import type { CliArgs } from '../config/config.js'; import { loadCliConfig } from '../config/config.js'; +import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js'; /** * Resolves the model to use based on the current configuration. @@ -204,6 +205,10 @@ class GeminiAgent { const config = await loadCliConfig( settings, this.extensions, + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + this.argv.extensions, + ), sessionId, this.argv, cwd,