mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-27 13:34:15 -07:00
Pass whole extensions rather than just context files (#10910)
Co-authored-by: Jake Macdonald <jakemac@google.com>
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user