From 51bb624d45c0d8bf0fc11ff614e08cd3db987790 Mon Sep 17 00:00:00 2001 From: christine betts Date: Tue, 26 Aug 2025 15:49:00 +0000 Subject: [PATCH] Add extensions enable command (#7042) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/cli/src/commands/extensions.tsx | 2 + .../cli/src/commands/extensions/enable.ts | 59 ++++++++++++++++ packages/cli/src/config/extension.test.ts | 68 ++++++++++++++++++- packages/cli/src/config/extension.ts | 4 ++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/commands/extensions/enable.ts diff --git a/packages/cli/src/commands/extensions.tsx b/packages/cli/src/commands/extensions.tsx index 8bfd0a88..733866a1 100644 --- a/packages/cli/src/commands/extensions.tsx +++ b/packages/cli/src/commands/extensions.tsx @@ -10,6 +10,7 @@ import { uninstallCommand } from './extensions/uninstall.js'; import { listCommand } from './extensions/list.js'; import { updateCommand } from './extensions/update.js'; import { disableCommand } from './extensions/disable.js'; +import { enableCommand } from './extensions/enable.js'; export const extensionsCommand: CommandModule = { command: 'extensions ', @@ -21,6 +22,7 @@ export const extensionsCommand: CommandModule = { .command(listCommand) .command(updateCommand) .command(disableCommand) + .command(enableCommand) .demandCommand(1, 'You need at least one command before continuing.') .version(false), handler: () => { diff --git a/packages/cli/src/commands/extensions/enable.ts b/packages/cli/src/commands/extensions/enable.ts new file mode 100644 index 00000000..7c2947b2 --- /dev/null +++ b/packages/cli/src/commands/extensions/enable.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +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'; + +interface EnableArgs { + name: string; + scope?: SettingScope; +} + +export async function handleEnable(args: EnableArgs) { + try { + const scopes = args.scope + ? [args.scope] + : [SettingScope.User, SettingScope.Workspace]; + enableExtension(args.name, scopes); + if (args.scope) { + console.log( + `Extension "${args.name}" successfully enabled for scope "${args.scope}".`, + ); + } else { + console.log( + `Extension "${args.name}" successfully enabled in all scopes.`, + ); + } + } catch (error) { + throw new FatalConfigError(getErrorMessage(error)); + } +} + +export const enableCommand: CommandModule = { + command: 'disable [--scope] ', + describe: 'Enables an extension.', + builder: (yargs) => + yargs + .positional('name', { + describe: 'The name of the extension to enable.', + type: 'string', + }) + .option('scope', { + describe: + 'The scope to enable the extenison in. If not set, will be enabled in all scopes.', + type: 'string', + choices: [SettingScope.User, SettingScope.Workspace], + }) + .check((_argv) => true), + handler: async (argv) => { + await handleEnable({ + name: argv['name'] as string, + scope: argv['scope'] as SettingScope, + }); + }, +}; diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 8c4c7b9c..37c82e01 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -13,12 +13,16 @@ import { INSTALL_METADATA_FILENAME, annotateActiveExtensions, disableExtension, + enableExtension, installExtension, loadExtensions, uninstallExtension, updateExtension, } from './extension.js'; -import { type MCPServerConfig } from '@google/gemini-cli-core'; +import { + type GeminiCLIExtension, + type MCPServerConfig, +} from '@google/gemini-cli-core'; import { execSync } from 'node:child_process'; import { SettingScope, loadSettings } from './settings.js'; import { type SimpleGit, simpleGit } from 'simple-git'; @@ -576,3 +580,65 @@ describe('disableExtension', () => { ); }); }); + +describe('enableExtension', () => { + let tempWorkspaceDir: string; + let tempHomeDir: string; + let userExtensionsDir: string; + + beforeEach(() => { + tempWorkspaceDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'gemini-cli-test-workspace-'), + ); + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'gemini-cli-test-home-'), + ); + userExtensionsDir = path.join(tempHomeDir, '.gemini', 'extensions'); + vi.mocked(os.homedir).mockReturnValue(tempHomeDir); + vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir); + }); + + afterEach(() => { + fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + fs.rmSync(userExtensionsDir, { recursive: true, force: true }); + }); + + afterAll(() => { + vi.restoreAllMocks(); + }); + + const getActiveExtensions = (): GeminiCLIExtension[] => { + const extensions = loadExtensions(tempWorkspaceDir); + const activeExtensions = annotateActiveExtensions( + extensions, + [], + tempWorkspaceDir, + ); + return activeExtensions.filter((e) => e.isActive); + }; + + it('should enable an extension at the user scope', () => { + createExtension(userExtensionsDir, 'ext1', '1.0.0'); + disableExtension('ext1', SettingScope.User); + let activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(0); + + enableExtension('ext1', [SettingScope.User]); + activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(1); + expect(activeExtensions[0].name).toBe('ext1'); + }); + + it('should enable an extension at the workspace scope', () => { + createExtension(userExtensionsDir, 'ext1', '1.0.0'); + disableExtension('ext1', SettingScope.Workspace); + let activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(0); + + enableExtension('ext1', [SettingScope.Workspace]); + activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(1); + expect(activeExtensions[0].name).toBe('ext1'); + }); +}); diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 231828f3..93c390d1 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -475,6 +475,10 @@ export function disableExtension( } } +export function enableExtension(name: string, scopes: SettingScope[]) { + removeFromDisabledExtensions(name, scopes); +} + /** * Removes an extension from the list of disabled extensions. * @param name The name of the extension to remove.