From 8980276b205e2b8f327b8b55f785a01e36ce18b8 Mon Sep 17 00:00:00 2001 From: Zack Birkenbuel Date: Wed, 8 Oct 2025 07:31:41 -0700 Subject: [PATCH] Rationalize different Extension typings (#10435) --- packages/a2a-server/src/config/config.ts | 29 ++-- packages/a2a-server/src/config/extension.ts | 66 ++++++-- packages/cli/src/commands/mcp/list.test.ts | 6 +- packages/cli/src/commands/mcp/list.ts | 20 ++- packages/cli/src/config/config.test.ts | 146 +++++++++--------- packages/cli/src/config/config.ts | 36 ++--- packages/cli/src/config/extension.test.ts | 81 ++++++---- packages/cli/src/config/extension.ts | 98 ++++++------ .../extensions/extensionEnablement.test.ts | 14 +- .../config/extensions/extensionEnablement.ts | 8 +- .../cli/src/config/extensions/github.test.ts | 5 + packages/cli/src/config/extensions/github.ts | 2 +- packages/cli/src/config/extensions/update.ts | 2 +- packages/cli/src/gemini.tsx | 2 +- .../components/WorkspaceMigrationDialog.tsx | 10 +- .../src/ui/hooks/useExtensionUpdates.test.ts | 3 + .../cli/src/ui/hooks/useWorkspaceMigration.ts | 12 +- .../cli/src/zed-integration/zedIntegration.ts | 7 +- packages/core/src/config/config.ts | 9 ++ 19 files changed, 300 insertions(+), 256 deletions(-) diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index dd380b24b3..4d53f7c1b3 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -20,16 +20,16 @@ import { GEMINI_CONFIG_DIR, DEFAULT_GEMINI_EMBEDDING_MODEL, DEFAULT_GEMINI_MODEL, + type GeminiCLIExtension, } from '@google/gemini-cli-core'; import { logger } from '../utils/logger.js'; import type { Settings } from './settings.js'; -import type { Extension } from './extension.js'; import { type AgentSettings, CoderAgentEvent } from '../types.js'; export async function loadConfig( settings: Settings, - extensions: Extension[], + extensions: GeminiCLIExtension[], taskId: string, ): Promise { const mcpServers = mergeMcpServers(settings, extensions); @@ -118,20 +118,21 @@ export async function loadConfig( return config; } -export function mergeMcpServers(settings: Settings, extensions: Extension[]) { +export function mergeMcpServers( + settings: Settings, + extensions: GeminiCLIExtension[], +) { const mcpServers = { ...(settings.mcpServers || {}) }; for (const extension of extensions) { - Object.entries(extension.config.mcpServers || {}).forEach( - ([key, server]) => { - if (mcpServers[key]) { - console.warn( - `Skipping extension MCP config for server with key "${key}" as it already exists.`, - ); - return; - } - mcpServers[key] = server; - }, - ); + Object.entries(extension.mcpServers || {}).forEach(([key, server]) => { + if (mcpServers[key]) { + console.warn( + `Skipping extension MCP config for server with key "${key}" as it already exists.`, + ); + return; + } + mcpServers[key] = server; + }); } return mcpServers; } diff --git a/packages/a2a-server/src/config/extension.ts b/packages/a2a-server/src/config/extension.ts index ef08215def..4932e24122 100644 --- a/packages/a2a-server/src/config/extension.ts +++ b/packages/a2a-server/src/config/extension.ts @@ -6,7 +6,11 @@ // Copied exactly from packages/cli/src/config/extension.ts, last PR #1026 -import type { MCPServerConfig } from '@google/gemini-cli-core'; +import type { + MCPServerConfig, + ExtensionInstallMetadata, + GeminiCLIExtension, +} from '@google/gemini-cli-core'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -14,47 +18,51 @@ import { logger } from '../utils/logger.js'; export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions'); export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json'; +export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json'; -export interface Extension { - config: ExtensionConfig; - contextFiles: string[]; -} - -export interface ExtensionConfig { +/** + * Extension definition as written to disk in gemini-extension.json files. + * This should *not* be referenced outside of the logic for reading files. + * If information is required for manipulating extensions (load, unload, update) + * outside of the loading process that data needs to be stored on the + * GeminiCLIExtension class defined in Core. + */ +interface ExtensionConfig { name: string; version: string; mcpServers?: Record; contextFileName?: string | string[]; + excludeTools?: string[]; } -export function loadExtensions(workspaceDir: string): Extension[] { +export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] { const allExtensions = [ ...loadExtensionsFromDir(workspaceDir), ...loadExtensionsFromDir(os.homedir()), ]; - const uniqueExtensions: Extension[] = []; + const uniqueExtensions: GeminiCLIExtension[] = []; const seenNames = new Set(); for (const extension of allExtensions) { - if (!seenNames.has(extension.config.name)) { + if (!seenNames.has(extension.name)) { logger.info( - `Loading extension: ${extension.config.name} (version: ${extension.config.version})`, + `Loading extension: ${extension.name} (version: ${extension.version})`, ); uniqueExtensions.push(extension); - seenNames.add(extension.config.name); + seenNames.add(extension.name); } } return uniqueExtensions; } -function loadExtensionsFromDir(dir: string): Extension[] { +function loadExtensionsFromDir(dir: string): GeminiCLIExtension[] { const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME); if (!fs.existsSync(extensionsDir)) { return []; } - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; for (const subdir of fs.readdirSync(extensionsDir)) { const extensionDir = path.join(extensionsDir, subdir); @@ -66,7 +74,7 @@ function loadExtensionsFromDir(dir: string): Extension[] { return extensions; } -function loadExtension(extensionDir: string): Extension | null { +function loadExtension(extensionDir: string): GeminiCLIExtension | null { if (!fs.statSync(extensionDir).isDirectory()) { logger.error( `Warning: unexpected file ${extensionDir} in extensions directory.`, @@ -92,14 +100,22 @@ function loadExtension(extensionDir: string): Extension | null { return null; } + const installMetadata = loadInstallMetadata(extensionDir); + const contextFiles = getContextFileNames(config) .map((contextFileName) => path.join(extensionDir, contextFileName)) .filter((contextFilePath) => fs.existsSync(contextFilePath)); return { - config, + name: config.name, + version: config.version, + path: extensionDir, contextFiles, - }; + installMetadata, + mcpServers: config.mcpServers, + excludeTools: config.excludeTools, + isActive: true, // Barring any other signals extensions should be considered Active. + } as GeminiCLIExtension; } catch (e) { logger.error( `Warning: error parsing extension config in ${configFilePath}: ${e}`, @@ -116,3 +132,19 @@ function getContextFileNames(config: ExtensionConfig): string[] { } return config.contextFileName; } + +export function loadInstallMetadata( + extensionDir: string, +): ExtensionInstallMetadata | undefined { + const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME); + try { + const configContent = fs.readFileSync(metadataFilePath, 'utf-8'); + const metadata = JSON.parse(configContent) as ExtensionInstallMetadata; + return metadata; + } catch (e) { + logger.warn( + `Failed to load or parse extension install metadata at ${metadataFilePath}: ${e}`, + ); + return undefined; + } +} diff --git a/packages/cli/src/commands/mcp/list.test.ts b/packages/cli/src/commands/mcp/list.test.ts index 31376ee5ee..3a191fc418 100644 --- a/packages/cli/src/commands/mcp/list.test.ts +++ b/packages/cli/src/commands/mcp/list.test.ts @@ -153,10 +153,8 @@ describe('mcp list command', () => { mockedLoadExtensions.mockReturnValue([ { - config: { - name: 'test-extension', - mcpServers: { 'extension-server': { command: '/ext/server' } }, - }, + name: 'test-extension', + mcpServers: { 'extension-server': { command: '/ext/server' } }, }, ]); diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts index da6457d10e..173630364c 100644 --- a/packages/cli/src/commands/mcp/list.ts +++ b/packages/cli/src/commands/mcp/list.ts @@ -27,17 +27,15 @@ async function getMcpServersFromConfig(): Promise< ); const mcpServers = { ...(settings.merged.mcpServers || {}) }; for (const extension of extensions) { - Object.entries(extension.config.mcpServers || {}).forEach( - ([key, server]) => { - if (mcpServers[key]) { - return; - } - mcpServers[key] = { - ...server, - extensionName: extension.config.name, - }; - }, - ); + Object.entries(extension.mcpServers || {}).forEach(([key, server]) => { + if (mcpServers[key]) { + return; + } + mcpServers[key] = { + ...server, + extensionName: extension.name, + }; + }); } return mcpServers; } diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index af2e0ff658..e439a162f3 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -14,10 +14,11 @@ import { DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_MODEL_AUTO, OutputFormat, + type GeminiCLIExtension, } from '@google/gemini-cli-core'; import { loadCliConfig, parseArguments, type CliArgs } from './config.js'; import type { Settings } from './settings.js'; -import { ExtensionStorage, type Extension } from './extension.js'; +import { ExtensionStorage } from './extension.js'; import * as ServerConfig from '@google/gemini-cli-core'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; @@ -1098,33 +1099,30 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { it('should pass extension context file paths to loadServerHierarchicalMemory', async () => { process.argv = ['node', 'script.js']; const settings: Settings = {}; - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { - name: 'ext1', - version: '1.0.0', - }, + name: 'ext1', + version: '1.0.0', contextFiles: ['/path/to/ext1/GEMINI.md'], + isActive: true, }, { path: '/path/to/ext2', - config: { - name: 'ext2', - version: '1.0.0', - }, + name: 'ext2', + version: '1.0.0', contextFiles: [], + isActive: true, }, { path: '/path/to/ext3', - config: { - name: 'ext3', - version: '1.0.0', - }, + name: 'ext3', + version: '1.0.0', contextFiles: [ '/path/to/ext3/context1.md', '/path/to/ext3/context2.md', ], + isActive: true, }, ]; const argv = await parseArguments({} as Settings); @@ -1195,19 +1193,18 @@ describe('mergeMcpServers', () => { }, }, }; - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { - name: 'ext1', - version: '1.0.0', - mcpServers: { - 'ext1-server': { - url: 'http://localhost:8081', - }, + name: 'ext1', + version: '1.0.0', + mcpServers: { + 'ext1-server': { + url: 'http://localhost:8081', }, }, contextFiles: [], + isActive: true, }, ]; const originalSettings = JSON.parse(JSON.stringify(settings)); @@ -1241,24 +1238,22 @@ describe('mergeExcludeTools', () => { it('should merge excludeTools from settings and extensions', async () => { const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } }; - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { - name: 'ext1', - version: '1.0.0', - excludeTools: ['tool3', 'tool4'], - }, + name: 'ext1', + version: '1.0.0', + excludeTools: ['tool3', 'tool4'], contextFiles: [], + isActive: true, }, { path: '/path/to/ext2', - config: { - name: 'ext2', - version: '1.0.0', - excludeTools: ['tool5'], - }, + name: 'ext2', + version: '1.0.0', + excludeTools: ['tool5'], contextFiles: [], + isActive: true, }, ]; process.argv = ['node', 'script.js']; @@ -1281,15 +1276,14 @@ describe('mergeExcludeTools', () => { it('should handle overlapping excludeTools between settings and extensions', async () => { const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } }; - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { - name: 'ext1', - version: '1.0.0', - excludeTools: ['tool2', 'tool3'], - }, + name: 'ext1', + version: '1.0.0', + excludeTools: ['tool2', 'tool3'], contextFiles: [], + isActive: true, }, ]; process.argv = ['node', 'script.js']; @@ -1312,24 +1306,22 @@ describe('mergeExcludeTools', () => { it('should handle overlapping excludeTools between extensions', async () => { const settings: Settings = { tools: { exclude: ['tool1'] } }; - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { - name: 'ext1', - version: '1.0.0', - excludeTools: ['tool2', 'tool3'], - }, + name: 'ext1', + version: '1.0.0', + excludeTools: ['tool2', 'tool3'], contextFiles: [], + isActive: true, }, { path: '/path/to/ext2', - config: { - name: 'ext2', - version: '1.0.0', - excludeTools: ['tool3', 'tool4'], - }, + name: 'ext2', + version: '1.0.0', + excludeTools: ['tool3', 'tool4'], contextFiles: [], + isActive: true, }, ]; process.argv = ['node', 'script.js']; @@ -1353,7 +1345,7 @@ describe('mergeExcludeTools', () => { it('should return an empty array when no excludeTools are specified and it is interactive', async () => { process.stdin.isTTY = true; const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const config = await loadCliConfig( @@ -1372,7 +1364,7 @@ describe('mergeExcludeTools', () => { it('should return default excludes when no excludeTools are specified and it is not interactive', async () => { process.stdin.isTTY = false; const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; process.argv = ['node', 'script.js', '-p', 'test']; const argv = await parseArguments({} as Settings); const config = await loadCliConfig( @@ -1392,7 +1384,7 @@ describe('mergeExcludeTools', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments({} as Settings); const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } }; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, extensions, @@ -1411,15 +1403,14 @@ describe('mergeExcludeTools', () => { it('should handle extensions with excludeTools but no settings', async () => { const settings: Settings = {}; - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext', - config: { - name: 'ext1', - version: '1.0.0', - excludeTools: ['tool1', 'tool2'], - }, + name: 'ext1', + version: '1.0.0', + excludeTools: ['tool1', 'tool2'], contextFiles: [], + isActive: true, }, ]; process.argv = ['node', 'script.js']; @@ -1442,15 +1433,14 @@ describe('mergeExcludeTools', () => { it('should not modify the original settings object', async () => { const settings: Settings = { tools: { exclude: ['tool1'] } }; - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext', - config: { - name: 'ext1', - version: '1.0.0', - excludeTools: ['tool2'], - }, + name: 'ext1', + version: '1.0.0', + excludeTools: ['tool2'], contextFiles: [], + isActive: true, }, ]; const originalSettings = JSON.parse(JSON.stringify(settings)); @@ -1486,7 +1476,7 @@ describe('Approval mode tool exclusion logic', () => { process.argv = ['node', 'script.js', '-p', 'test']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, @@ -1516,7 +1506,7 @@ describe('Approval mode tool exclusion logic', () => { ]; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, @@ -1546,7 +1536,7 @@ describe('Approval mode tool exclusion logic', () => { ]; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, @@ -1576,7 +1566,7 @@ describe('Approval mode tool exclusion logic', () => { ]; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, @@ -1599,7 +1589,7 @@ describe('Approval mode tool exclusion logic', () => { process.argv = ['node', 'script.js', '--yolo', '-p', 'test']; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, @@ -1633,7 +1623,7 @@ describe('Approval mode tool exclusion logic', () => { process.argv = testCase.args; const argv = await parseArguments({} as Settings); const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, @@ -1664,7 +1654,7 @@ describe('Approval mode tool exclusion logic', () => { ]; const argv = await parseArguments({} as Settings); const settings: Settings = { tools: { exclude: ['custom_tool'] } }; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; const config = await loadCliConfig( settings, @@ -1694,7 +1684,7 @@ describe('Approval mode tool exclusion logic', () => { }; const settings: Settings = {}; - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; await expect( loadCliConfig( settings, @@ -1976,16 +1966,20 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { }); describe('loadCliConfig extensions', () => { - const mockExtensions: Extension[] = [ + const mockExtensions: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { name: 'ext1', version: '1.0.0' }, + name: 'ext1', + version: '1.0.0', contextFiles: ['/path/to/ext1.md'], + isActive: true, }, { path: '/path/to/ext2', - config: { name: 'ext2', version: '1.0.0' }, + name: 'ext2', + version: '1.0.0', contextFiles: ['/path/to/ext2.md'], + isActive: true, }, ]; diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index bda3b8a5b2..5b3a4a6ae7 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -15,6 +15,7 @@ import type { FileFilteringOptions, MCPServerConfig, OutputFormat, + GeminiCLIExtension, } from '@google/gemini-cli-core'; import { extensionsCommand } from '../commands/extensions.js'; import { @@ -37,7 +38,6 @@ import { } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; -import type { Extension } from './extension.js'; import { annotateActiveExtensions } from './extension.js'; import { getCliVersion } from '../utils/version.js'; import { loadSandboxConfig } from './sandboxConfig.js'; @@ -472,7 +472,7 @@ export function isDebugMode(argv: CliArgs): boolean { export async function loadCliConfig( settings: Settings, - extensions: Extension[], + extensions: GeminiCLIExtension[], extensionEnablementManager: ExtensionEnablementManager, sessionId: string, argv: CliArgs, @@ -787,30 +787,28 @@ function allowedMcpServers( return mcpServers; } -function mergeMcpServers(settings: Settings, extensions: Extension[]) { +function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) { const mcpServers = { ...(settings.mcpServers || {}) }; for (const extension of extensions) { - Object.entries(extension.config.mcpServers || {}).forEach( - ([key, server]) => { - if (mcpServers[key]) { - logger.warn( - `Skipping extension MCP config for server with key "${key}" as it already exists.`, - ); - return; - } - mcpServers[key] = { - ...server, - extensionName: extension.config.name, - }; - }, - ); + Object.entries(extension.mcpServers || {}).forEach(([key, server]) => { + if (mcpServers[key]) { + logger.warn( + `Skipping extension MCP config for server with key "${key}" as it already exists.`, + ); + return; + } + mcpServers[key] = { + ...server, + extensionName: extension.name, + }; + }); } return mcpServers; } function mergeExcludeTools( settings: Settings, - extensions: Extension[], + extensions: GeminiCLIExtension[], extraExcludes?: string[] | undefined, ): string[] { const allExcludeTools = new Set([ @@ -818,7 +816,7 @@ function mergeExcludeTools( ...(extraExcludes || []), ]); for (const extension of extensions) { - for (const tool of extension.config.excludeTools || []) { + for (const tool of extension.excludeTools || []) { allExcludeTools.add(tool); } } diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index adede30060..c4f8061a3e 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -22,7 +22,6 @@ import { performWorkspaceExtensionMigration, requestConsentNonInteractive, uninstallExtension, - type Extension, } from './extension.js'; import { GEMINI_DIR, @@ -158,7 +157,7 @@ describe('extension tests', () => { ); expect(extensions).toHaveLength(1); expect(extensions[0].path).toBe(extensionDir); - expect(extensions[0].config.name).toBe('test-extension'); + expect(extensions[0].name).toBe('test-extension'); }); it('should load context file path when GEMINI.md is present', () => { @@ -179,8 +178,8 @@ describe('extension tests', () => { ); expect(extensions).toHaveLength(2); - const ext1 = extensions.find((e) => e.config.name === 'ext1'); - const ext2 = extensions.find((e) => e.config.name === 'ext2'); + const ext1 = extensions.find((e) => e.name === 'ext1'); + const ext2 = extensions.find((e) => e.name === 'ext2'); expect(ext1?.contextFiles).toEqual([ path.join(userExtensionsDir, 'ext1', 'GEMINI.md'), ]); @@ -201,7 +200,7 @@ describe('extension tests', () => { ); expect(extensions).toHaveLength(1); - const ext1 = extensions.find((e) => e.config.name === 'ext1'); + const ext1 = extensions.find((e) => e.name === 'ext1'); expect(ext1?.contextFiles).toEqual([ path.join(userExtensionsDir, 'ext1', 'my-context-file.md'), ]); @@ -254,13 +253,12 @@ describe('extension tests', () => { new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), ); expect(extensions).toHaveLength(1); - const loadedConfig = extensions[0].config; const expectedCwd = path.join( userExtensionsDir, 'test-extension', 'server', ); - expect(loadedConfig.mcpServers?.['test-server'].cwd).toBe(expectedCwd); + expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(expectedCwd); }); it('should load a linked extension correctly', async () => { @@ -287,7 +285,7 @@ describe('extension tests', () => { expect(extensions).toHaveLength(1); const linkedExt = extensions[0]; - expect(linkedExt.config.name).toBe('my-linked-extension'); + expect(linkedExt.name).toBe('my-linked-extension'); expect(linkedExt.path).toBe(sourceExtDir); expect(linkedExt.installMetadata).toEqual({ @@ -340,10 +338,10 @@ describe('extension tests', () => { expect(extensions).toHaveLength(1); const extension = extensions[0]; - expect(extension.config.name).toBe('test-extension'); - expect(extension.config.mcpServers).toBeDefined(); + expect(extension.name).toBe('test-extension'); + expect(extension.mcpServers).toBeDefined(); - const serverConfig = extension.config.mcpServers?.['test-server']; + const serverConfig = extension.mcpServers?.['test-server']; expect(serverConfig).toBeDefined(); expect(serverConfig?.env).toBeDefined(); expect(serverConfig?.env?.API_KEY).toBe('test-api-key-123'); @@ -393,7 +391,7 @@ describe('extension tests', () => { expect(extensions).toHaveLength(1); const extension = extensions[0]; - const serverConfig = extension.config.mcpServers!['test-server']; + const serverConfig = extension.mcpServers!['test-server']; expect(serverConfig.env).toBeDefined(); expect(serverConfig.env!.MISSING_VAR).toBe('$UNDEFINED_ENV_VAR'); expect(serverConfig.env!.MISSING_VAR_BRACES).toBe('${ALSO_UNDEFINED}'); @@ -422,7 +420,7 @@ describe('extension tests', () => { ); expect(extensions).toHaveLength(1); - expect(extensions[0].config.name).toBe('good-ext'); + expect(extensions[0].name).toBe('good-ext'); expect(consoleSpy).toHaveBeenCalledOnce(); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -456,7 +454,7 @@ describe('extension tests', () => { ); expect(extensions).toHaveLength(1); - expect(extensions[0].config.name).toBe('good-ext'); + expect(extensions[0].name).toBe('good-ext'); expect(consoleSpy).toHaveBeenCalledOnce(); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -485,8 +483,7 @@ describe('extension tests', () => { new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), ); expect(extensions).toHaveLength(1); - const loadedConfig = extensions[0].config; - expect(loadedConfig.mcpServers?.['test-server'].trust).toBeUndefined(); + expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined(); }); it('should throw an error for invalid extension names', () => { @@ -513,21 +510,27 @@ describe('extension tests', () => { }); describe('annotateActiveExtensions', () => { - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { name: 'ext1', version: '1.0.0' }, + name: 'ext1', + version: '1.0.0', contextFiles: [], + isActive: true, }, { path: '/path/to/ext2', - config: { name: 'ext2', version: '1.0.0' }, + name: 'ext2', + version: '1.0.0', contextFiles: [], + isActive: true, }, { path: '/path/to/ext3', - config: { name: 'ext3', version: '1.0.0' }, + name: 'ext3', + version: '1.0.0', contextFiles: [], + isActive: true, }, ]; @@ -622,13 +625,15 @@ describe('extension tests', () => { }); it('should be true if autoUpdate is true in install metadata', () => { - const extensionsWithAutoUpdate: Extension[] = extensions.map((e) => ({ - ...e, - installMetadata: { - ...e.installMetadata!, - autoUpdate: true, - }, - })); + const extensionsWithAutoUpdate: GeminiCLIExtension[] = extensions.map( + (e) => ({ + ...e, + installMetadata: { + ...e.installMetadata!, + autoUpdate: true, + }, + }), + ); const activeExtensions = annotateActiveExtensions( extensionsWithAutoUpdate, tempHomeDir, @@ -642,31 +647,37 @@ describe('extension tests', () => { }); it('should respect the per-extension settings from install metadata', () => { - const extensionsWithAutoUpdate: Extension[] = [ + const extensionsWithAutoUpdate: GeminiCLIExtension[] = [ { path: '/path/to/ext1', - config: { name: 'ext1', version: '1.0.0' }, + name: 'ext1', + version: '1.0.0', contextFiles: [], installMetadata: { source: 'test', type: 'local', autoUpdate: true, }, + isActive: true, }, { path: '/path/to/ext2', - config: { name: 'ext2', version: '1.0.0' }, + name: 'ext2', + version: '1.0.0', contextFiles: [], installMetadata: { source: 'test', type: 'local', autoUpdate: false, }, + isActive: true, }, { path: '/path/to/ext3', - config: { name: 'ext3', version: '1.0.0' }, + name: 'ext3', + version: '1.0.0', contextFiles: [], + isActive: true, }, ]; const activeExtensions = annotateActiveExtensions( @@ -1229,7 +1240,7 @@ This extension will run the following MCP servers: name: 'ext2', version: '1.0.0', }); - const extensionsToMigrate: Extension[] = [ + const extensionsToMigrate: GeminiCLIExtension[] = [ loadExtension({ extensionDir: ext1Path, workspaceDir: tempWorkspaceDir, @@ -1273,15 +1284,17 @@ This extension will run the following MCP servers: version: '1.0.0', }); - const extensions: Extension[] = [ + const extensions: GeminiCLIExtension[] = [ loadExtension({ extensionDir: ext1Path, workspaceDir: tempWorkspaceDir, })!, { path: '/ext/path/1', - config: { name: 'ext2', version: '1.0.0' }, + name: 'ext2', + version: '1.0.0', contextFiles: [], + isActive: true, }, ]; diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 4d8b6c22bc..c68ba763b7 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -45,14 +45,14 @@ export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions'); export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json'; export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json'; -export interface Extension { - path: string; - config: ExtensionConfig; - contextFiles: string[]; - installMetadata?: ExtensionInstallMetadata | undefined; -} - -export interface ExtensionConfig { +/** + * Extension definition as written to disk in gemini-extension.json files. + * This should *not* be referenced outside of the logic for reading files. + * If information is required for manipulating extensions (load, unload, update) + * outside of the loading process that data needs to be stored on the + * GeminiCLIExtension class defined in Core. + */ +interface ExtensionConfig { name: string; version: string; mcpServers?: Record; @@ -96,7 +96,9 @@ export class ExtensionStorage { } } -export function getWorkspaceExtensions(workspaceDir: string): Extension[] { +export function getWorkspaceExtensions( + workspaceDir: string, +): GeminiCLIExtension[] { // If the workspace dir is the user extensions dir, there are no workspace extensions. if (path.resolve(workspaceDir) === path.resolve(os.homedir())) { return []; @@ -112,7 +114,7 @@ export async function copyExtension( } export async function performWorkspaceExtensionMigration( - extensions: Extension[], + extensions: GeminiCLIExtension[], requestConsent: (consent: string) => Promise, ): Promise { const failedInstallNames: string[] = []; @@ -125,7 +127,7 @@ export async function performWorkspaceExtensionMigration( }; await installExtension(installMetadata, requestConsent); } catch (_) { - failedInstallNames.push(extension.config.name); + failedInstallNames.push(extension.name); } } return failedInstallNames; @@ -148,7 +150,7 @@ function getTelemetryConfig(cwd: string) { export function loadExtensions( extensionEnablementManager: ExtensionEnablementManager, workspaceDir: string = process.cwd(), -): Extension[] { +): GeminiCLIExtension[] { const settings = loadSettings(workspaceDir).merged; const allExtensions = [...loadUserExtensions()]; @@ -160,41 +162,41 @@ export function loadExtensions( allExtensions.push(...getWorkspaceExtensions(workspaceDir)); } - const uniqueExtensions = new Map(); + const uniqueExtensions = new Map(); for (const extension of allExtensions) { if ( - !uniqueExtensions.has(extension.config.name) && - extensionEnablementManager.isEnabled(extension.config.name, workspaceDir) + !uniqueExtensions.has(extension.name) && + extensionEnablementManager.isEnabled(extension.name, workspaceDir) ) { - uniqueExtensions.set(extension.config.name, extension); + uniqueExtensions.set(extension.name, extension); } } return Array.from(uniqueExtensions.values()); } -export function loadUserExtensions(): Extension[] { +export function loadUserExtensions(): GeminiCLIExtension[] { const userExtensions = loadExtensionsFromDir(os.homedir()); - const uniqueExtensions = new Map(); + const uniqueExtensions = new Map(); for (const extension of userExtensions) { - if (!uniqueExtensions.has(extension.config.name)) { - uniqueExtensions.set(extension.config.name, extension); + if (!uniqueExtensions.has(extension.name)) { + uniqueExtensions.set(extension.name, extension); } } return Array.from(uniqueExtensions.values()); } -export function loadExtensionsFromDir(dir: string): Extension[] { +export function loadExtensionsFromDir(dir: string): GeminiCLIExtension[] { const storage = new Storage(dir); const extensionsDir = storage.getExtensionsDir(); if (!fs.existsSync(extensionsDir)) { return []; } - const extensions: Extension[] = []; + const extensions: GeminiCLIExtension[] = []; for (const subdir of fs.readdirSync(extensionsDir)) { const extensionDir = path.join(extensionsDir, subdir); @@ -206,7 +208,9 @@ export function loadExtensionsFromDir(dir: string): Extension[] { return extensions; } -export function loadExtension(context: LoadExtensionContext): Extension | null { +export function loadExtension( + context: LoadExtensionContext, +): GeminiCLIExtension | null { const { extensionDir, workspaceDir } = context; if (!fs.statSync(extensionDir).isDirectory()) { return null; @@ -243,10 +247,14 @@ export function loadExtension(context: LoadExtensionContext): Extension | null { .filter((contextFilePath) => fs.existsSync(contextFilePath)); return { + name: config.name, + version: config.version, path: effectiveExtensionPath, - config, contextFiles, installMetadata, + mcpServers: config.mcpServers, + excludeTools: config.excludeTools, + isActive: true, // Barring any other signals extensions should be considered Active. }; } catch (e) { console.error( @@ -261,7 +269,7 @@ export function loadExtension(context: LoadExtensionContext): Extension | null { export function loadExtensionByName( name: string, workspaceDir: string = process.cwd(), -): Extension | null { +): GeminiCLIExtension | null { const userExtensionsDir = ExtensionStorage.getUserExtensionsDir(); if (!fs.existsSync(userExtensionsDir)) { return null; @@ -273,10 +281,7 @@ export function loadExtensionByName( continue; } const extension = loadExtension({ extensionDir, workspaceDir }); - if ( - extension && - extension.config.name.toLowerCase() === name.toLowerCase() - ) { + if (extension && extension.name.toLowerCase() === name.toLowerCase()) { return extension; } } @@ -320,17 +325,14 @@ function getContextFileNames(config: ExtensionConfig): string[] { * @param workspaceDir The current workspace directory. */ export function annotateActiveExtensions( - extensions: Extension[], + extensions: GeminiCLIExtension[], workspaceDir: string, manager: ExtensionEnablementManager, ): GeminiCLIExtension[] { 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, + ...extension, + isActive: manager.isEnabled(extension.name, workspaceDir), })); } @@ -489,7 +491,7 @@ export async function installExtension( const installedExtensions = loadUserExtensions(); if ( installedExtensions.some( - (installed) => installed.config.name === newExtensionName, + (installed) => installed.name === newExtensionName, ) ) { throw new Error( @@ -672,11 +674,10 @@ export async function uninstallExtension( const installedExtensions = loadUserExtensions(); const extensionName = installedExtensions.find( (installed) => - installed.config.name.toLowerCase() === - extensionIdentifier.toLowerCase() || + installed.name.toLowerCase() === extensionIdentifier.toLowerCase() || installed.installMetadata?.source.toLowerCase() === extensionIdentifier.toLowerCase(), - )?.config.name; + )?.name; if (!extensionName) { throw new Error(`Extension not found.`); } @@ -698,20 +699,17 @@ export async function uninstallExtension( } export function toOutputString( - extension: Extension, + extension: GeminiCLIExtension, workspaceDir: string, ): string { const manager = new ExtensionEnablementManager( ExtensionStorage.getUserExtensionsDir(), ); - const userEnabled = manager.isEnabled(extension.config.name, os.homedir()); - const workspaceEnabled = manager.isEnabled( - extension.config.name, - workspaceDir, - ); + const userEnabled = manager.isEnabled(extension.name, os.homedir()); + const workspaceEnabled = manager.isEnabled(extension.name, workspaceDir); const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗'); - let output = `${status} ${extension.config.name} (${extension.config.version})`; + let output = `${status} ${extension.name} (${extension.version})`; output += `\n Path: ${extension.path}`; if (extension.installMetadata) { output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`; @@ -730,15 +728,15 @@ export function toOutputString( output += `\n ${contextFile}`; }); } - if (extension.config.mcpServers) { + if (extension.mcpServers) { output += `\n MCP servers:`; - Object.keys(extension.config.mcpServers).forEach((key) => { + Object.keys(extension.mcpServers).forEach((key) => { output += `\n ${key}`; }); } - if (extension.config.excludeTools) { + if (extension.excludeTools) { output += `\n Excluded tools:`; - extension.config.excludeTools.forEach((tool) => { + extension.excludeTools.forEach((tool) => { output += `\n ${tool}`; }); } diff --git a/packages/cli/src/config/extensions/extensionEnablement.test.ts b/packages/cli/src/config/extensions/extensionEnablement.test.ts index b87fc2d814..8b2adb4ae6 100644 --- a/packages/cli/src/config/extensions/extensionEnablement.test.ts +++ b/packages/cli/src/config/extensions/extensionEnablement.test.ts @@ -9,7 +9,7 @@ import fs from 'node:fs'; import os from 'node:os'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ExtensionEnablementManager, Override } from './extensionEnablement.js'; -import type { Extension } from '../extension.js'; +import type { GeminiCLIExtension } from '@google/gemini-cli-core'; // Helper to create a temporary directory for testing function createTestDir() { @@ -286,9 +286,9 @@ describe('ExtensionEnablementManager', () => { 'ext-two', ]); const extensions = [ - { config: { name: 'ext-one' } }, - { config: { name: 'ext-two' } }, - ] as Extension[]; + { name: 'ext-one' }, + { name: 'ext-two' }, + ] as GeminiCLIExtension[]; manager.validateExtensionOverrides(extensions); expect(consoleErrorSpy).not.toHaveBeenCalled(); }); @@ -300,9 +300,9 @@ describe('ExtensionEnablementManager', () => { 'ext-another-invalid', ]); const extensions = [ - { config: { name: 'ext-one' } }, - { config: { name: 'ext-two' } }, - ] as Extension[]; + { name: 'ext-one' }, + { name: 'ext-two' }, + ] as GeminiCLIExtension[]; manager.validateExtensionOverrides(extensions); expect(consoleErrorSpy).toHaveBeenCalledTimes(2); expect(consoleErrorSpy).toHaveBeenCalledWith( diff --git a/packages/cli/src/config/extensions/extensionEnablement.ts b/packages/cli/src/config/extensions/extensionEnablement.ts index 737bc08d4d..6dbdf2491c 100644 --- a/packages/cli/src/config/extensions/extensionEnablement.ts +++ b/packages/cli/src/config/extensions/extensionEnablement.ts @@ -6,7 +6,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { type Extension } from '../extension.js'; +import type { GeminiCLIExtension } from '@google/gemini-cli-core'; export interface ExtensionEnablementConfig { overrides: string[]; @@ -119,13 +119,11 @@ export class ExtensionEnablementManager { enabledExtensionNames?.map((name) => name.toLowerCase()) ?? []; } - validateExtensionOverrides(extensions: Extension[]) { + validateExtensionOverrides(extensions: GeminiCLIExtension[]) { for (const name of this.enabledExtensionNamesOverride) { if (name === 'none') continue; if ( - !extensions.some( - (ext) => ext.config.name.toLowerCase() === name.toLowerCase(), - ) + !extensions.some((ext) => ext.name.toLowerCase() === name.toLowerCase()) ) { console.error(`Extension not found: ${name}`); } diff --git a/packages/cli/src/config/extensions/github.test.ts b/packages/cli/src/config/extensions/github.test.ts index e519b7e6df..9bf2fab07d 100644 --- a/packages/cli/src/config/extensions/github.test.ts +++ b/packages/cli/src/config/extensions/github.test.ts @@ -137,6 +137,7 @@ describe('git extension helpers', () => { type: 'link', source: '', }, + contextFiles: [], }; const result = await checkForExtensionUpdate(extension); expect(result).toBe(ExtensionUpdateState.NOT_UPDATABLE); @@ -152,6 +153,7 @@ describe('git extension helpers', () => { type: 'git', source: '', }, + contextFiles: [], }; mockGit.getRemotes.mockResolvedValue([]); const result = await checkForExtensionUpdate(extension); @@ -168,6 +170,7 @@ describe('git extension helpers', () => { type: 'git', source: 'my/ext', }, + contextFiles: [], }; mockGit.getRemotes.mockResolvedValue([ { name: 'origin', refs: { fetch: 'http://my-repo.com' } }, @@ -189,6 +192,7 @@ describe('git extension helpers', () => { type: 'git', source: 'my/ext', }, + contextFiles: [], }; mockGit.getRemotes.mockResolvedValue([ { name: 'origin', refs: { fetch: 'http://my-repo.com' } }, @@ -210,6 +214,7 @@ describe('git extension helpers', () => { type: 'git', source: 'my/ext', }, + contextFiles: [], }; mockGit.getRemotes.mockRejectedValue(new Error('git error')); diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 3c43c344e1..f20ca2f2f5 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -134,7 +134,7 @@ export async function checkForExtensionUpdate( ); return ExtensionUpdateState.ERROR; } - if (newExtension.config.version !== extension.version) { + if (newExtension.version !== extension.version) { return ExtensionUpdateState.UPDATE_AVAILABLE; } return ExtensionUpdateState.UP_TO_DATE; diff --git a/packages/cli/src/config/extensions/update.ts b/packages/cli/src/config/extensions/update.ts index cb1fa70e03..4314c55533 100644 --- a/packages/cli/src/config/extensions/update.ts +++ b/packages/cli/src/config/extensions/update.ts @@ -90,7 +90,7 @@ export async function updateExtension( }); throw new Error('Updated extension not found after installation.'); } - const updatedVersion = updatedExtension.config.version; + const updatedVersion = updatedExtension.version; dispatchExtensionStateUpdate({ type: 'SET_STATE', payload: { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 40813d7364..f18ed15c96 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -357,7 +357,7 @@ export async function main() { if (config.getListExtensions()) { console.log('Installed extensions:'); for (const extension of extensions) { - console.log(`- ${extension.config.name}`); + console.log(`- ${extension.name}`); } process.exit(0); } diff --git a/packages/cli/src/ui/components/WorkspaceMigrationDialog.tsx b/packages/cli/src/ui/components/WorkspaceMigrationDialog.tsx index 35977e7268..55ddb606f6 100644 --- a/packages/cli/src/ui/components/WorkspaceMigrationDialog.tsx +++ b/packages/cli/src/ui/components/WorkspaceMigrationDialog.tsx @@ -5,16 +5,14 @@ */ import { Box, Text, useInput } from 'ink'; -import { - type Extension, - performWorkspaceExtensionMigration, -} from '../../config/extension.js'; +import type { GeminiCLIExtension } from '@google/gemini-cli-core'; +import { performWorkspaceExtensionMigration } from '../../config/extension.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { theme } from '../semantic-colors.js'; import { useState } from 'react'; export function WorkspaceMigrationDialog(props: { - workspaceExtensions: Extension[]; + workspaceExtensions: GeminiCLIExtension[]; onOpen: () => void; onClose: () => void; }) { @@ -92,7 +90,7 @@ export function WorkspaceMigrationDialog(props: { {workspaceExtensions.map((extension) => ( - - {extension.config.name} + - {extension.name} ))} diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts index c7a1fd0cfd..bfdbcea54c 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts @@ -70,6 +70,7 @@ describe('useExtensionUpdates', () => { source: 'https://some/repo', autoUpdate: false, }, + contextFiles: [], }, ]; const addItem = vi.fn(); @@ -262,6 +263,7 @@ describe('useExtensionUpdates', () => { source: 'https://some/repo1', autoUpdate: false, }, + contextFiles: [], }, { name: 'test-extension-2', @@ -274,6 +276,7 @@ describe('useExtensionUpdates', () => { source: 'https://some/repo2', autoUpdate: false, }, + contextFiles: [], }, ]; const addItem = vi.fn(); diff --git a/packages/cli/src/ui/hooks/useWorkspaceMigration.ts b/packages/cli/src/ui/hooks/useWorkspaceMigration.ts index 8f9a3addaf..e4312d9ea2 100644 --- a/packages/cli/src/ui/hooks/useWorkspaceMigration.ts +++ b/packages/cli/src/ui/hooks/useWorkspaceMigration.ts @@ -5,19 +5,17 @@ */ import { useState, useEffect } from 'react'; -import { - type Extension, - getWorkspaceExtensions, -} from '../../config/extension.js'; +import type { GeminiCLIExtension } from '@google/gemini-cli-core'; +import { getWorkspaceExtensions } from '../../config/extension.js'; import { type LoadedSettings, SettingScope } from '../../config/settings.js'; import process from 'node:process'; export function useWorkspaceMigration(settings: LoadedSettings) { const [showWorkspaceMigrationDialog, setShowWorkspaceMigrationDialog] = useState(false); - const [workspaceExtensions, setWorkspaceExtensions] = useState( - [], - ); + const [workspaceExtensions, setWorkspaceExtensions] = useState< + GeminiCLIExtension[] + >([]); useEffect(() => { // Default to true if not set. diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index bf6a50f641..34acec161c 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -11,6 +11,7 @@ import type { GeminiChat, ToolResult, ToolCallConfirmationDetails, + GeminiCLIExtension, } from '@google/gemini-cli-core'; import { AuthType, @@ -40,7 +41,7 @@ import * as path from 'node:path'; import { z } from 'zod'; import { randomUUID } from 'node:crypto'; -import { ExtensionStorage, type Extension } from '../config/extension.js'; +import { ExtensionStorage } from '../config/extension.js'; import type { CliArgs } from '../config/config.js'; import { loadCliConfig } from '../config/config.js'; import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js'; @@ -61,7 +62,7 @@ export function resolveModel(model: string, isInFallbackMode: boolean): string { export async function runZedIntegration( config: Config, settings: LoadedSettings, - extensions: Extension[], + extensions: GeminiCLIExtension[], argv: CliArgs, ) { const stdout = Writable.toWeb(process.stdout) as WritableStream; @@ -88,7 +89,7 @@ class GeminiAgent { constructor( private config: Config, private settings: LoadedSettings, - private extensions: Extension[], + private extensions: GeminiCLIExtension[], private argv: CliArgs, private client: acp.Client, ) {} diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 1765e329a5..bbc26a0e62 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -116,12 +116,21 @@ export interface OutputSettings { format?: OutputFormat; } +/** + * All information required in CLI to handle an extension. Defined in Core so + * that the collection of loaded, active, and inactive extensions can be passed + * around on the config object though Core does not use this information + * directly. + */ export interface GeminiCLIExtension { name: string; version: string; isActive: boolean; path: string; installMetadata?: ExtensionInstallMetadata; + mcpServers?: Record; + contextFiles: string[]; + excludeTools?: string[]; } export interface ExtensionInstallMetadata {