mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 12:04:56 -07:00
Create ExtensionManager class which manages all high level extension tasks (#11667)
This commit is contained in:
@@ -5,11 +5,12 @@
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { disableExtension } from '../../config/extension.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { loadSettings, 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';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface DisableArgs {
|
||||
name: string;
|
||||
@@ -17,20 +18,19 @@ interface DisableArgs {
|
||||
}
|
||||
|
||||
export function handleDisable(args: DisableArgs) {
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
loadedSettings: loadSettings(workspaceDir),
|
||||
});
|
||||
|
||||
try {
|
||||
if (args.scope?.toLowerCase() === 'workspace') {
|
||||
disableExtension(
|
||||
args.name,
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
extensionManager.disableExtension(args.name, SettingScope.Workspace);
|
||||
} else {
|
||||
disableExtension(
|
||||
args.name,
|
||||
SettingScope.User,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
extensionManager.disableExtension(args.name, SettingScope.User);
|
||||
}
|
||||
debugLogger.log(
|
||||
`Extension "${args.name}" successfully disabled for scope "${args.scope}".`,
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import {
|
||||
debugLogger,
|
||||
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';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface EnableArgs {
|
||||
name: string;
|
||||
@@ -20,16 +21,18 @@ interface EnableArgs {
|
||||
}
|
||||
|
||||
export function handleEnable(args: EnableArgs) {
|
||||
const extensionEnablementManager = new ExtensionEnablementManager();
|
||||
const workingDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir: workingDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
loadedSettings: loadSettings(workingDir),
|
||||
});
|
||||
try {
|
||||
if (args.scope?.toLowerCase() === 'workspace') {
|
||||
enableExtension(
|
||||
args.name,
|
||||
SettingScope.Workspace,
|
||||
extensionEnablementManager,
|
||||
);
|
||||
extensionManager.enableExtension(args.name, SettingScope.Workspace);
|
||||
} else {
|
||||
enableExtension(args.name, SettingScope.User, extensionEnablementManager);
|
||||
extensionManager.enableExtension(args.name, SettingScope.User);
|
||||
}
|
||||
if (args.scope) {
|
||||
debugLogger.log(
|
||||
|
||||
@@ -12,11 +12,21 @@ const mockInstallOrUpdateExtension = vi.hoisted(() => vi.fn());
|
||||
const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn());
|
||||
const mockStat = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../config/extension.js', () => ({
|
||||
installOrUpdateExtension: mockInstallOrUpdateExtension,
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: mockRequestConsentNonInteractive,
|
||||
}));
|
||||
|
||||
vi.mock('../../config/extension-manager.ts', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../config/extension-manager.js')>();
|
||||
return {
|
||||
...actual,
|
||||
ExtensionManager: vi.fn().mockImplementation(() => ({
|
||||
installOrUpdateExtension: mockInstallOrUpdateExtension,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../utils/errors.js', () => ({
|
||||
getErrorMessage: vi.fn((error: Error) => error.message),
|
||||
}));
|
||||
@@ -54,7 +64,7 @@ describe('handleInstall', () => {
|
||||
mockInstallOrUpdateExtension.mockClear();
|
||||
mockRequestConsentNonInteractive.mockClear();
|
||||
mockStat.mockClear();
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should install an extension from a http source', async () => {
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
installOrUpdateExtension,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extension.js';
|
||||
import {
|
||||
debugLogger,
|
||||
type ExtensionInstallMetadata,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import {
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface InstallArgs {
|
||||
@@ -67,13 +68,16 @@ export async function handleInstall(args: InstallArgs) {
|
||||
debugLogger.log('You have consented to the following:');
|
||||
debugLogger.log(INSTALL_WARNING_MESSAGE);
|
||||
}
|
||||
const name = await installOrUpdateExtension(
|
||||
installMetadata,
|
||||
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent,
|
||||
process.cwd(),
|
||||
undefined,
|
||||
promptForSetting,
|
||||
);
|
||||
requestSetting: promptForSetting,
|
||||
loadedSettings: loadSettings(workspaceDir),
|
||||
});
|
||||
const name =
|
||||
await extensionManager.installOrUpdateExtension(installMetadata);
|
||||
debugLogger.log(`Extension "${name}" installed successfully and enabled.`);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
installOrUpdateExtension,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extension.js';
|
||||
import {
|
||||
debugLogger,
|
||||
type ExtensionInstallMetadata,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface InstallArgs {
|
||||
path: string;
|
||||
@@ -26,10 +26,15 @@ export async function handleLink(args: InstallArgs) {
|
||||
source: args.path,
|
||||
type: 'link',
|
||||
};
|
||||
const extensionName = await installOrUpdateExtension(
|
||||
installMetadata,
|
||||
requestConsentNonInteractive,
|
||||
);
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
loadedSettings: loadSettings(workspaceDir),
|
||||
});
|
||||
const extensionName =
|
||||
await extensionManager.installOrUpdateExtension(installMetadata);
|
||||
debugLogger.log(
|
||||
`Extension "${extensionName}" linked successfully and enabled.`,
|
||||
);
|
||||
|
||||
@@ -5,24 +5,32 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadExtensions, toOutputString } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
export async function handleList() {
|
||||
try {
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(),
|
||||
process.cwd(),
|
||||
);
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
loadedSettings: loadSettings(workspaceDir),
|
||||
});
|
||||
const extensions = extensionManager.loadExtensions();
|
||||
if (extensions.length === 0) {
|
||||
debugLogger.log('No extensions installed.');
|
||||
return;
|
||||
}
|
||||
debugLogger.log(
|
||||
extensions
|
||||
.map((extension, _): string => toOutputString(extension, process.cwd()))
|
||||
.map((extension, _): string =>
|
||||
extensionManager.toOutputString(extension),
|
||||
)
|
||||
.join('\n\n'),
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { uninstallExtension } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface UninstallArgs {
|
||||
name: string; // can be extension name or source URL.
|
||||
@@ -15,7 +18,14 @@ interface UninstallArgs {
|
||||
|
||||
export async function handleUninstall(args: UninstallArgs) {
|
||||
try {
|
||||
await uninstallExtension(args.name, false);
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
loadedSettings: loadSettings(workspaceDir),
|
||||
});
|
||||
await extensionManager.uninstallExtension(args.name, false);
|
||||
debugLogger.log(`Extension "${args.name}" successfully uninstalled.`);
|
||||
} catch (error) {
|
||||
debugLogger.error(getErrorMessage(error));
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
loadExtensions,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extension.js';
|
||||
import {
|
||||
updateAllUpdatableExtensions,
|
||||
type ExtensionUpdateInfo,
|
||||
@@ -18,8 +14,11 @@ import {
|
||||
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
interface UpdateArgs {
|
||||
name?: string;
|
||||
@@ -30,13 +29,15 @@ const updateOutput = (info: ExtensionUpdateInfo) =>
|
||||
`Extension "${info.name}" successfully updated: ${info.originalVersion} → ${info.updatedVersion}.`;
|
||||
|
||||
export async function handleUpdate(args: UpdateArgs) {
|
||||
const workingDir = process.cwd();
|
||||
const extensionEnablementManager = new ExtensionEnablementManager(
|
||||
// Force enable named extensions, otherwise we will only update the enabled
|
||||
// ones.
|
||||
args.name ? [args.name] : [],
|
||||
);
|
||||
const extensions = loadExtensions(extensionEnablementManager);
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
loadedSettings: loadSettings(workspaceDir),
|
||||
});
|
||||
|
||||
const extensions = extensionManager.loadExtensions();
|
||||
if (args.name) {
|
||||
try {
|
||||
const extension = extensions.find(
|
||||
@@ -54,7 +55,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
}
|
||||
const updateState = await checkForExtensionUpdate(
|
||||
extension,
|
||||
extensionEnablementManager,
|
||||
extensionManager,
|
||||
);
|
||||
if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) {
|
||||
debugLogger.log(`Extension "${args.name}" is already up to date.`);
|
||||
@@ -63,9 +64,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,
|
||||
extensionManager,
|
||||
updateState,
|
||||
() => {},
|
||||
))!;
|
||||
@@ -88,7 +87,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
const extensionState = new Map();
|
||||
await checkForAllExtensionUpdates(
|
||||
extensions,
|
||||
extensionEnablementManager,
|
||||
extensionManager,
|
||||
(action) => {
|
||||
if (action.type === 'SET_STATE') {
|
||||
extensionState.set(action.payload.name, {
|
||||
@@ -96,14 +95,11 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||
});
|
||||
}
|
||||
},
|
||||
workingDir,
|
||||
);
|
||||
let updateInfos = await updateAllUpdatableExtensions(
|
||||
workingDir,
|
||||
requestConsentNonInteractive,
|
||||
extensions,
|
||||
extensionState,
|
||||
extensionEnablementManager,
|
||||
extensionManager,
|
||||
() => {},
|
||||
);
|
||||
updateInfos = updateInfos.filter(
|
||||
|
||||
@@ -7,19 +7,20 @@
|
||||
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
import { listMcpServers } from './list.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { ExtensionStorage, loadExtensions } from '../../config/extension.js';
|
||||
import { createTransport, debugLogger } from '@google/gemini-cli-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionStorage } from '../../config/extensions/storage.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
loadSettings: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../config/extension.js', () => ({
|
||||
loadExtensions: vi.fn(),
|
||||
vi.mock('../../config/extensions/storage.js', () => ({
|
||||
ExtensionStorage: {
|
||||
getUserExtensionsDir: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock('../../config/extension-manager.js');
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
createTransport: vi.fn(),
|
||||
MCPServerStatus: {
|
||||
@@ -46,9 +47,9 @@ vi.mock('@modelcontextprotocol/sdk/client/index.js');
|
||||
const mockedGetUserExtensionsDir =
|
||||
ExtensionStorage.getUserExtensionsDir as Mock;
|
||||
const mockedLoadSettings = loadSettings as Mock;
|
||||
const mockedLoadExtensions = loadExtensions as Mock;
|
||||
const mockedCreateTransport = createTransport as Mock;
|
||||
const MockedClient = Client as Mock;
|
||||
const MockedExtensionManager = ExtensionManager as Mock;
|
||||
|
||||
interface MockClient {
|
||||
connect: Mock;
|
||||
@@ -56,12 +57,17 @@ interface MockClient {
|
||||
close: Mock;
|
||||
}
|
||||
|
||||
interface MockExtensionManager {
|
||||
loadExtensions: Mock;
|
||||
}
|
||||
|
||||
interface MockTransport {
|
||||
close: Mock;
|
||||
}
|
||||
|
||||
describe('mcp list command', () => {
|
||||
let mockClient: MockClient;
|
||||
let mockExtensionManager: MockExtensionManager;
|
||||
let mockTransport: MockTransport;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -73,10 +79,14 @@ describe('mcp list command', () => {
|
||||
ping: vi.fn(),
|
||||
close: vi.fn(),
|
||||
};
|
||||
mockExtensionManager = {
|
||||
loadExtensions: vi.fn(),
|
||||
};
|
||||
|
||||
MockedClient.mockImplementation(() => mockClient);
|
||||
MockedExtensionManager.mockImplementation(() => mockExtensionManager);
|
||||
mockedCreateTransport.mockResolvedValue(mockTransport);
|
||||
mockedLoadExtensions.mockReturnValue([]);
|
||||
mockExtensionManager.loadExtensions.mockReturnValue([]);
|
||||
mockedGetUserExtensionsDir.mockReturnValue('/mocked/extensions/dir');
|
||||
});
|
||||
|
||||
@@ -149,7 +159,7 @@ describe('mcp list command', () => {
|
||||
},
|
||||
});
|
||||
|
||||
mockedLoadExtensions.mockReturnValue([
|
||||
mockExtensionManager.loadExtensions.mockReturnValue([
|
||||
{
|
||||
name: 'test-extension',
|
||||
mcpServers: { 'extension-server': { command: '/ext/server' } },
|
||||
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { loadExtensions } from '../../config/extension.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
const COLOR_GREEN = '\u001b[32m';
|
||||
const COLOR_YELLOW = '\u001b[33m';
|
||||
@@ -26,7 +27,13 @@ async function getMcpServersFromConfig(): Promise<
|
||||
Record<string, MCPServerConfig>
|
||||
> {
|
||||
const settings = loadSettings();
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
const extensionManager = new ExtensionManager({
|
||||
loadedSettings: settings,
|
||||
workspaceDir: process.cwd(),
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
});
|
||||
const extensions = extensionManager.loadExtensions();
|
||||
const mcpServers = { ...(settings.merged.mcpServers || {}) };
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
|
||||
Reference in New Issue
Block a user