mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Fix default extension context filename and update docs (#1024)
This commit is contained in:
committed by
GitHub
parent
1fa41af918
commit
54f0d9d0e5
@@ -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',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
extensions.push(extensionConfig);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to load extension config from ${extensionPath}:`,
|
||||
e,
|
||||
);
|
||||
const extension = loadExtension(extensionDir);
|
||||
if (extension != null) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function loadExtension(extensionDir: string): Extension | null {
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
console.error(
|
||||
`Warning: unexpected file ${extensionDir} in extensions directory.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
const configContent = fs.readFileSync(configFilePath, 'utf-8');
|
||||
const config = JSON.parse(configContent) as ExtensionConfig;
|
||||
if (!config.name || !config.version) {
|
||||
console.error(
|
||||
`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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user