diff options
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/config/config.test.ts | 37 | ||||
| -rw-r--r-- | packages/cli/src/config/config.ts | 30 | ||||
| -rw-r--r-- | packages/cli/src/config/extension.test.ts | 22 | ||||
| -rw-r--r-- | packages/cli/src/config/extension.ts | 111 | ||||
| -rw-r--r-- | packages/cli/src/gemini.tsx | 4 |
5 files changed, 113 insertions, 91 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 7a77c81e..60c693bb 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -4,12 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -// packages/cli/src/config/config.test.ts - import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as os from 'os'; import { loadCliConfig } from './config.js'; import { Settings } from './settings.js'; +import { Extension } from './extension.js'; import * as ServerConfig from '@gemini-cli/core'; const MOCK_HOME_DIR = '/mock/home/user'; @@ -210,27 +209,41 @@ 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 = [ + const extensions: Extension[] = [ { - name: 'ext1', - version: '1.0.0', - contextFileName: '/path/to/ext1/gemini.md', + config: { + name: 'ext1', + version: '1.0.0', + }, + contextFiles: ['/path/to/ext1/GEMINI.md'], }, { - name: 'ext2', - version: '1.0.0', + config: { + name: 'ext2', + version: '1.0.0', + }, + contextFiles: [], }, { - name: 'ext3', - version: '1.0.0', - contextFileName: '/path/to/ext3/gemini.md', + config: { + name: 'ext3', + version: '1.0.0', + }, + contextFiles: [ + '/path/to/ext3/context1.md', + '/path/to/ext3/context2.md', + ], }, ]; await loadCliConfig(settings, extensions, [], 'session-id'); expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( expect.any(String), false, - ['/path/to/ext1/gemini.md', '/path/to/ext3/gemini.md'], + [ + '/path/to/ext1/GEMINI.md', + '/path/to/ext3/context1.md', + '/path/to/ext3/context2.md', + ], ); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 9215afdd..b9a995ed 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -20,7 +20,7 @@ import { } from '@gemini-cli/core'; import { Settings } from './settings.js'; import { getEffectiveModel } from '../utils/modelCheck.js'; -import { ExtensionConfig } from './extension.js'; +import { Extension } from './extension.js'; import * as dotenv from 'dotenv'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -132,7 +132,7 @@ export async function loadHierarchicalGeminiMemory( export async function loadCliConfig( settings: Settings, - extensions: ExtensionConfig[], + extensions: Extension[], geminiIgnorePatterns: string[], sessionId: string, ): Promise<Config> { @@ -152,9 +152,7 @@ export async function loadCliConfig( setServerGeminiMdFilename(getCurrentGeminiMdFilename()); } - const extensionContextFilePaths = extensions - .map((e) => e.contextFileName) - .filter((p): p is string => !!p); + const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles); // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( @@ -206,18 +204,20 @@ export async function loadCliConfig( }); } -function mergeMcpServers(settings: Settings, extensions: ExtensionConfig[]) { +function mergeMcpServers(settings: Settings, extensions: Extension[]) { const mcpServers = settings.mcpServers || {}; for (const extension of extensions) { - 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; - }); + 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; + }, + ); } return mcpServers; } diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 6e0d1658..754e9dd0 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -41,7 +41,7 @@ describe('loadExtensions', () => { fs.rmSync(tempHomeDir, { recursive: true, force: true }); }); - it('should load context file path when gemini.md is present', () => { + it('should load context file path when GEMINI.md is present', () => { const workspaceExtensionsDir = path.join( tempWorkspaceDir, EXTENSIONS_DIRECTORY_NAME, @@ -53,12 +53,12 @@ describe('loadExtensions', () => { const extensions = loadExtensions(tempWorkspaceDir); expect(extensions).toHaveLength(2); - const ext1 = extensions.find((e) => e.name === 'ext1'); - const ext2 = extensions.find((e) => e.name === 'ext2'); - expect(ext1?.contextFileName).toBe( - path.join(workspaceExtensionsDir, 'ext1', 'gemini.md'), - ); - expect(ext2?.contextFileName).toBeUndefined(); + const ext1 = extensions.find((e) => e.config.name === 'ext1'); + const ext2 = extensions.find((e) => e.config.name === 'ext2'); + expect(ext1?.contextFiles).toEqual([ + path.join(workspaceExtensionsDir, 'ext1', 'GEMINI.md'), + ]); + expect(ext2?.contextFiles).toEqual([]); }); it('should load context file path from the extension config', () => { @@ -78,10 +78,10 @@ describe('loadExtensions', () => { const extensions = loadExtensions(tempWorkspaceDir); expect(extensions).toHaveLength(1); - const ext1 = extensions.find((e) => e.name === 'ext1'); - expect(ext1?.contextFileName).toBe( + const ext1 = extensions.find((e) => e.config.name === 'ext1'); + expect(ext1?.contextFiles).toEqual([ path.join(workspaceExtensionsDir, 'ext1', 'my-context.md'), - ); + ]); }); }); @@ -100,7 +100,7 @@ function createExtension( ); if (addContextFile) { - fs.writeFileSync(path.join(extDir, 'gemini.md'), 'context'); + fs.writeFileSync(path.join(extDir, 'GEMINI.md'), 'context'); } if (contextFileName) { diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 685c0b74..84863e32 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -12,6 +12,11 @@ import * as os from 'os'; export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions'); export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json'; +export interface Extension { + config: ExtensionConfig; + contextFiles: string[]; +} + export interface ExtensionConfig { name: string; version: string; @@ -19,88 +24,92 @@ export interface ExtensionConfig { contextFileName?: string | string[]; } -export function loadExtensions(workspaceDir: string): ExtensionConfig[] { +export function loadExtensions(workspaceDir: string): Extension[] { const allExtensions = [ ...loadExtensionsFromDir(workspaceDir), ...loadExtensionsFromDir(os.homedir()), ]; - const uniqueExtensions: ExtensionConfig[] = []; + const uniqueExtensions: Extension[] = []; const seenNames = new Set<string>(); for (const extension of allExtensions) { - if (!seenNames.has(extension.name)) { + if (!seenNames.has(extension.config.name)) { console.log( - `Loading extension: ${extension.name} (version: ${extension.version})`, + `Loading extension: ${extension.config.name} (version: ${extension.config.version})`, ); uniqueExtensions.push(extension); - seenNames.add(extension.name); + seenNames.add(extension.config.name); } } return uniqueExtensions; } -function loadExtensionsFromDir(dir: string): ExtensionConfig[] { +function loadExtensionsFromDir(dir: string): Extension[] { const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME); if (!fs.existsSync(extensionsDir)) { return []; } - const extensions: ExtensionConfig[] = []; + const extensions: Extension[] = []; for (const subdir of fs.readdirSync(extensionsDir)) { const extensionDir = path.join(extensionsDir, subdir); - if (!fs.statSync(extensionDir).isDirectory()) { - console.error( - `Warning: unexpected file ${extensionDir} in extensions directory.`, - ); - continue; - } - - const extensionPath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME); - if (!fs.existsSync(extensionPath)) { - console.error( - `Warning: extension directory ${extensionDir} does not contain a config file ${extensionPath}.`, - ); - continue; + const extension = loadExtension(extensionDir); + if (extension != null) { + extensions.push(extension); } + } + return extensions; +} - try { - const fileContent = fs.readFileSync(extensionPath, 'utf-8'); - const extensionConfig = JSON.parse(fileContent) as ExtensionConfig; - if (!extensionConfig.name || !extensionConfig.version) { - console.error( - `Invalid extension config in ${extensionPath}: missing name or version.`, - ); - continue; - } +function loadExtension(extensionDir: string): Extension | null { + if (!fs.statSync(extensionDir).isDirectory()) { + console.error( + `Warning: unexpected file ${extensionDir} in extensions directory.`, + ); + return null; + } - if (extensionConfig.contextFileName) { - const contextFileNames = Array.isArray(extensionConfig.contextFileName) - ? extensionConfig.contextFileName - : [extensionConfig.contextFileName]; - const resolvedPaths = contextFileNames - .map((fileName) => path.join(extensionDir, fileName)) - .filter((filePath) => fs.existsSync(filePath)); - if (resolvedPaths.length > 0) { - extensionConfig.contextFileName = - resolvedPaths.length === 1 ? resolvedPaths[0] : resolvedPaths; - } - } else { - const contextFilePath = path.join(extensionDir, 'gemini.md'); - if (fs.existsSync(contextFilePath)) { - extensionConfig.contextFileName = contextFilePath; - } - } + const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME); + if (!fs.existsSync(configFilePath)) { + console.error( + `Warning: extension directory ${extensionDir} does not contain a config file ${configFilePath}.`, + ); + return null; + } - extensions.push(extensionConfig); - } catch (e) { + try { + const configContent = fs.readFileSync(configFilePath, 'utf-8'); + const config = JSON.parse(configContent) as ExtensionConfig; + if (!config.name || !config.version) { console.error( - `Failed to load extension config from ${extensionPath}:`, - e, + `Invalid extension config in ${configFilePath}: missing name or version.`, ); + return null; } + + const contextFiles = getContextFileNames(config) + .map((contextFileName) => path.join(extensionDir, contextFileName)) + .filter((contextFilePath) => fs.existsSync(contextFilePath)); + + return { + config, + contextFiles, + }; + } catch (e) { + console.error( + `Warning: error parsing extension config in ${configFilePath}: ${e}`, + ); + return null; } +} - return extensions; +function getContextFileNames(config: ExtensionConfig): string[] { + if (!config.contextFileName) { + return ['GEMINI.md', 'gemini.md', 'Gemini.md']; + } else if (!Array.isArray(config.contextFileName)) { + return [config.contextFileName]; + } + return config.contextFileName; } diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 34ec3bd0..35c94214 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -16,7 +16,7 @@ import { themeManager } from './ui/themes/theme-manager.js'; import { getStartupWarnings } from './utils/startupWarnings.js'; import { runNonInteractive } from './nonInteractiveCli.js'; import { loadGeminiIgnorePatterns } from './utils/loadIgnorePatterns.js'; -import { loadExtensions, ExtensionConfig } from './config/extension.js'; +import { loadExtensions, Extension } from './config/extension.js'; import { cleanupCheckpoints } from './utils/cleanup.js'; import { ApprovalMode, @@ -164,7 +164,7 @@ process.on('unhandledRejection', (reason, _promise) => { async function loadNonInteractiveConfig( config: Config, - extensions: ExtensionConfig[], + extensions: Extension[], settings: LoadedSettings, ) { if (config.getApprovalMode() === ApprovalMode.YOLO) { |
