[extensions] Add disable command (#7001)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
parent
d77391b3cd
commit
dff175c4f4
11 changed files with 291 additions and 29 deletions
|
|
@ -9,6 +9,7 @@ import { installCommand } from './extensions/install.js';
|
||||||
import { uninstallCommand } from './extensions/uninstall.js';
|
import { uninstallCommand } from './extensions/uninstall.js';
|
||||||
import { listCommand } from './extensions/list.js';
|
import { listCommand } from './extensions/list.js';
|
||||||
import { updateCommand } from './extensions/update.js';
|
import { updateCommand } from './extensions/update.js';
|
||||||
|
import { disableCommand } from './extensions/disable.js';
|
||||||
|
|
||||||
export const extensionsCommand: CommandModule = {
|
export const extensionsCommand: CommandModule = {
|
||||||
command: 'extensions <command>',
|
command: 'extensions <command>',
|
||||||
|
|
@ -19,6 +20,7 @@ export const extensionsCommand: CommandModule = {
|
||||||
.command(uninstallCommand)
|
.command(uninstallCommand)
|
||||||
.command(listCommand)
|
.command(listCommand)
|
||||||
.command(updateCommand)
|
.command(updateCommand)
|
||||||
|
.command(disableCommand)
|
||||||
.demandCommand(1, 'You need at least one command before continuing.')
|
.demandCommand(1, 'You need at least one command before continuing.')
|
||||||
.version(false),
|
.version(false),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
|
|
|
||||||
51
packages/cli/src/commands/extensions/disable.ts
Normal file
51
packages/cli/src/commands/extensions/disable.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type CommandModule } from 'yargs';
|
||||||
|
import { disableExtension } from '../../config/extension.js';
|
||||||
|
import { SettingScope } from '../../config/settings.js';
|
||||||
|
import { getErrorMessage } from '../../utils/errors.js';
|
||||||
|
|
||||||
|
interface DisableArgs {
|
||||||
|
name: string;
|
||||||
|
scope: SettingScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDisable(args: DisableArgs) {
|
||||||
|
try {
|
||||||
|
disableExtension(args.name, args.scope);
|
||||||
|
console.log(
|
||||||
|
`Extension "${args.name}" successfully disabled for scope "${args.scope}".`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(getErrorMessage(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const disableCommand: CommandModule = {
|
||||||
|
command: 'disable [--scope] <name>',
|
||||||
|
describe: 'Disables an extension.',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional('name', {
|
||||||
|
describe: 'The name of the extension to disable.',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
.option('scope', {
|
||||||
|
describe: 'The scope to disable the extenison in.',
|
||||||
|
type: 'string',
|
||||||
|
default: SettingScope.User,
|
||||||
|
choices: [SettingScope.User, SettingScope.Workspace],
|
||||||
|
})
|
||||||
|
.check((_argv) => true),
|
||||||
|
handler: async (argv) => {
|
||||||
|
await handleDisable({
|
||||||
|
name: argv['name'] as string,
|
||||||
|
scope: argv['scope'] as SettingScope,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -10,6 +10,8 @@ import {
|
||||||
type ExtensionInstallMetadata,
|
type ExtensionInstallMetadata,
|
||||||
} from '../../config/extension.js';
|
} from '../../config/extension.js';
|
||||||
|
|
||||||
|
import { getErrorMessage } from '../../utils/errors.js';
|
||||||
|
|
||||||
interface InstallArgs {
|
interface InstallArgs {
|
||||||
source?: string;
|
source?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
|
@ -26,7 +28,7 @@ export async function handleInstall(args: InstallArgs) {
|
||||||
`Extension "${extensionName}" installed successfully and enabled.`,
|
`Extension "${extensionName}" installed successfully and enabled.`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
console.error(getErrorMessage(error));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import type { CommandModule } from 'yargs';
|
import type { CommandModule } from 'yargs';
|
||||||
import { loadUserExtensions, toOutputString } from '../../config/extension.js';
|
import { loadUserExtensions, toOutputString } from '../../config/extension.js';
|
||||||
|
import { getErrorMessage } from '../../utils/errors.js';
|
||||||
|
|
||||||
export async function handleList() {
|
export async function handleList() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -20,7 +21,7 @@ export async function handleList() {
|
||||||
.join('\n\n'),
|
.join('\n\n'),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
console.error(getErrorMessage(error));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import type { CommandModule } from 'yargs';
|
import type { CommandModule } from 'yargs';
|
||||||
import { uninstallExtension } from '../../config/extension.js';
|
import { uninstallExtension } from '../../config/extension.js';
|
||||||
|
import { getErrorMessage } from '../../utils/errors.js';
|
||||||
|
|
||||||
interface UninstallArgs {
|
interface UninstallArgs {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -16,7 +17,7 @@ export async function handleUninstall(args: UninstallArgs) {
|
||||||
await uninstallExtension(args.name);
|
await uninstallExtension(args.name);
|
||||||
console.log(`Extension "${args.name}" successfully uninstalled.`);
|
console.log(`Extension "${args.name}" successfully uninstalled.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
console.error(getErrorMessage(error));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import type { CommandModule } from 'yargs';
|
import type { CommandModule } from 'yargs';
|
||||||
import { updateExtension } from '../../config/extension.js';
|
import { updateExtension } from '../../config/extension.js';
|
||||||
|
import { getErrorMessage } from '../../utils/errors.js';
|
||||||
|
|
||||||
interface UpdateArgs {
|
interface UpdateArgs {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -23,7 +24,7 @@ export async function handleUpdate(args: UpdateArgs) {
|
||||||
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion} → ${updatedExtensionInfo.updatedVersion}.`,
|
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion} → ${updatedExtensionInfo.updatedVersion}.`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
console.error(getErrorMessage(error));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,7 @@ export async function loadCliConfig(
|
||||||
const allExtensions = annotateActiveExtensions(
|
const allExtensions = annotateActiveExtensions(
|
||||||
extensions,
|
extensions,
|
||||||
argv.extensions || [],
|
argv.extensions || [],
|
||||||
|
cwd,
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeExtensions = extensions.filter(
|
const activeExtensions = extensions.filter(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
EXTENSIONS_CONFIG_FILENAME,
|
EXTENSIONS_CONFIG_FILENAME,
|
||||||
INSTALL_METADATA_FILENAME,
|
INSTALL_METADATA_FILENAME,
|
||||||
annotateActiveExtensions,
|
annotateActiveExtensions,
|
||||||
|
disableExtension,
|
||||||
installExtension,
|
installExtension,
|
||||||
loadExtensions,
|
loadExtensions,
|
||||||
uninstallExtension,
|
uninstallExtension,
|
||||||
|
|
@ -19,6 +20,7 @@ import {
|
||||||
} from './extension.js';
|
} from './extension.js';
|
||||||
import { type MCPServerConfig } from '@google/gemini-cli-core';
|
import { type MCPServerConfig } from '@google/gemini-cli-core';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
|
import { SettingScope, loadSettings } from './settings.js';
|
||||||
import { type SimpleGit, simpleGit } from 'simple-git';
|
import { type SimpleGit, simpleGit } from 'simple-git';
|
||||||
|
|
||||||
vi.mock('simple-git', () => ({
|
vi.mock('simple-git', () => ({
|
||||||
|
|
@ -130,6 +132,33 @@ describe('loadExtensions', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should filter out disabled extensions', () => {
|
||||||
|
const workspaceExtensionsDir = path.join(
|
||||||
|
tempWorkspaceDir,
|
||||||
|
EXTENSIONS_DIRECTORY_NAME,
|
||||||
|
);
|
||||||
|
fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
|
||||||
|
|
||||||
|
createExtension(workspaceExtensionsDir, 'ext1', '1.0.0');
|
||||||
|
createExtension(workspaceExtensionsDir, 'ext2', '2.0.0');
|
||||||
|
|
||||||
|
const settingsDir = path.join(tempWorkspaceDir, '.gemini');
|
||||||
|
fs.mkdirSync(settingsDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(settingsDir, 'settings.json'),
|
||||||
|
JSON.stringify({ extensions: { disabled: ['ext1'] } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const extensions = loadExtensions(tempWorkspaceDir);
|
||||||
|
const activeExtensions = annotateActiveExtensions(
|
||||||
|
extensions,
|
||||||
|
[],
|
||||||
|
tempWorkspaceDir,
|
||||||
|
).filter((e) => e.isActive);
|
||||||
|
expect(activeExtensions).toHaveLength(1);
|
||||||
|
expect(activeExtensions[0].name).toBe('ext2');
|
||||||
|
});
|
||||||
|
|
||||||
it('should hydrate variables', () => {
|
it('should hydrate variables', () => {
|
||||||
const workspaceExtensionsDir = path.join(
|
const workspaceExtensionsDir = path.join(
|
||||||
tempWorkspaceDir,
|
tempWorkspaceDir,
|
||||||
|
|
@ -164,22 +193,39 @@ describe('loadExtensions', () => {
|
||||||
|
|
||||||
describe('annotateActiveExtensions', () => {
|
describe('annotateActiveExtensions', () => {
|
||||||
const extensions = [
|
const extensions = [
|
||||||
{ config: { name: 'ext1', version: '1.0.0' }, contextFiles: [] },
|
{
|
||||||
{ config: { name: 'ext2', version: '1.0.0' }, contextFiles: [] },
|
path: '/path/to/ext1',
|
||||||
{ config: { name: 'ext3', version: '1.0.0' }, contextFiles: [] },
|
config: { name: 'ext1', version: '1.0.0' },
|
||||||
|
contextFiles: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/path/to/ext2',
|
||||||
|
config: { name: 'ext2', version: '1.0.0' },
|
||||||
|
contextFiles: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/path/to/ext3',
|
||||||
|
config: { name: 'ext3', version: '1.0.0' },
|
||||||
|
contextFiles: [],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it('should mark all extensions as active if no enabled extensions are provided', () => {
|
it('should mark all extensions as active if no enabled extensions are provided', () => {
|
||||||
const activeExtensions = annotateActiveExtensions(extensions, []);
|
const activeExtensions = annotateActiveExtensions(
|
||||||
|
extensions,
|
||||||
|
[],
|
||||||
|
'/path/to/workspace',
|
||||||
|
);
|
||||||
expect(activeExtensions).toHaveLength(3);
|
expect(activeExtensions).toHaveLength(3);
|
||||||
expect(activeExtensions.every((e) => e.isActive)).toBe(true);
|
expect(activeExtensions.every((e) => e.isActive)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should mark only the enabled extensions as active', () => {
|
it('should mark only the enabled extensions as active', () => {
|
||||||
const activeExtensions = annotateActiveExtensions(extensions, [
|
const activeExtensions = annotateActiveExtensions(
|
||||||
'ext1',
|
extensions,
|
||||||
'ext3',
|
['ext1', 'ext3'],
|
||||||
]);
|
'/path/to/workspace',
|
||||||
|
);
|
||||||
expect(activeExtensions).toHaveLength(3);
|
expect(activeExtensions).toHaveLength(3);
|
||||||
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
||||||
true,
|
true,
|
||||||
|
|
@ -193,13 +239,21 @@ describe('annotateActiveExtensions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should mark all extensions as inactive when "none" is provided', () => {
|
it('should mark all extensions as inactive when "none" is provided', () => {
|
||||||
const activeExtensions = annotateActiveExtensions(extensions, ['none']);
|
const activeExtensions = annotateActiveExtensions(
|
||||||
|
extensions,
|
||||||
|
['none'],
|
||||||
|
'/path/to/workspace',
|
||||||
|
);
|
||||||
expect(activeExtensions).toHaveLength(3);
|
expect(activeExtensions).toHaveLength(3);
|
||||||
expect(activeExtensions.every((e) => !e.isActive)).toBe(true);
|
expect(activeExtensions.every((e) => !e.isActive)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle case-insensitivity', () => {
|
it('should handle case-insensitivity', () => {
|
||||||
const activeExtensions = annotateActiveExtensions(extensions, ['EXT1']);
|
const activeExtensions = annotateActiveExtensions(
|
||||||
|
extensions,
|
||||||
|
['EXT1'],
|
||||||
|
'/path/to/workspace',
|
||||||
|
);
|
||||||
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
@ -207,7 +261,7 @@ describe('annotateActiveExtensions', () => {
|
||||||
|
|
||||||
it('should log an error for unknown extensions', () => {
|
it('should log an error for unknown extensions', () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
annotateActiveExtensions(extensions, ['ext4']);
|
annotateActiveExtensions(extensions, ['ext4'], '/path/to/workspace');
|
||||||
expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4');
|
expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4');
|
||||||
consoleSpy.mockRestore();
|
consoleSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
@ -470,3 +524,55 @@ describe('updateExtension', () => {
|
||||||
expect(updatedConfig.version).toBe('1.1.0');
|
expect(updatedConfig.version).toBe('1.1.0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('disableExtension', () => {
|
||||||
|
let tempWorkspaceDir: string;
|
||||||
|
let tempHomeDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempWorkspaceDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'gemini-cli-test-workspace-'),
|
||||||
|
);
|
||||||
|
tempHomeDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||||
|
);
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable an extension at the user scope', () => {
|
||||||
|
disableExtension('my-extension', SettingScope.User);
|
||||||
|
const settings = loadSettings(tempWorkspaceDir);
|
||||||
|
expect(
|
||||||
|
settings.forScope(SettingScope.User).settings.extensions?.disabled,
|
||||||
|
).toEqual(['my-extension']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable an extension at the workspace scope', () => {
|
||||||
|
disableExtension('my-extension', SettingScope.Workspace);
|
||||||
|
const settings = loadSettings(tempWorkspaceDir);
|
||||||
|
expect(
|
||||||
|
settings.forScope(SettingScope.Workspace).settings.extensions?.disabled,
|
||||||
|
).toEqual(['my-extension']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle disabling the same extension twice', () => {
|
||||||
|
disableExtension('my-extension', SettingScope.User);
|
||||||
|
disableExtension('my-extension', SettingScope.User);
|
||||||
|
const settings = loadSettings(tempWorkspaceDir);
|
||||||
|
expect(
|
||||||
|
settings.forScope(SettingScope.User).settings.extensions?.disabled,
|
||||||
|
).toEqual(['my-extension']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if you request system scope', () => {
|
||||||
|
expect(() => disableExtension('my-extension', SettingScope.System)).toThrow(
|
||||||
|
'System and SystemDefaults scopes are not supported.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import { simpleGit } from 'simple-git';
|
import { simpleGit } from 'simple-git';
|
||||||
|
import { SettingScope, loadSettings } from '../config/settings.js';
|
||||||
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
import { recursivelyHydrateStrings } from './extensions/variables.js';
|
import { recursivelyHydrateStrings } from './extensions/variables.js';
|
||||||
|
|
||||||
export const EXTENSIONS_DIRECTORY_NAME = '.gemini/extensions';
|
export const EXTENSIONS_DIRECTORY_NAME = '.gemini/extensions';
|
||||||
|
|
@ -63,10 +65,6 @@ export class ExtensionStorage {
|
||||||
return path.join(this.getExtensionDir(), EXTENSIONS_CONFIG_FILENAME);
|
return path.join(this.getExtensionDir(), EXTENSIONS_CONFIG_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSettingsPath(): string {
|
|
||||||
return process.cwd();
|
|
||||||
}
|
|
||||||
|
|
||||||
static getUserExtensionsDir(): string {
|
static getUserExtensionsDir(): string {
|
||||||
const storage = new Storage(os.homedir());
|
const storage = new Storage(os.homedir());
|
||||||
return storage.getExtensionsDir();
|
return storage.getExtensionsDir();
|
||||||
|
|
@ -169,7 +167,9 @@ export function loadExtension(extensionDir: string): Extension | null {
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Warning: error parsing extension config in ${configFilePath}: ${e}`,
|
`Warning: error parsing extension config in ${configFilePath}: ${getErrorMessage(
|
||||||
|
e,
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -197,17 +197,28 @@ function getContextFileNames(config: ExtensionConfig): string[] {
|
||||||
return config.contextFileName;
|
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 in list of disabled extensions in settings.
|
||||||
|
* @param extensions The base list of extensions.
|
||||||
|
* @param enabledExtensionNames The names of explicitly enabled extensions.
|
||||||
|
* @param workspaceDir The current workspace directory.
|
||||||
|
*/
|
||||||
export function annotateActiveExtensions(
|
export function annotateActiveExtensions(
|
||||||
extensions: Extension[],
|
extensions: Extension[],
|
||||||
enabledExtensionNames: string[],
|
enabledExtensionNames: string[],
|
||||||
|
workspaceDir: string,
|
||||||
): GeminiCLIExtension[] {
|
): GeminiCLIExtension[] {
|
||||||
|
const settings = loadSettings(workspaceDir).merged;
|
||||||
|
const disabledExtensions = settings.extensions?.disabled ?? [];
|
||||||
|
|
||||||
const annotatedExtensions: GeminiCLIExtension[] = [];
|
const annotatedExtensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
if (enabledExtensionNames.length === 0) {
|
if (enabledExtensionNames.length === 0) {
|
||||||
return extensions.map((extension) => ({
|
return extensions.map((extension) => ({
|
||||||
name: extension.config.name,
|
name: extension.config.name,
|
||||||
version: extension.config.version,
|
version: extension.config.version,
|
||||||
isActive: true,
|
isActive: !disabledExtensions.includes(extension.config.name),
|
||||||
path: extension.path,
|
path: extension.path,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -286,6 +297,7 @@ async function copyExtension(
|
||||||
|
|
||||||
export async function installExtension(
|
export async function installExtension(
|
||||||
installMetadata: ExtensionInstallMetadata,
|
installMetadata: ExtensionInstallMetadata,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||||
await fs.promises.mkdir(extensionsDir, { recursive: true });
|
await fs.promises.mkdir(extensionsDir, { recursive: true });
|
||||||
|
|
@ -295,10 +307,7 @@ export async function installExtension(
|
||||||
installMetadata.type === 'local' &&
|
installMetadata.type === 'local' &&
|
||||||
!path.isAbsolute(installMetadata.source)
|
!path.isAbsolute(installMetadata.source)
|
||||||
) {
|
) {
|
||||||
installMetadata.source = path.resolve(
|
installMetadata.source = path.resolve(cwd, installMetadata.source);
|
||||||
process.cwd(),
|
|
||||||
installMetadata.source,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let localSourcePath: string;
|
let localSourcePath: string;
|
||||||
|
|
@ -349,7 +358,10 @@ export async function installExtension(
|
||||||
return newExtensionName;
|
return newExtensionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallExtension(extensionName: string): Promise<void> {
|
export async function uninstallExtension(
|
||||||
|
extensionName: string,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
): Promise<void> {
|
||||||
const installedExtensions = loadUserExtensions();
|
const installedExtensions = loadUserExtensions();
|
||||||
if (
|
if (
|
||||||
!installedExtensions.some(
|
!installedExtensions.some(
|
||||||
|
|
@ -358,6 +370,11 @@ export async function uninstallExtension(extensionName: string): Promise<void> {
|
||||||
) {
|
) {
|
||||||
throw new Error(`Extension "${extensionName}" not found.`);
|
throw new Error(`Extension "${extensionName}" not found.`);
|
||||||
}
|
}
|
||||||
|
removeFromDisabledExtensions(
|
||||||
|
extensionName,
|
||||||
|
[SettingScope.User, SettingScope.Workspace],
|
||||||
|
cwd,
|
||||||
|
);
|
||||||
const storage = new ExtensionStorage(extensionName);
|
const storage = new ExtensionStorage(extensionName);
|
||||||
return await fs.promises.rm(storage.getExtensionDir(), {
|
return await fs.promises.rm(storage.getExtensionDir(), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
|
|
@ -394,6 +411,7 @@ export function toOutputString(extension: Extension): string {
|
||||||
|
|
||||||
export async function updateExtension(
|
export async function updateExtension(
|
||||||
extensionName: string,
|
extensionName: string,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
): Promise<ExtensionUpdateInfo | undefined> {
|
): Promise<ExtensionUpdateInfo | undefined> {
|
||||||
const installedExtensions = loadUserExtensions();
|
const installedExtensions = loadUserExtensions();
|
||||||
const extension = installedExtensions.find(
|
const extension = installedExtensions.find(
|
||||||
|
|
@ -413,8 +431,8 @@ export async function updateExtension(
|
||||||
const tempDir = await ExtensionStorage.createTmpDir();
|
const tempDir = await ExtensionStorage.createTmpDir();
|
||||||
try {
|
try {
|
||||||
await copyExtension(extension.path, tempDir);
|
await copyExtension(extension.path, tempDir);
|
||||||
await uninstallExtension(extensionName);
|
await uninstallExtension(extensionName, cwd);
|
||||||
await installExtension(extension.installMetadata);
|
await installExtension(extension.installMetadata, cwd);
|
||||||
|
|
||||||
const updatedExtension = loadExtension(extension.path);
|
const updatedExtension = loadExtension(extension.path);
|
||||||
if (!updatedExtension) {
|
if (!updatedExtension) {
|
||||||
|
|
@ -426,10 +444,57 @@ export async function updateExtension(
|
||||||
updatedVersion,
|
updatedVersion,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error updating extension, rolling back. ${e}`);
|
console.error(
|
||||||
|
`Error updating extension, rolling back. ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
await copyExtension(tempDir, extension.path);
|
await copyExtension(tempDir, extension.path);
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function disableExtension(
|
||||||
|
name: string,
|
||||||
|
scope: SettingScope,
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
) {
|
||||||
|
if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) {
|
||||||
|
throw new Error('System and SystemDefaults scopes are not supported.');
|
||||||
|
}
|
||||||
|
const settings = loadSettings(cwd);
|
||||||
|
const settingsFile = settings.forScope(scope);
|
||||||
|
const extensionSettings = settingsFile.settings.extensions || {
|
||||||
|
disabled: [],
|
||||||
|
};
|
||||||
|
const disabledExtensions = extensionSettings.disabled || [];
|
||||||
|
if (!disabledExtensions.includes(name)) {
|
||||||
|
disabledExtensions.push(name);
|
||||||
|
extensionSettings.disabled = disabledExtensions;
|
||||||
|
settings.setValue(scope, 'extensions', extensionSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an extension from the list of disabled extensions.
|
||||||
|
* @param name The name of the extension to remove.
|
||||||
|
* @param scope The scopes to remove the name from.
|
||||||
|
*/
|
||||||
|
function removeFromDisabledExtensions(
|
||||||
|
name: string,
|
||||||
|
scopes: SettingScope[],
|
||||||
|
cwd: string = process.cwd(),
|
||||||
|
) {
|
||||||
|
const settings = loadSettings(cwd);
|
||||||
|
for (const scope of scopes) {
|
||||||
|
const settingsFile = settings.forScope(scope);
|
||||||
|
const extensionSettings = settingsFile.settings.extensions || {
|
||||||
|
disabled: [],
|
||||||
|
};
|
||||||
|
const disabledExtensions = extensionSettings.disabled || [];
|
||||||
|
extensionSettings.disabled = disabledExtensions.filter(
|
||||||
|
(extension) => extension !== name,
|
||||||
|
);
|
||||||
|
settings.setValue(scope, 'extensions', extensionSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,26 @@ export const SETTINGS_SCHEMA = {
|
||||||
description: 'Enable extension management features.',
|
description: 'Enable extension management features.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
extensions: {
|
||||||
|
type: 'object',
|
||||||
|
label: 'Extensions',
|
||||||
|
category: 'Extensions',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: {},
|
||||||
|
description: 'Settings for extensions.',
|
||||||
|
showInDialog: false,
|
||||||
|
properties: {
|
||||||
|
disabled: {
|
||||||
|
type: 'array',
|
||||||
|
label: 'Disabled Extensions',
|
||||||
|
category: 'Extensions',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: [] as string[],
|
||||||
|
description: 'List of disabled extensions.',
|
||||||
|
showInDialog: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
skipNextSpeakerCheck: {
|
skipNextSpeakerCheck: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Skip Next Speaker Check',
|
label: 'Skip Next Speaker Check',
|
||||||
|
|
|
||||||
12
packages/cli/src/utils/errors.ts
Normal file
12
packages/cli/src/utils/errors.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue