mirror of
https://github.com/google-gemini/gemini-cli
synced 2026-04-21 13:37:17 +00:00
Pass whole extensions rather than just context files (#10910)
Co-authored-by: Jake Macdonald <jakemac@google.com>
This commit is contained in:
parent
995ae717cc
commit
cc7e1472f9
35 changed files with 487 additions and 1193 deletions
|
|
@ -73,13 +73,12 @@ export async function loadConfig(
|
|||
};
|
||||
|
||||
const fileService = new FileDiscoveryService(workspaceDir);
|
||||
const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles);
|
||||
const { memoryContent, fileCount } = await loadServerHierarchicalMemory(
|
||||
workspaceDir,
|
||||
[workspaceDir],
|
||||
false,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
extensions,
|
||||
settings.folderTrust === true,
|
||||
);
|
||||
configParams.userMemory = memoryContent;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { type CommandModule } from 'yargs';
|
|||
import { disableExtension } from '../../config/extension.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
interface DisableArgs {
|
||||
|
|
@ -16,11 +17,20 @@ interface DisableArgs {
|
|||
}
|
||||
|
||||
export function handleDisable(args: DisableArgs) {
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
try {
|
||||
if (args.scope?.toLowerCase() === 'workspace') {
|
||||
disableExtension(args.name, SettingScope.Workspace);
|
||||
disableExtension(
|
||||
args.name,
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
} else {
|
||||
disableExtension(args.name, SettingScope.User);
|
||||
disableExtension(
|
||||
args.name,
|
||||
SettingScope.User,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
}
|
||||
debugLogger.log(
|
||||
`Extension "${args.name}" successfully disabled for scope "${args.scope}".`,
|
||||
|
|
@ -41,7 +51,7 @@ export const disableCommand: CommandModule = {
|
|||
type: 'string',
|
||||
})
|
||||
.option('scope', {
|
||||
describe: 'The scope to disable the extenison in.',
|
||||
describe: 'The scope to disable the extension in.',
|
||||
type: 'string',
|
||||
default: SettingScope.User,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { type CommandModule } from 'yargs';
|
|||
import { FatalConfigError, getErrorMessage } from '@google/gemini-cli-core';
|
||||
import { enableExtension } from '../../config/extension.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
|
||||
interface EnableArgs {
|
||||
name: string;
|
||||
|
|
@ -15,11 +16,16 @@ interface EnableArgs {
|
|||
}
|
||||
|
||||
export function handleEnable(args: EnableArgs) {
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
try {
|
||||
if (args.scope?.toLowerCase() === 'workspace') {
|
||||
enableExtension(args.name, SettingScope.Workspace);
|
||||
enableExtension(
|
||||
args.name,
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
} else {
|
||||
enableExtension(args.name, SettingScope.User);
|
||||
enableExtension(args.name, SettingScope.User, extensionEnablementManager);
|
||||
}
|
||||
if (args.scope) {
|
||||
console.log(
|
||||
|
|
@ -46,7 +52,7 @@ export const enableCommand: CommandModule = {
|
|||
})
|
||||
.option('scope', {
|
||||
describe:
|
||||
'The scope to enable the extenison in. If not set, will be enabled in all scopes.',
|
||||
'The scope to enable the extension in. If not set, will be enabled in all scopes.',
|
||||
type: 'string',
|
||||
})
|
||||
.check((argv) => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
loadExtensions,
|
||||
annotateActiveExtensions,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extension.js';
|
||||
import {
|
||||
|
|
@ -37,12 +36,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
|||
// ones.
|
||||
args.name ? [args.name] : [],
|
||||
);
|
||||
const allExtensions = loadExtensions(extensionEnablementManager);
|
||||
const extensions = annotateActiveExtensions(
|
||||
allExtensions,
|
||||
workingDir,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
const extensions = loadExtensions(extensionEnablementManager);
|
||||
if (args.name) {
|
||||
try {
|
||||
const extension = extensions.find(
|
||||
|
|
@ -58,7 +52,10 @@ export async function handleUpdate(args: UpdateArgs) {
|
|||
);
|
||||
return;
|
||||
}
|
||||
const updateState = await checkForExtensionUpdate(extension);
|
||||
const updateState = await checkForExtensionUpdate(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) {
|
||||
debugLogger.log(`Extension "${args.name}" is already up to date.`);
|
||||
return;
|
||||
|
|
@ -66,6 +63,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
|||
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
|
||||
const updatedExtensionInfo = (await updateExtension(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
workingDir,
|
||||
requestConsentNonInteractive,
|
||||
updateState,
|
||||
|
|
@ -90,6 +88,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
|||
const extensionState = new Map();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensions,
|
||||
extensionEnablementManager,
|
||||
(action) => {
|
||||
if (action.type === 'SET_STATE') {
|
||||
extensionState.set(action.payload.name, {
|
||||
|
|
@ -104,6 +103,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
|||
requestConsentNonInteractive,
|
||||
extensions,
|
||||
extensionState,
|
||||
extensionEnablementManager,
|
||||
() => {},
|
||||
);
|
||||
updateInfos = updateInfos.filter(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ async function getMcpServersFromConfig(): Promise<
|
|||
}
|
||||
mcpServers[key] = {
|
||||
...server,
|
||||
extensionName: extension.name,
|
||||
extension,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ export async function listMcpServers(): Promise<void> {
|
|||
|
||||
let serverInfo =
|
||||
serverName +
|
||||
(server.extensionName ? ` (from ${server.extensionName})` : '') +
|
||||
(server.extension?.name ? ` (from ${server.extension.name})` : '') +
|
||||
': ';
|
||||
if (server.httpUrl) {
|
||||
serverInfo += `${server.httpUrl} (http)`;
|
||||
|
|
|
|||
|
|
@ -243,38 +243,6 @@ describe('Configuration Integration Tests', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Extension Context Files', () => {
|
||||
it('should have an empty array for extension context files by default', () => {
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
cwd: '/tmp',
|
||||
model: 'test-model',
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: undefined,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
};
|
||||
const config = new Config(configParams);
|
||||
expect(config.getExtensionContextFilePaths()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should correctly store and return extension context file paths', () => {
|
||||
const contextFiles = ['/path/to/file1.txt', '/path/to/file2.js'];
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: 'test-session',
|
||||
cwd: '/tmp',
|
||||
model: 'test-model',
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: undefined,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
extensionContextFilePaths: contextFiles,
|
||||
};
|
||||
const config = new Config(configParams);
|
||||
expect(config.getExtensionContextFilePaths()).toEqual(contextFiles);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Approval Mode Integration Tests', () => {
|
||||
let parseArguments: typeof import('./config.js').parseArguments;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -40,7 +40,6 @@ import {
|
|||
} from '@google/gemini-cli-core';
|
||||
import type { Settings } from './settings.js';
|
||||
|
||||
import { annotateActiveExtensions } from './extension.js';
|
||||
import { getCliVersion } from '../utils/version.js';
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
import { resolvePath } from '../utils/resolvePath.js';
|
||||
|
|
@ -48,7 +47,6 @@ import { appEvents } from '../utils/events.js';
|
|||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import { createPolicyEngineConfig } from './policy.js';
|
||||
import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
|
||||
export interface CliArgs {
|
||||
query: string | undefined;
|
||||
|
|
@ -289,7 +287,7 @@ export async function loadHierarchicalGeminiMemory(
|
|||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
settings: Settings,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
extensions: GeminiCLIExtension[],
|
||||
folderTrust: boolean,
|
||||
memoryImportFormat: 'flat' | 'tree' = 'tree',
|
||||
fileFilteringOptions?: FileFilteringOptions,
|
||||
|
|
@ -315,7 +313,7 @@ export async function loadHierarchicalGeminiMemory(
|
|||
includeDirectoriesToReadGemini,
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
extensions,
|
||||
folderTrust,
|
||||
memoryImportFormat,
|
||||
fileFilteringOptions,
|
||||
|
|
@ -364,8 +362,7 @@ export function isDebugMode(argv: CliArgs): boolean {
|
|||
|
||||
export async function loadCliConfig(
|
||||
settings: Settings,
|
||||
extensions: GeminiCLIExtension[],
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
allExtensions: GeminiCLIExtension[],
|
||||
sessionId: string,
|
||||
argv: CliArgs,
|
||||
cwd: string = process.cwd(),
|
||||
|
|
@ -379,16 +376,6 @@ export async function loadCliConfig(
|
|||
const folderTrust = settings.security?.folderTrust?.enabled ?? false;
|
||||
const trustedFolder = isWorkspaceTrusted(settings)?.isTrusted ?? true;
|
||||
|
||||
const allExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
cwd,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
|
||||
const activeExtensions = extensions.filter(
|
||||
(_, i) => allExtensions[i].isActive,
|
||||
);
|
||||
|
||||
// Set the context filename in the server's memoryTool module BEFORE loading memory
|
||||
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
||||
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
|
||||
|
|
@ -400,10 +387,6 @@ export async function loadCliConfig(
|
|||
setServerGeminiMdFilename(getCurrentGeminiMdFilename());
|
||||
}
|
||||
|
||||
const extensionContextFilePaths = activeExtensions.flatMap(
|
||||
(e) => e.contextFiles,
|
||||
);
|
||||
|
||||
const fileService = new FileDiscoveryService(cwd);
|
||||
|
||||
const fileFiltering = {
|
||||
|
|
@ -425,13 +408,13 @@ export async function loadCliConfig(
|
|||
debugMode,
|
||||
fileService,
|
||||
settings,
|
||||
extensionContextFilePaths,
|
||||
allExtensions,
|
||||
trustedFolder,
|
||||
memoryImportFormat,
|
||||
fileFiltering,
|
||||
);
|
||||
|
||||
let mcpServers = mergeMcpServers(settings, activeExtensions);
|
||||
let mcpServers = mergeMcpServers(settings, allExtensions);
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
|
||||
// Determine approval mode with backward compatibility
|
||||
|
|
@ -527,7 +510,7 @@ export async function loadCliConfig(
|
|||
|
||||
const excludeTools = mergeExcludeTools(
|
||||
settings,
|
||||
activeExtensions,
|
||||
allExtensions,
|
||||
extraExcludes.length > 0 ? extraExcludes : undefined,
|
||||
);
|
||||
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
|
||||
|
|
@ -618,10 +601,10 @@ export async function loadCliConfig(
|
|||
fileDiscoveryService: fileService,
|
||||
bugCommand: settings.advanced?.bugCommand,
|
||||
model: resolvedModel,
|
||||
extensionContextFilePaths,
|
||||
maxSessionTurns: settings.model?.maxSessionTurns ?? -1,
|
||||
experimentalZedIntegration: argv.experimentalAcp || false,
|
||||
listExtensions: argv.listExtensions || false,
|
||||
enabledExtensions: argv.extensions,
|
||||
extensions: allExtensions,
|
||||
blockedMcpServers,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
|
|
@ -668,7 +651,7 @@ function allowedMcpServers(
|
|||
if (!isAllowed) {
|
||||
blockedMcpServers.push({
|
||||
name: key,
|
||||
extensionName: server.extensionName || '',
|
||||
extensionName: server.extension?.name || '',
|
||||
});
|
||||
}
|
||||
return isAllowed;
|
||||
|
|
@ -678,7 +661,7 @@ function allowedMcpServers(
|
|||
blockedMcpServers.push(
|
||||
...Object.entries(mcpServers).map(([key, server]) => ({
|
||||
name: key,
|
||||
extensionName: server.extensionName || '',
|
||||
extensionName: server.extension?.name || '',
|
||||
})),
|
||||
);
|
||||
mcpServers = {};
|
||||
|
|
@ -689,6 +672,9 @@ function allowedMcpServers(
|
|||
function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) {
|
||||
const mcpServers = { ...(settings.mcpServers || {}) };
|
||||
for (const extension of extensions) {
|
||||
if (!extension.isActive) {
|
||||
continue;
|
||||
}
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
if (mcpServers[key]) {
|
||||
debugLogger.warn(
|
||||
|
|
@ -698,7 +684,7 @@ function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) {
|
|||
}
|
||||
mcpServers[key] = {
|
||||
...server,
|
||||
extensionName: extension.name,
|
||||
extension,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -715,6 +701,9 @@ function mergeExcludeTools(
|
|||
...(extraExcludes || []),
|
||||
]);
|
||||
for (const extension of extensions) {
|
||||
if (!extension.isActive) {
|
||||
continue;
|
||||
}
|
||||
for (const tool of extension.excludeTools || []) {
|
||||
allExcludeTools.add(tool);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
ExtensionStorage,
|
||||
INSTALL_METADATA_FILENAME,
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
annotateActiveExtensions,
|
||||
disableExtension,
|
||||
enableExtension,
|
||||
installOrUpdateExtension,
|
||||
|
|
@ -202,7 +201,7 @@ describe('extension tests', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should filter out disabled extensions', () => {
|
||||
it('should annotate disabled extensions', () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'disabled-extension',
|
||||
|
|
@ -213,20 +212,19 @@ describe('extension tests', () => {
|
|||
name: 'enabled-extension',
|
||||
version: '2.0.0',
|
||||
});
|
||||
const manager = new ExtensionEnablementManager();
|
||||
disableExtension(
|
||||
'disabled-extension',
|
||||
SettingScope.User,
|
||||
manager,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
const manager = new ExtensionEnablementManager();
|
||||
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');
|
||||
expect(extensions).toHaveLength(2);
|
||||
expect(extensions[0].name).toBe('disabled-extension');
|
||||
expect(extensions[0].isActive).toBe(false);
|
||||
expect(extensions[1].name).toBe('enabled-extension');
|
||||
expect(extensions[1].isActive).toBe(true);
|
||||
});
|
||||
|
||||
it('should hydrate variables', () => {
|
||||
|
|
@ -477,6 +475,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir: badExtDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
expect(extension).toBeNull();
|
||||
|
|
@ -501,6 +500,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
const expectedHash = createHash('sha256')
|
||||
|
|
@ -523,6 +523,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
const expectedHash = createHash('sha256')
|
||||
|
|
@ -545,6 +546,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
const expectedHash = createHash('sha256')
|
||||
|
|
@ -567,6 +569,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
const expectedHash = createHash('sha256')
|
||||
|
|
@ -589,6 +592,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
const expectedHash = createHash('sha256')
|
||||
|
|
@ -616,6 +620,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir: new ExtensionStorage(extensionName).getExtensionDir(),
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
const expectedHash = createHash('sha256')
|
||||
|
|
@ -634,6 +639,7 @@ describe('extension tests', () => {
|
|||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
});
|
||||
|
||||
const expectedHash = createHash('sha256')
|
||||
|
|
@ -644,182 +650,6 @@ describe('extension tests', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('annotateActiveExtensions', () => {
|
||||
const extensions: GeminiCLIExtension[] = [
|
||||
{
|
||||
path: '/path/to/ext1',
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
contextFiles: [],
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
path: '/path/to/ext2',
|
||||
name: 'ext2',
|
||||
version: '1.0.0',
|
||||
contextFiles: [],
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
path: '/path/to/ext3',
|
||||
name: 'ext3',
|
||||
version: '1.0.0',
|
||||
contextFiles: [],
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
it('should mark all extensions as active if no enabled extensions are provided', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(activeExtensions).toHaveLength(3);
|
||||
expect(activeExtensions.every((e) => e.isActive)).toBe(true);
|
||||
});
|
||||
|
||||
it('should mark only the enabled extensions as active', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(['ext1', 'ext3']),
|
||||
);
|
||||
expect(activeExtensions).toHaveLength(3);
|
||||
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
||||
true,
|
||||
);
|
||||
expect(activeExtensions.find((e) => e.name === 'ext2')?.isActive).toBe(
|
||||
false,
|
||||
);
|
||||
expect(activeExtensions.find((e) => e.name === 'ext3')?.isActive).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should mark all extensions as inactive when "none" is provided', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(['none']),
|
||||
);
|
||||
expect(activeExtensions).toHaveLength(3);
|
||||
expect(activeExtensions.every((e) => !e.isActive)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle case-insensitivity', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(['EXT1']),
|
||||
);
|
||||
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should log an error for unknown extensions', () => {
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(['ext4']),
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4');
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('autoUpdate', () => {
|
||||
it('should be false if autoUpdate is not set in install metadata', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
activeExtensions.every(
|
||||
(e) => e.installMetadata?.autoUpdate === false,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true if autoUpdate is true in install metadata', () => {
|
||||
const extensionsWithAutoUpdate: GeminiCLIExtension[] = extensions.map(
|
||||
(e) => ({
|
||||
...e,
|
||||
installMetadata: {
|
||||
...e.installMetadata!,
|
||||
autoUpdate: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensionsWithAutoUpdate,
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
activeExtensions.every((e) => e.installMetadata?.autoUpdate === true),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should respect the per-extension settings from install metadata', () => {
|
||||
const extensionsWithAutoUpdate: GeminiCLIExtension[] = [
|
||||
{
|
||||
path: '/path/to/ext1',
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
contextFiles: [],
|
||||
installMetadata: {
|
||||
source: 'test',
|
||||
type: 'local',
|
||||
autoUpdate: true,
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
path: '/path/to/ext2',
|
||||
name: 'ext2',
|
||||
version: '1.0.0',
|
||||
contextFiles: [],
|
||||
installMetadata: {
|
||||
source: 'test',
|
||||
type: 'local',
|
||||
autoUpdate: false,
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
path: '/path/to/ext3',
|
||||
name: 'ext3',
|
||||
version: '1.0.0',
|
||||
contextFiles: [],
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensionsWithAutoUpdate,
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
activeExtensions.find((e) => e.name === 'ext1')?.installMetadata
|
||||
?.autoUpdate,
|
||||
).toBe(true);
|
||||
expect(
|
||||
activeExtensions.find((e) => e.name === 'ext2')?.installMetadata
|
||||
?.autoUpdate,
|
||||
).toBe(false);
|
||||
expect(
|
||||
activeExtensions.find((e) => e.name === 'ext3')?.installMetadata
|
||||
?.autoUpdate,
|
||||
).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('installExtension', () => {
|
||||
it('should install an extension from a local path', async () => {
|
||||
const sourceExtDir = createExtension({
|
||||
|
|
@ -1194,6 +1024,7 @@ This extension will run the following MCP servers:
|
|||
await loadExtensionConfig({
|
||||
extensionDir: sourceExtDir,
|
||||
workspaceDir: process.cwd(),
|
||||
extensionEnablementManager: new ExtensionEnablementManager(),
|
||||
}),
|
||||
),
|
||||
).resolves.toBe('my-local-extension');
|
||||
|
|
@ -1512,7 +1343,11 @@ This extension will run the following MCP servers:
|
|||
version: '1.0.0',
|
||||
});
|
||||
|
||||
disableExtension('my-extension', SettingScope.User);
|
||||
disableExtension(
|
||||
'my-extension',
|
||||
SettingScope.User,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
isEnabled({
|
||||
name: 'my-extension',
|
||||
|
|
@ -1531,6 +1366,7 @@ This extension will run the following MCP servers:
|
|||
disableExtension(
|
||||
'my-extension',
|
||||
SettingScope.Workspace,
|
||||
new ExtensionEnablementManager(),
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
expect(
|
||||
|
|
@ -1554,8 +1390,16 @@ This extension will run the following MCP servers:
|
|||
version: '1.0.0',
|
||||
});
|
||||
|
||||
disableExtension('my-extension', SettingScope.User);
|
||||
disableExtension('my-extension', SettingScope.User);
|
||||
disableExtension(
|
||||
'my-extension',
|
||||
SettingScope.User,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
disableExtension(
|
||||
'my-extension',
|
||||
SettingScope.User,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
isEnabled({
|
||||
name: 'my-extension',
|
||||
|
|
@ -1566,7 +1410,11 @@ This extension will run the following MCP servers:
|
|||
|
||||
it('should throw an error if you request system scope', () => {
|
||||
expect(() =>
|
||||
disableExtension('my-extension', SettingScope.System),
|
||||
disableExtension(
|
||||
'my-extension',
|
||||
SettingScope.System,
|
||||
new ExtensionEnablementManager(),
|
||||
),
|
||||
).toThrow('System and SystemDefaults scopes are not supported.');
|
||||
});
|
||||
|
||||
|
|
@ -1577,7 +1425,11 @@ This extension will run the following MCP servers:
|
|||
version: '1.0.0',
|
||||
});
|
||||
|
||||
disableExtension('ext1', SettingScope.Workspace);
|
||||
disableExtension(
|
||||
'ext1',
|
||||
SettingScope.Workspace,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
|
||||
expect(mockLogExtensionDisable).toHaveBeenCalled();
|
||||
expect(ExtensionDisableEvent).toHaveBeenCalledWith(
|
||||
|
|
@ -1595,12 +1447,7 @@ This extension will run the following MCP servers:
|
|||
const getActiveExtensions = (): GeminiCLIExtension[] => {
|
||||
const manager = new ExtensionEnablementManager();
|
||||
const extensions = loadExtensions(manager);
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
tempWorkspaceDir,
|
||||
manager,
|
||||
);
|
||||
return activeExtensions.filter((e) => e.isActive);
|
||||
return extensions.filter((e) => e.isActive);
|
||||
};
|
||||
|
||||
it('should enable an extension at the user scope', () => {
|
||||
|
|
@ -1609,11 +1456,12 @@ This extension will run the following MCP servers:
|
|||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
disableExtension('ext1', SettingScope.User);
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
disableExtension('ext1', SettingScope.User, extensionEnablementManager);
|
||||
let activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
|
||||
enableExtension('ext1', SettingScope.User);
|
||||
enableExtension('ext1', SettingScope.User, extensionEnablementManager);
|
||||
activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(1);
|
||||
expect(activeExtensions[0].name).toBe('ext1');
|
||||
|
|
@ -1625,11 +1473,20 @@ This extension will run the following MCP servers:
|
|||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
disableExtension('ext1', SettingScope.Workspace);
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
disableExtension(
|
||||
'ext1',
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
let activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(0);
|
||||
|
||||
enableExtension('ext1', SettingScope.Workspace);
|
||||
enableExtension(
|
||||
'ext1',
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
activeExtensions = getActiveExtensions();
|
||||
expect(activeExtensions).toHaveLength(1);
|
||||
expect(activeExtensions[0].name).toBe('ext1');
|
||||
|
|
@ -1641,8 +1498,17 @@ This extension will run the following MCP servers:
|
|||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
disableExtension('ext1', SettingScope.Workspace);
|
||||
enableExtension('ext1', SettingScope.Workspace);
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
disableExtension(
|
||||
'ext1',
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
enableExtension(
|
||||
'ext1',
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
|
||||
expect(mockLogExtensionEnable).toHaveBeenCalled();
|
||||
expect(ExtensionEnableEvent).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export function loadExtensions(
|
|||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
if (extension != null) {
|
||||
extensions.push(extension);
|
||||
|
|
@ -151,10 +152,7 @@ export function loadExtensions(
|
|||
const uniqueExtensions = new Map<string, GeminiCLIExtension>();
|
||||
|
||||
for (const extension of extensions) {
|
||||
if (
|
||||
!uniqueExtensions.has(extension.name) &&
|
||||
extensionEnablementManager.isEnabled(extension.name, workspaceDir)
|
||||
) {
|
||||
if (!uniqueExtensions.has(extension.name)) {
|
||||
uniqueExtensions.set(extension.name, extension);
|
||||
}
|
||||
}
|
||||
|
|
@ -165,7 +163,7 @@ export function loadExtensions(
|
|||
export function loadExtension(
|
||||
context: LoadExtensionContext,
|
||||
): GeminiCLIExtension | null {
|
||||
const { extensionDir, workspaceDir } = context;
|
||||
const { extensionDir, workspaceDir, extensionEnablementManager } = context;
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -181,6 +179,7 @@ export function loadExtension(
|
|||
let config = loadExtensionConfig({
|
||||
extensionDir: effectiveExtensionPath,
|
||||
workspaceDir,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
|
||||
config = resolveEnvVarsInObject(config);
|
||||
|
|
@ -230,7 +229,7 @@ export function loadExtension(
|
|||
installMetadata,
|
||||
mcpServers: config.mcpServers,
|
||||
excludeTools: config.excludeTools,
|
||||
isActive: true, // Barring any other signals extensions should be considered Active.
|
||||
isActive: extensionEnablementManager.isEnabled(config.name, workspaceDir),
|
||||
id,
|
||||
};
|
||||
} catch (e) {
|
||||
|
|
@ -245,6 +244,7 @@ export function loadExtension(
|
|||
|
||||
export function loadExtensionByName(
|
||||
name: string,
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
workspaceDir: string = process.cwd(),
|
||||
): GeminiCLIExtension | null {
|
||||
const userExtensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||
|
|
@ -257,7 +257,11 @@ export function loadExtensionByName(
|
|||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const extension = loadExtension({ extensionDir, workspaceDir });
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
if (extension && extension.name.toLowerCase() === name.toLowerCase()) {
|
||||
return extension;
|
||||
}
|
||||
|
|
@ -294,25 +298,6 @@ function getContextFileNames(config: ExtensionConfig): string[] {
|
|||
return config.contextFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an annotated list of extensions. If an extension is listed in enabledExtensionNames, it will be active.
|
||||
* If enabledExtensionNames is empty, an extension is active unless it is disabled.
|
||||
* @param extensions The base list of extensions.
|
||||
* @param enabledExtensionNames The names of explicitly enabled extensions.
|
||||
* @param workspaceDir The current workspace directory.
|
||||
*/
|
||||
export function annotateActiveExtensions(
|
||||
extensions: GeminiCLIExtension[],
|
||||
workspaceDir: string,
|
||||
manager: ExtensionEnablementManager,
|
||||
): GeminiCLIExtension[] {
|
||||
manager.validateExtensionOverrides(extensions);
|
||||
return extensions.map((extension) => ({
|
||||
...extension,
|
||||
isActive: manager.isEnabled(extension.name, workspaceDir),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests consent from the user to perform an action, by reading a Y/n
|
||||
* character from stdin.
|
||||
|
|
@ -409,6 +394,7 @@ export async function installOrUpdateExtension(
|
|||
const telemetryConfig = getTelemetryConfig(cwd);
|
||||
let newExtensionConfig: ExtensionConfig | null = null;
|
||||
let localSourcePath: string | undefined;
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
|
||||
try {
|
||||
const settings = loadSettings(cwd).merged;
|
||||
|
|
@ -480,6 +466,7 @@ export async function installOrUpdateExtension(
|
|||
newExtensionConfig = loadExtensionConfig({
|
||||
extensionDir: localSourcePath,
|
||||
workspaceDir: cwd,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
|
||||
const newExtensionName = newExtensionConfig.name;
|
||||
|
|
@ -555,7 +542,11 @@ export async function installOrUpdateExtension(
|
|||
'success',
|
||||
),
|
||||
);
|
||||
enableExtension(newExtensionConfig.name, SettingScope.User);
|
||||
enableExtension(
|
||||
newExtensionConfig.name,
|
||||
SettingScope.User,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
}
|
||||
|
||||
return newExtensionConfig!.name;
|
||||
|
|
@ -567,6 +558,7 @@ export async function installOrUpdateExtension(
|
|||
newExtensionConfig = loadExtensionConfig({
|
||||
extensionDir: localSourcePath,
|
||||
workspaceDir: cwd,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
} catch {
|
||||
// Ignore error, this is just for logging.
|
||||
|
|
@ -791,38 +783,38 @@ export function toOutputString(
|
|||
export function disableExtension(
|
||||
name: string,
|
||||
scope: SettingScope,
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
cwd: string = process.cwd(),
|
||||
) {
|
||||
const config = getTelemetryConfig(cwd);
|
||||
if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) {
|
||||
throw new Error('System and SystemDefaults scopes are not supported.');
|
||||
}
|
||||
const extension = loadExtensionByName(name, cwd);
|
||||
const extension = loadExtensionByName(name, extensionEnablementManager, cwd);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with name ${name} does not exist.`);
|
||||
}
|
||||
|
||||
const manager = new ExtensionEnablementManager([name]);
|
||||
const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir();
|
||||
manager.disable(name, true, scopePath);
|
||||
extensionEnablementManager.disable(name, true, scopePath);
|
||||
logExtensionDisable(config, new ExtensionDisableEvent(name, scope));
|
||||
}
|
||||
|
||||
export function enableExtension(
|
||||
name: string,
|
||||
scope: SettingScope,
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
cwd: string = process.cwd(),
|
||||
) {
|
||||
if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) {
|
||||
throw new Error('System and SystemDefaults scopes are not supported.');
|
||||
}
|
||||
const extension = loadExtensionByName(name, cwd);
|
||||
const extension = loadExtensionByName(name, extensionEnablementManager, cwd);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with name ${name} does not exist.`);
|
||||
}
|
||||
const manager = new ExtensionEnablementManager();
|
||||
const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir();
|
||||
manager.enable(name, true, scopePath);
|
||||
extensionEnablementManager.enable(name, true, scopePath);
|
||||
const config = getTelemetryConfig(cwd);
|
||||
logExtensionEnable(config, new ExtensionEnableEvent(name, scope));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import * as path from 'node:path';
|
|||
import * as tar from 'tar';
|
||||
import * as archiver from 'archiver';
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { ExtensionEnablementManager } from './extensionEnablement.js';
|
||||
|
||||
const mockPlatform = vi.hoisted(() => vi.fn());
|
||||
const mockArch = vi.hoisted(() => vi.fn());
|
||||
|
|
@ -149,7 +150,10 @@ describe('git extension helpers', () => {
|
|||
},
|
||||
contextFiles: [],
|
||||
};
|
||||
const result = await checkForExtensionUpdate(extension);
|
||||
const result = await checkForExtensionUpdate(
|
||||
extension,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(result).toBe(ExtensionUpdateState.NOT_UPDATABLE);
|
||||
});
|
||||
|
||||
|
|
@ -166,7 +170,10 @@ describe('git extension helpers', () => {
|
|||
contextFiles: [],
|
||||
};
|
||||
mockGit.getRemotes.mockResolvedValue([]);
|
||||
const result = await checkForExtensionUpdate(extension);
|
||||
const result = await checkForExtensionUpdate(
|
||||
extension,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(result).toBe(ExtensionUpdateState.ERROR);
|
||||
});
|
||||
|
||||
|
|
@ -188,7 +195,10 @@ describe('git extension helpers', () => {
|
|||
mockGit.listRemote.mockResolvedValue('remote-hash\tHEAD');
|
||||
mockGit.revparse.mockResolvedValue('local-hash');
|
||||
|
||||
const result = await checkForExtensionUpdate(extension);
|
||||
const result = await checkForExtensionUpdate(
|
||||
extension,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(result).toBe(ExtensionUpdateState.UPDATE_AVAILABLE);
|
||||
});
|
||||
|
||||
|
|
@ -210,7 +220,10 @@ describe('git extension helpers', () => {
|
|||
mockGit.listRemote.mockResolvedValue('same-hash\tHEAD');
|
||||
mockGit.revparse.mockResolvedValue('same-hash');
|
||||
|
||||
const result = await checkForExtensionUpdate(extension);
|
||||
const result = await checkForExtensionUpdate(
|
||||
extension,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(result).toBe(ExtensionUpdateState.UP_TO_DATE);
|
||||
});
|
||||
|
||||
|
|
@ -228,7 +241,10 @@ describe('git extension helpers', () => {
|
|||
};
|
||||
mockGit.getRemotes.mockRejectedValue(new Error('git error'));
|
||||
|
||||
const result = await checkForExtensionUpdate(extension);
|
||||
const result = await checkForExtensionUpdate(
|
||||
extension,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(result).toBe(ExtensionUpdateState.ERROR);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { EXTENSIONS_CONFIG_FILENAME, loadExtension } from '../extension.js';
|
|||
import * as tar from 'tar';
|
||||
import extract from 'extract-zip';
|
||||
import { fetchJson, getGitHubToken } from './github_fetch.js';
|
||||
import { type ExtensionEnablementManager } from './extensionEnablement.js';
|
||||
|
||||
/**
|
||||
* Clones a Git repository to a specified local path.
|
||||
|
|
@ -152,6 +153,7 @@ export async function fetchReleaseFromGithub(
|
|||
|
||||
export async function checkForExtensionUpdate(
|
||||
extension: GeminiCLIExtension,
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<ExtensionUpdateState> {
|
||||
const installMetadata = extension.installMetadata;
|
||||
|
|
@ -159,6 +161,7 @@ export async function checkForExtensionUpdate(
|
|||
const newExtension = loadExtension({
|
||||
extensionDir: installMetadata.source,
|
||||
workspaceDir: cwd,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
if (!newExtension) {
|
||||
debugLogger.error(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import * as path from 'node:path';
|
|||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
INSTALL_METADATA_FILENAME,
|
||||
annotateActiveExtensions,
|
||||
loadExtension,
|
||||
} from '../extension.js';
|
||||
import { checkForAllExtensionUpdates, updateExtension } from './update.js';
|
||||
|
|
@ -128,18 +127,15 @@ describe('update tests', () => {
|
|||
);
|
||||
});
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: targetExtDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir: targetExtDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
const updateInfo = await updateExtension(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
tempHomeDir,
|
||||
async (_) => true,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
|
|
@ -185,18 +181,15 @@ describe('update tests', () => {
|
|||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
await updateExtension(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
tempHomeDir,
|
||||
async (_) => true,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
|
|
@ -235,19 +228,16 @@ describe('update tests', () => {
|
|||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
|
||||
const dispatch = vi.fn();
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
await expect(
|
||||
updateExtension(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
tempHomeDir,
|
||||
async (_) => true,
|
||||
ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
|
|
@ -283,16 +273,12 @@ describe('update tests', () => {
|
|||
type: 'git',
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{ name: 'origin', refs: { fetch: 'https://some.git/repo' } },
|
||||
|
|
@ -303,6 +289,7 @@ describe('update tests', () => {
|
|||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionEnablementManager,
|
||||
dispatch,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
|
@ -325,16 +312,12 @@ describe('update tests', () => {
|
|||
type: 'git',
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
{ name: 'origin', refs: { fetch: 'https://some.git/repo' } },
|
||||
|
|
@ -345,6 +328,7 @@ describe('update tests', () => {
|
|||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionEnablementManager,
|
||||
dispatch,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
|
@ -371,19 +355,16 @@ describe('update tests', () => {
|
|||
version: '1.0.0',
|
||||
installMetadata: { source: sourceExtensionDir, type: 'local' },
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: installedExtensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir: installedExtensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionEnablementManager,
|
||||
dispatch,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
|
@ -410,19 +391,16 @@ describe('update tests', () => {
|
|||
version: '1.0.0',
|
||||
installMetadata: { source: sourceExtensionDir, type: 'local' },
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: installedExtensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir: installedExtensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionEnablementManager,
|
||||
dispatch,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
|
@ -445,22 +423,19 @@ describe('update tests', () => {
|
|||
type: 'git',
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
|
||||
mockGit.getRemotes.mockRejectedValue(new Error('Git error'));
|
||||
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
[extension],
|
||||
extensionEnablementManager,
|
||||
dispatch,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { checkForExtensionUpdate } from './github.js';
|
|||
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { type ExtensionEnablementManager } from './extensionEnablement.js';
|
||||
|
||||
export interface ExtensionUpdateInfo {
|
||||
name: string;
|
||||
|
|
@ -30,6 +31,7 @@ export interface ExtensionUpdateInfo {
|
|||
|
||||
export async function updateExtension(
|
||||
extension: GeminiCLIExtension,
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
cwd: string = process.cwd(),
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
currentState: ExtensionUpdateState,
|
||||
|
|
@ -67,6 +69,7 @@ export async function updateExtension(
|
|||
const previousExtensionConfig = await loadExtensionConfig({
|
||||
extensionDir: extension.path,
|
||||
workspaceDir: cwd,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
await installOrUpdateExtension(
|
||||
installMetadata,
|
||||
|
|
@ -79,6 +82,7 @@ export async function updateExtension(
|
|||
const updatedExtension = loadExtension({
|
||||
extensionDir: updatedExtensionStorage.getExtensionDir(),
|
||||
workspaceDir: cwd,
|
||||
extensionEnablementManager,
|
||||
});
|
||||
if (!updatedExtension) {
|
||||
dispatchExtensionStateUpdate({
|
||||
|
|
@ -120,6 +124,7 @@ export async function updateAllUpdatableExtensions(
|
|||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
extensions: GeminiCLIExtension[],
|
||||
extensionsState: Map<string, ExtensionUpdateStatus>,
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
dispatch: (action: ExtensionUpdateAction) => void,
|
||||
): Promise<ExtensionUpdateInfo[]> {
|
||||
return (
|
||||
|
|
@ -133,6 +138,7 @@ export async function updateAllUpdatableExtensions(
|
|||
.map((extension) =>
|
||||
updateExtension(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
cwd,
|
||||
requestConsent,
|
||||
extensionsState.get(extension.name)!.status,
|
||||
|
|
@ -150,6 +156,7 @@ export interface ExtensionUpdateCheckResult {
|
|||
|
||||
export async function checkForAllExtensionUpdates(
|
||||
extensions: GeminiCLIExtension[],
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
dispatch: (action: ExtensionUpdateAction) => void,
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<void> {
|
||||
|
|
@ -174,11 +181,12 @@ export async function checkForAllExtensionUpdates(
|
|||
},
|
||||
});
|
||||
promises.push(
|
||||
checkForExtensionUpdate(extension, cwd).then((state) =>
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state },
|
||||
}),
|
||||
checkForExtensionUpdate(extension, extensionEnablementManager, cwd).then(
|
||||
(state) =>
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ExtensionEnablementManager } from './extensionEnablement.js';
|
||||
|
||||
export interface VariableDefinition {
|
||||
type: 'string';
|
||||
description: string;
|
||||
|
|
@ -18,6 +20,7 @@ export interface VariableSchema {
|
|||
export interface LoadExtensionContext {
|
||||
extensionDir: string;
|
||||
workspaceDir: string;
|
||||
extensionEnablementManager: ExtensionEnablementManager;
|
||||
}
|
||||
|
||||
const PATH_SEPARATOR_DEFINITION = {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import {
|
|||
import * as fs from 'node:fs'; // fs will be mocked separately
|
||||
import stripJsonComments from 'strip-json-comments'; // Will be mocked separately
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import { disableExtension } from './extension.js';
|
||||
import { disableExtension, ExtensionStorage } from './extension.js';
|
||||
|
||||
// These imports will get the versions from the vi.mock('./settings.js', ...) factory.
|
||||
import {
|
||||
|
|
@ -65,7 +65,8 @@ import {
|
|||
migrateDeprecatedSettings,
|
||||
SettingScope,
|
||||
} from './settings.js';
|
||||
import { FatalConfigError, GEMINI_DIR } from '@google/gemini-cli-core';
|
||||
import { FatalConfigError, GEMINI_DIR, Storage } from '@google/gemini-cli-core';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
|
||||
const MOCK_WORKSPACE_DIR = '/mock/workspace';
|
||||
// Use the (mocked) GEMINI_DIR for consistency
|
||||
|
|
@ -93,9 +94,7 @@ vi.mock('fs', async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock('./extension.js', () => ({
|
||||
disableExtension: vi.fn(),
|
||||
}));
|
||||
vi.mock('./extension.js');
|
||||
|
||||
vi.mock('strip-json-comments', () => ({
|
||||
default: vi.fn((content) => content),
|
||||
|
|
@ -2349,7 +2348,9 @@ describe('Settings Loading and Merging', () => {
|
|||
mockFsExistsSync = vi.mocked(fs.existsSync);
|
||||
mockFsReadFileSync = vi.mocked(fs.readFileSync);
|
||||
mockDisableExtension = vi.mocked(disableExtension);
|
||||
|
||||
vi.mocked(ExtensionStorage.getUserExtensionsDir).mockReturnValue(
|
||||
new Storage(osActual.homedir()).getExtensionsDir(),
|
||||
);
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
|
|
@ -2392,11 +2393,13 @@ describe('Settings Loading and Merging', () => {
|
|||
expect(mockDisableExtension).toHaveBeenCalledWith(
|
||||
'user-ext-1',
|
||||
SettingScope.User,
|
||||
expect.any(ExtensionEnablementManager),
|
||||
MOCK_WORKSPACE_DIR,
|
||||
);
|
||||
expect(mockDisableExtension).toHaveBeenCalledWith(
|
||||
'shared-ext',
|
||||
SettingScope.User,
|
||||
expect.any(ExtensionEnablementManager),
|
||||
MOCK_WORKSPACE_DIR,
|
||||
);
|
||||
|
||||
|
|
@ -2404,11 +2407,13 @@ describe('Settings Loading and Merging', () => {
|
|||
expect(mockDisableExtension).toHaveBeenCalledWith(
|
||||
'workspace-ext-1',
|
||||
SettingScope.Workspace,
|
||||
expect.any(ExtensionEnablementManager),
|
||||
MOCK_WORKSPACE_DIR,
|
||||
);
|
||||
expect(mockDisableExtension).toHaveBeenCalledWith(
|
||||
'shared-ext',
|
||||
SettingScope.Workspace,
|
||||
expect.any(ExtensionEnablementManager),
|
||||
MOCK_WORKSPACE_DIR,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
|||
import { customDeepMerge, type MergeableObject } from '../utils/deepMerge.js';
|
||||
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
||||
import { disableExtension } from './extension.js';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
|
||||
function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
|
||||
let current: SettingDefinition | undefined = undefined;
|
||||
|
|
@ -755,8 +756,14 @@ export function migrateDeprecatedSettings(
|
|||
console.log(
|
||||
`Migrating deprecated extensions.disabled settings from ${scope} settings...`,
|
||||
);
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
for (const extension of settings.extensions.disabled ?? []) {
|
||||
disableExtension(extension, scope, workspaceDir);
|
||||
disableExtension(
|
||||
extension,
|
||||
scope,
|
||||
extensionEnablementManager,
|
||||
workspaceDir,
|
||||
);
|
||||
}
|
||||
|
||||
const newExtensionsValue = { ...settings.extensions };
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ export async function main() {
|
|||
? getNodeMemoryArgs(isDebugMode)
|
||||
: [];
|
||||
const sandboxConfig = await loadSandboxConfig(settings.merged, argv);
|
||||
// We intentially omit the list of extensions here because extensions
|
||||
// We intentionally omit the list of extensions here because extensions
|
||||
// should not impact auth or setting up the sandbox.
|
||||
// TODO(jacobr): refactor loadCliConfig so there is a minimal version
|
||||
// that only initializes enough config to enable refreshAuth or find
|
||||
|
|
@ -289,7 +289,6 @@ export async function main() {
|
|||
const partialConfig = await loadCliConfig(
|
||||
settings.merged,
|
||||
[],
|
||||
new ExtensionEnablementManager(),
|
||||
sessionId,
|
||||
argv,
|
||||
);
|
||||
|
|
@ -367,7 +366,6 @@ export async function main() {
|
|||
const config = await loadCliConfig(
|
||||
settings.merged,
|
||||
extensions,
|
||||
extensionEnablementManager,
|
||||
sessionId,
|
||||
argv,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ import { useSessionStats } from './contexts/SessionContext.js';
|
|||
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||
import { useExtensionUpdates } from './hooks/useExtensionUpdates.js';
|
||||
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
|
||||
import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js';
|
||||
|
||||
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
||||
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
|
||||
|
|
@ -159,6 +160,9 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
);
|
||||
|
||||
const extensions = config.getExtensions();
|
||||
const [extensionEnablementManager] = useState<ExtensionEnablementManager>(
|
||||
new ExtensionEnablementManager(config.getEnabledExtensions()),
|
||||
);
|
||||
const {
|
||||
extensionsUpdateState,
|
||||
extensionsUpdateStateInternal,
|
||||
|
|
@ -167,6 +171,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
addConfirmUpdateExtensionRequest,
|
||||
} = useExtensionUpdates(
|
||||
extensions,
|
||||
extensionEnablementManager,
|
||||
historyManager.addItem,
|
||||
config.getWorkingDir(),
|
||||
);
|
||||
|
|
@ -529,7 +534,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
settings.merged,
|
||||
config.getExtensionContextFilePaths(),
|
||||
config.getExtensions(),
|
||||
config.isTrustedFolder(),
|
||||
settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree'
|
||||
config.getFileFilteringOptions(),
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ describe('directoryCommand', () => {
|
|||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
getDebugMode: () => false,
|
||||
getFileService: () => ({}),
|
||||
getExtensionContextFilePaths: () => [],
|
||||
getFileFilteringOptions: () => ({ ignore: [], include: [] }),
|
||||
setUserMemory: vi.fn(),
|
||||
setGeminiMdFileCount: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export const directoryCommand: SlashCommand = {
|
|||
],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
config.getExtensions(),
|
||||
config.getFolderTrust(),
|
||||
context.services.settings.merged.context?.importFormat ||
|
||||
'tree', // Use setting or default to 'tree'
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ describe('memoryCommand', () => {
|
|||
getWorkingDir: () => '/test/dir',
|
||||
getDebugMode: () => false,
|
||||
getFileService: () => ({}) as FileDiscoveryService,
|
||||
getExtensionContextFilePaths: () => [],
|
||||
getExtensions: () => [],
|
||||
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||
getWorkspaceContext: () => ({
|
||||
getDirectories: () => [],
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const memoryCommand: SlashCommand = {
|
|||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
settings.merged,
|
||||
config.getExtensionContextFilePaths(),
|
||||
config.getExtensions(),
|
||||
config.isTrustedFolder(),
|
||||
settings.merged.context?.importFormat || 'tree',
|
||||
config.getFileFilteringOptions(),
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ describe('<ExtensionsList />', () => {
|
|||
const mockUIState = (
|
||||
extensions: unknown[],
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateState>,
|
||||
disabledExtensions: string[] = [],
|
||||
) => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
commandContext: createMockCommandContext({
|
||||
|
|
@ -37,13 +36,6 @@ describe('<ExtensionsList />', () => {
|
|||
config: {
|
||||
getExtensions: () => extensions,
|
||||
},
|
||||
settings: {
|
||||
merged: {
|
||||
extensions: {
|
||||
disabled: disabledExtensions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
extensionsUpdateState,
|
||||
|
|
@ -58,7 +50,7 @@ describe('<ExtensionsList />', () => {
|
|||
});
|
||||
|
||||
it('should render a list of extensions with their version and status', () => {
|
||||
mockUIState(mockExtensions, new Map(), ['ext-disabled']);
|
||||
mockUIState(mockExtensions, new Map());
|
||||
const { lastFrame } = render(<ExtensionsList />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('ext-one (v1.0.0) - active');
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import { ExtensionUpdateState } from '../../state/extensions.js';
|
|||
export const ExtensionsList = () => {
|
||||
const { commandContext, extensionsUpdateState } = useUIState();
|
||||
const allExtensions = commandContext.services.config!.getExtensions();
|
||||
const settings = commandContext.services.settings;
|
||||
const disabledExtensions = settings.merged.extensions?.disabled ?? [];
|
||||
|
||||
if (allExtensions.length === 0) {
|
||||
return <Text>No extensions installed.</Text>;
|
||||
|
|
@ -24,8 +22,9 @@ export const ExtensionsList = () => {
|
|||
<Box flexDirection="column" paddingLeft={2}>
|
||||
{allExtensions.map((ext) => {
|
||||
const state = extensionsUpdateState.get(ext.name);
|
||||
const isActive = !disabledExtensions.includes(ext.name);
|
||||
const isActive = ext.isActive;
|
||||
const activeString = isActive ? 'active' : 'disabled';
|
||||
const activeColor = isActive ? 'green' : 'grey';
|
||||
|
||||
let stateColor = 'gray';
|
||||
const stateText = state || 'unknown state';
|
||||
|
|
@ -55,7 +54,7 @@ export const ExtensionsList = () => {
|
|||
<Box key={ext.name}>
|
||||
<Text>
|
||||
<Text color="cyan">{`${ext.name} (v${ext.version})`}</Text>
|
||||
{` - ${activeString}`}
|
||||
<Text color={activeColor}>{` - ${activeString}`}</Text>
|
||||
{<Text color={stateColor}>{` (${stateText})`}</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -115,8 +115,8 @@ export const McpStatus: React.FC<McpStatusProps> = ({
|
|||
}
|
||||
|
||||
let serverDisplayName = serverName;
|
||||
if (server.extensionName) {
|
||||
serverDisplayName += ` (from ${server.extensionName})`;
|
||||
if (server.extension?.name) {
|
||||
serverDisplayName += ` (from ${server.extension?.name})`;
|
||||
}
|
||||
|
||||
const toolCount = serverTools.length;
|
||||
|
|
|
|||
|
|
@ -8,21 +8,18 @@ import { vi } from 'vitest';
|
|||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
annotateActiveExtensions,
|
||||
loadExtension,
|
||||
} from '../../config/extension.js';
|
||||
import { loadExtension } from '../../config/extension.js';
|
||||
import { createExtension } from '../../test-utils/createExtension.js';
|
||||
import { useExtensionUpdates } from './useExtensionUpdates.js';
|
||||
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { MessageType } from '../types.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
import {
|
||||
checkForAllExtensionUpdates,
|
||||
updateExtension,
|
||||
} from '../../config/extensions/update.js';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const mockedOs = await importOriginal<typeof os>();
|
||||
|
|
@ -76,7 +73,7 @@ describe('useExtensionUpdates', () => {
|
|||
const cwd = '/test/cwd';
|
||||
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (_extensions, dispatch, _cwd) => {
|
||||
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
|
|
@ -88,7 +85,12 @@ describe('useExtensionUpdates', () => {
|
|||
);
|
||||
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(extensions as GeminiCLIExtension[], addItem, cwd),
|
||||
useExtensionUpdates(
|
||||
extensions as GeminiCLIExtension[],
|
||||
new ExtensionEnablementManager(),
|
||||
addItem,
|
||||
cwd,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -113,16 +115,17 @@ describe('useExtensionUpdates', () => {
|
|||
autoUpdate: true,
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension({ extensionDir, workspaceDir: tempHomeDir })!],
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempHomeDir,
|
||||
extensionEnablementManager,
|
||||
})!;
|
||||
|
||||
const addItem = vi.fn();
|
||||
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (_extensions, dispatch, _cwd) => {
|
||||
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
|
|
@ -139,7 +142,14 @@ describe('useExtensionUpdates', () => {
|
|||
name: '',
|
||||
});
|
||||
|
||||
renderHook(() => useExtensionUpdates([extension], addItem, tempHomeDir));
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(
|
||||
[extension],
|
||||
extensionEnablementManager,
|
||||
addItem,
|
||||
tempHomeDir,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
|
|
@ -177,25 +187,24 @@ describe('useExtensionUpdates', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const extensions = annotateActiveExtensions(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: extensionDir1,
|
||||
workspaceDir: tempHomeDir,
|
||||
})!,
|
||||
loadExtension({
|
||||
extensionDir: extensionDir2,
|
||||
workspaceDir: tempHomeDir,
|
||||
})!,
|
||||
],
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const extensions = [
|
||||
loadExtension({
|
||||
extensionDir: extensionDir1,
|
||||
workspaceDir: tempHomeDir,
|
||||
extensionEnablementManager,
|
||||
})!,
|
||||
loadExtension({
|
||||
extensionDir: extensionDir2,
|
||||
workspaceDir: tempHomeDir,
|
||||
extensionEnablementManager,
|
||||
})!,
|
||||
];
|
||||
|
||||
const addItem = vi.fn();
|
||||
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (_extensions, dispatch, _cwd) => {
|
||||
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: {
|
||||
|
|
@ -225,7 +234,14 @@ describe('useExtensionUpdates', () => {
|
|||
name: '',
|
||||
});
|
||||
|
||||
renderHook(() => useExtensionUpdates(extensions, addItem, tempHomeDir));
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(
|
||||
extensions,
|
||||
extensionEnablementManager,
|
||||
addItem,
|
||||
tempHomeDir,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
|
|
@ -282,7 +298,7 @@ describe('useExtensionUpdates', () => {
|
|||
const cwd = '/test/cwd';
|
||||
|
||||
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
|
||||
async (_extensions, dispatch, _cwd) => {
|
||||
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
|
||||
dispatch({ type: 'BATCH_CHECK_START' });
|
||||
dispatch({
|
||||
type: 'SET_STATE',
|
||||
|
|
@ -303,8 +319,14 @@ describe('useExtensionUpdates', () => {
|
|||
},
|
||||
);
|
||||
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
renderHook(() =>
|
||||
useExtensionUpdates(extensions as GeminiCLIExtension[], addItem, cwd),
|
||||
useExtensionUpdates(
|
||||
extensions as GeminiCLIExtension[],
|
||||
extensionEnablementManager,
|
||||
addItem,
|
||||
cwd,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
type ExtensionUpdateInfo,
|
||||
} from '../../config/extension.js';
|
||||
import { checkExhaustive } from '../../utils/checks.js';
|
||||
import type { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
|
||||
type ConfirmationRequestWrapper = {
|
||||
prompt: React.ReactNode;
|
||||
|
|
@ -49,6 +50,7 @@ function confirmationRequestsReducer(
|
|||
|
||||
export const useExtensionUpdates = (
|
||||
extensions: GeminiCLIExtension[],
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
cwd: string,
|
||||
) => {
|
||||
|
|
@ -93,11 +95,13 @@ export const useExtensionUpdates = (
|
|||
if (extensionsToCheck.length === 0) return;
|
||||
checkForAllExtensionUpdates(
|
||||
extensionsToCheck,
|
||||
extensionEnablementManager,
|
||||
dispatchExtensionStateUpdate,
|
||||
cwd,
|
||||
);
|
||||
}, [
|
||||
extensions,
|
||||
extensionEnablementManager,
|
||||
extensionsUpdateState.extensionStatuses,
|
||||
cwd,
|
||||
dispatchExtensionStateUpdate,
|
||||
|
|
@ -154,6 +158,7 @@ export const useExtensionUpdates = (
|
|||
} else {
|
||||
const updatePromise = updateExtension(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
cwd,
|
||||
(description) =>
|
||||
requestConsentInteractive(
|
||||
|
|
@ -210,6 +215,7 @@ export const useExtensionUpdates = (
|
|||
}
|
||||
}, [
|
||||
extensions,
|
||||
extensionEnablementManager,
|
||||
extensionsUpdateState,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
addItem,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import { z } from 'zod';
|
|||
import { randomUUID } from 'node:crypto';
|
||||
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.
|
||||
|
|
@ -206,7 +205,6 @@ class GeminiAgent {
|
|||
const config = await loadCliConfig(
|
||||
settings,
|
||||
this.extensions,
|
||||
new ExtensionEnablementManager(this.argv.extensions),
|
||||
sessionId,
|
||||
this.argv,
|
||||
cwd,
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export class MCPServerConfig {
|
|||
readonly description?: string,
|
||||
readonly includeTools?: string[],
|
||||
readonly excludeTools?: string[],
|
||||
readonly extensionName?: string,
|
||||
readonly extension?: GeminiCLIExtension,
|
||||
// OAuth configuration
|
||||
readonly oauth?: MCPOAuthConfig,
|
||||
readonly authProviderType?: AuthProviderType,
|
||||
|
|
@ -249,11 +249,11 @@ export interface ConfigParameters {
|
|||
includeDirectories?: string[];
|
||||
bugCommand?: BugCommandSettings;
|
||||
model: string;
|
||||
extensionContextFilePaths?: string[];
|
||||
maxSessionTurns?: number;
|
||||
experimentalZedIntegration?: boolean;
|
||||
listExtensions?: boolean;
|
||||
extensions?: GeminiCLIExtension[];
|
||||
enabledExtensions?: string[];
|
||||
blockedMcpServers?: Array<{ name: string; extensionName: string }>;
|
||||
noBrowser?: boolean;
|
||||
summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>;
|
||||
|
|
@ -332,7 +332,6 @@ export class Config {
|
|||
private readonly cwd: string;
|
||||
private readonly bugCommand: BugCommandSettings | undefined;
|
||||
private model: string;
|
||||
private readonly extensionContextFilePaths: string[];
|
||||
private readonly noBrowser: boolean;
|
||||
private readonly folderTrust: boolean;
|
||||
private ideMode: boolean;
|
||||
|
|
@ -341,6 +340,7 @@ export class Config {
|
|||
private readonly maxSessionTurns: number;
|
||||
private readonly listExtensions: boolean;
|
||||
private readonly _extensions: GeminiCLIExtension[];
|
||||
private readonly _enabledExtensions: string[];
|
||||
private readonly _blockedMcpServers: Array<{
|
||||
name: string;
|
||||
extensionName: string;
|
||||
|
|
@ -436,12 +436,12 @@ export class Config {
|
|||
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
|
||||
this.bugCommand = params.bugCommand;
|
||||
this.model = params.model;
|
||||
this.extensionContextFilePaths = params.extensionContextFilePaths ?? [];
|
||||
this.maxSessionTurns = params.maxSessionTurns ?? -1;
|
||||
this.experimentalZedIntegration =
|
||||
params.experimentalZedIntegration ?? false;
|
||||
this.listExtensions = params.listExtensions ?? false;
|
||||
this._extensions = params.extensions ?? [];
|
||||
this._enabledExtensions = params.enabledExtensions ?? [];
|
||||
this._blockedMcpServers = params.blockedMcpServers ?? [];
|
||||
this.noBrowser = params.noBrowser ?? false;
|
||||
this.summarizeToolOutput = params.summarizeToolOutput;
|
||||
|
|
@ -542,7 +542,7 @@ export class Config {
|
|||
|
||||
async refreshAuth(authMethod: AuthType) {
|
||||
// Vertex and Genai have incompatible encryption and sending history with
|
||||
// throughtSignature from Genai to Vertex will fail, we need to strip them
|
||||
// thoughtSignature from Genai to Vertex will fail, we need to strip them
|
||||
if (
|
||||
this.contentGeneratorConfig?.authType === AuthType.USE_GEMINI &&
|
||||
authMethod === AuthType.LOGIN_WITH_GOOGLE
|
||||
|
|
@ -869,10 +869,6 @@ export class Config {
|
|||
return this.usageStatisticsEnabled;
|
||||
}
|
||||
|
||||
getExtensionContextFilePaths(): string[] {
|
||||
return this.extensionContextFilePaths;
|
||||
}
|
||||
|
||||
getExperimentalZedIntegration(): boolean {
|
||||
return this.experimentalZedIntegration;
|
||||
}
|
||||
|
|
@ -889,6 +885,12 @@ export class Config {
|
|||
return this._extensions;
|
||||
}
|
||||
|
||||
// The list of explicitly enabled extensions, if any were given, may contain
|
||||
// the string "none".
|
||||
getEnabledExtensions(): string[] {
|
||||
return this._enabledExtensions;
|
||||
}
|
||||
|
||||
getBlockedMcpServers(): Array<{ name: string; extensionName: string }> {
|
||||
return this._blockedMcpServers;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|||
import { McpClientManager } from './mcp-client-manager.js';
|
||||
import { McpClient } from './mcp-client.js';
|
||||
import type { ToolRegistry } from './tool-registry.js';
|
||||
import type { PromptRegistry } from '../prompts/prompt-registry.js';
|
||||
import type { WorkspaceContext } from '../utils/workspaceContext.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
|
||||
vi.mock('./mcp-client.js', async () => {
|
||||
|
|
@ -38,18 +36,16 @@ describe('McpClientManager', () => {
|
|||
vi.mocked(McpClient).mockReturnValue(
|
||||
mockedMcpClient as unknown as McpClient,
|
||||
);
|
||||
const manager = new McpClientManager(
|
||||
{
|
||||
'test-server': {},
|
||||
},
|
||||
'',
|
||||
{} as ToolRegistry,
|
||||
{} as PromptRegistry,
|
||||
false,
|
||||
{} as WorkspaceContext,
|
||||
);
|
||||
const manager = new McpClientManager({} as ToolRegistry);
|
||||
await manager.discoverAllMcpTools({
|
||||
isTrustedFolder: () => true,
|
||||
getMcpServers: () => ({
|
||||
'test-server': {},
|
||||
}),
|
||||
getMcpServerCommand: () => '',
|
||||
getPromptRegistry: () => {},
|
||||
getDebugMode: () => false,
|
||||
getWorkspaceContext: () => {},
|
||||
} as unknown as Config);
|
||||
expect(mockedMcpClient.connect).toHaveBeenCalledOnce();
|
||||
expect(mockedMcpClient.discover).toHaveBeenCalledOnce();
|
||||
|
|
@ -65,18 +61,16 @@ describe('McpClientManager', () => {
|
|||
vi.mocked(McpClient).mockReturnValue(
|
||||
mockedMcpClient as unknown as McpClient,
|
||||
);
|
||||
const manager = new McpClientManager(
|
||||
{
|
||||
'test-server': {},
|
||||
},
|
||||
'',
|
||||
{} as ToolRegistry,
|
||||
{} as PromptRegistry,
|
||||
false,
|
||||
{} as WorkspaceContext,
|
||||
);
|
||||
const manager = new McpClientManager({} as ToolRegistry);
|
||||
await manager.discoverAllMcpTools({
|
||||
isTrustedFolder: () => false,
|
||||
getMcpServers: () => ({
|
||||
'test-server': {},
|
||||
}),
|
||||
getMcpServerCommand: () => '',
|
||||
getPromptRegistry: () => {},
|
||||
getDebugMode: () => false,
|
||||
getWorkspaceContext: () => {},
|
||||
} as unknown as Config);
|
||||
expect(mockedMcpClient.connect).not.toHaveBeenCalled();
|
||||
expect(mockedMcpClient.discover).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Config, MCPServerConfig } from '../config/config.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { ToolRegistry } from './tool-registry.js';
|
||||
import type { PromptRegistry } from '../prompts/prompt-registry.js';
|
||||
import {
|
||||
McpClient,
|
||||
MCPDiscoveryState,
|
||||
|
|
@ -14,7 +13,6 @@ import {
|
|||
} from './mcp-client.js';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
import type { EventEmitter } from 'node:events';
|
||||
import type { WorkspaceContext } from '../utils/workspaceContext.js';
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of multiple MCP clients, including local child processes.
|
||||
|
|
@ -23,30 +21,12 @@ import type { WorkspaceContext } from '../utils/workspaceContext.js';
|
|||
*/
|
||||
export class McpClientManager {
|
||||
private clients: Map<string, McpClient> = new Map();
|
||||
private readonly mcpServers: Record<string, MCPServerConfig>;
|
||||
private readonly mcpServerCommand: string | undefined;
|
||||
private readonly toolRegistry: ToolRegistry;
|
||||
private readonly promptRegistry: PromptRegistry;
|
||||
private readonly debugMode: boolean;
|
||||
private readonly workspaceContext: WorkspaceContext;
|
||||
private discoveryState: MCPDiscoveryState = MCPDiscoveryState.NOT_STARTED;
|
||||
private readonly eventEmitter?: EventEmitter;
|
||||
|
||||
constructor(
|
||||
mcpServers: Record<string, MCPServerConfig>,
|
||||
mcpServerCommand: string | undefined,
|
||||
toolRegistry: ToolRegistry,
|
||||
promptRegistry: PromptRegistry,
|
||||
debugMode: boolean,
|
||||
workspaceContext: WorkspaceContext,
|
||||
eventEmitter?: EventEmitter,
|
||||
) {
|
||||
this.mcpServers = mcpServers;
|
||||
this.mcpServerCommand = mcpServerCommand;
|
||||
constructor(toolRegistry: ToolRegistry, eventEmitter?: EventEmitter) {
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.promptRegistry = promptRegistry;
|
||||
this.debugMode = debugMode;
|
||||
this.workspaceContext = workspaceContext;
|
||||
this.eventEmitter = eventEmitter;
|
||||
}
|
||||
|
||||
|
|
@ -62,22 +42,23 @@ export class McpClientManager {
|
|||
await this.stop();
|
||||
|
||||
const servers = populateMcpServerCommand(
|
||||
this.mcpServers,
|
||||
this.mcpServerCommand,
|
||||
cliConfig.getMcpServers() || {},
|
||||
cliConfig.getMcpServerCommand(),
|
||||
);
|
||||
|
||||
this.discoveryState = MCPDiscoveryState.IN_PROGRESS;
|
||||
|
||||
this.eventEmitter?.emit('mcp-client-update', this.clients);
|
||||
const discoveryPromises = Object.entries(servers).map(
|
||||
async ([name, config]) => {
|
||||
const discoveryPromises = Object.entries(servers)
|
||||
.filter(([_, config]) => !config.extension || config.extension.isActive)
|
||||
.map(async ([name, config]) => {
|
||||
const client = new McpClient(
|
||||
name,
|
||||
config,
|
||||
this.toolRegistry,
|
||||
this.promptRegistry,
|
||||
this.workspaceContext,
|
||||
this.debugMode,
|
||||
cliConfig.getPromptRegistry(),
|
||||
cliConfig.getWorkspaceContext(),
|
||||
cliConfig.getDebugMode(),
|
||||
);
|
||||
this.clients.set(name, client);
|
||||
|
||||
|
|
@ -95,8 +76,7 @@ export class McpClientManager {
|
|||
)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(discoveryPromises);
|
||||
this.discoveryState = MCPDiscoveryState.COMPLETED;
|
||||
|
|
|
|||
|
|
@ -174,15 +174,7 @@ export class ToolRegistry {
|
|||
|
||||
constructor(config: Config, eventEmitter?: EventEmitter) {
|
||||
this.config = config;
|
||||
this.mcpClientManager = new McpClientManager(
|
||||
this.config.getMcpServers() ?? {},
|
||||
this.config.getMcpServerCommand(),
|
||||
this,
|
||||
this.config.getPromptRegistry(),
|
||||
this.config.getDebugMode(),
|
||||
this.config.getWorkspaceContext(),
|
||||
eventEmitter,
|
||||
);
|
||||
this.mcpClientManager = new McpClientManager(this, eventEmitter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '../tools/memoryTool.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
import { GEMINI_DIR } from './paths.js';
|
||||
import type { GeminiCLIExtension } from '../config/config.js';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actualOs = await importOriginal<typeof os>();
|
||||
|
|
@ -87,7 +88,7 @@ describe('loadServerHierarchicalMemory', () => {
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
false, // untrusted
|
||||
);
|
||||
|
||||
|
|
@ -116,7 +117,7 @@ describe('loadServerHierarchicalMemory', () => {
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
false, // untrusted
|
||||
);
|
||||
|
||||
|
|
@ -132,7 +133,7 @@ describe('loadServerHierarchicalMemory', () => {
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ describe('loadServerHierarchicalMemory', () => {
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -181,7 +182,7 @@ default context content
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -212,7 +213,7 @@ custom context content
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -247,7 +248,7 @@ cwd context content
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -279,7 +280,7 @@ Subdir custom memory
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -311,7 +312,7 @@ Src directory memory
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -355,7 +356,7 @@ Subdir memory
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -408,7 +409,7 @@ Subdir memory
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
'tree',
|
||||
{
|
||||
|
|
@ -444,7 +445,7 @@ My code memory
|
|||
[],
|
||||
true,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
'tree', // importFormat
|
||||
{
|
||||
|
|
@ -466,7 +467,7 @@ My code memory
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -488,7 +489,12 @@ My code memory
|
|||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[extensionFilePath],
|
||||
[
|
||||
{
|
||||
contextFiles: [extensionFilePath],
|
||||
isActive: true,
|
||||
} as GeminiCLIExtension,
|
||||
], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -515,7 +521,7 @@ Extension memory content
|
|||
[includedDir],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -550,7 +556,7 @@ included directory memory
|
|||
createdFiles.map((f) => path.dirname(f)),
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
@ -585,7 +591,7 @@ included directory memory
|
|||
[childDir, parentDir], // Deliberately include duplicates
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
[], // extensions
|
||||
DEFAULT_FOLDER_TRUST,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { processImports } from './memoryImportProcessor.js';
|
|||
import type { FileFilteringOptions } from '../config/constants.js';
|
||||
import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/constants.js';
|
||||
import { GEMINI_DIR } from './paths.js';
|
||||
import type { GeminiCLIExtension } from '../config/config.js';
|
||||
|
||||
// Simple console logger, similar to the one previously in CLI's config.ts
|
||||
// TODO: Integrate with a more robust server-side logger if available/appropriate.
|
||||
|
|
@ -84,7 +85,6 @@ async function getGeminiMdFilePathsInternal(
|
|||
userHomePath: string,
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
folderTrust: boolean,
|
||||
fileFilteringOptions: FileFilteringOptions,
|
||||
maxDirs: number,
|
||||
|
|
@ -107,7 +107,6 @@ async function getGeminiMdFilePathsInternal(
|
|||
userHomePath,
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
folderTrust,
|
||||
fileFilteringOptions,
|
||||
maxDirs,
|
||||
|
|
@ -137,7 +136,6 @@ async function getGeminiMdFilePathsInternalForEachDir(
|
|||
userHomePath: string,
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
folderTrust: boolean,
|
||||
fileFilteringOptions: FileFilteringOptions,
|
||||
maxDirs: number,
|
||||
|
|
@ -226,11 +224,6 @@ async function getGeminiMdFilePathsInternalForEachDir(
|
|||
}
|
||||
}
|
||||
|
||||
// Add extension context file paths.
|
||||
for (const extensionPath of extensionContextFilePaths) {
|
||||
allPaths.add(extensionPath);
|
||||
}
|
||||
|
||||
const finalPaths = Array.from(allPaths);
|
||||
|
||||
if (debugMode)
|
||||
|
|
@ -343,7 +336,7 @@ export async function loadServerHierarchicalMemory(
|
|||
includeDirectoriesToReadGemini: readonly string[],
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
extensions: GeminiCLIExtension[],
|
||||
folderTrust: boolean,
|
||||
importFormat: 'flat' | 'tree' = 'tree',
|
||||
fileFilteringOptions?: FileFilteringOptions,
|
||||
|
|
@ -363,11 +356,18 @@ export async function loadServerHierarchicalMemory(
|
|||
userHomePath,
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
folderTrust,
|
||||
fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||
maxDirs,
|
||||
);
|
||||
|
||||
// Add extension file paths separately since they may be conditionally enabled.
|
||||
filePaths.push(
|
||||
...extensions
|
||||
.filter((ext) => ext.isActive)
|
||||
.flatMap((ext) => ext.contextFiles),
|
||||
);
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
if (debugMode)
|
||||
logger.debug('No GEMINI.md files found in hierarchy of the workspace.');
|
||||
|
|
|
|||
Loading…
Reference in a new issue