mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
[extensions] Add extensions uninstall command (#6877)
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { CommandModule } from 'yargs';
|
import { CommandModule } from 'yargs';
|
||||||
import { installCommand } from './extensions/install.js';
|
import { installCommand } from './extensions/install.js';
|
||||||
|
import { uninstallCommand } from './extensions/uninstall.js';
|
||||||
|
|
||||||
export const extensionsCommand: CommandModule = {
|
export const extensionsCommand: CommandModule = {
|
||||||
command: 'extensions <command>',
|
command: 'extensions <command>',
|
||||||
@@ -13,6 +14,7 @@ export const extensionsCommand: CommandModule = {
|
|||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs
|
yargs
|
||||||
.command(installCommand)
|
.command(installCommand)
|
||||||
|
.command(uninstallCommand)
|
||||||
.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: () => {
|
||||||
|
|||||||
18
packages/cli/src/commands/extensions/uninstall.test.ts
Normal file
18
packages/cli/src/commands/extensions/uninstall.test.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { uninstallCommand } from './uninstall.js';
|
||||||
|
import yargs from 'yargs';
|
||||||
|
|
||||||
|
describe('extensions uninstall command', () => {
|
||||||
|
it('should fail if no source is provided', () => {
|
||||||
|
const validationParser = yargs([]).command(uninstallCommand).fail(false);
|
||||||
|
expect(() => validationParser.parse('uninstall')).toThrow(
|
||||||
|
'Not enough non-option arguments: got 0, need at least 1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
46
packages/cli/src/commands/extensions/uninstall.ts
Normal file
46
packages/cli/src/commands/extensions/uninstall.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CommandModule } from 'yargs';
|
||||||
|
import { uninstallExtension } from '../../config/extension.js';
|
||||||
|
|
||||||
|
interface UninstallArgs {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUninstall(args: UninstallArgs) {
|
||||||
|
try {
|
||||||
|
await uninstallExtension(args.name);
|
||||||
|
console.log(`Extension "${args.name}" successfully uninstalled.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error((error as Error).message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uninstallCommand: CommandModule = {
|
||||||
|
command: 'uninstall <name>',
|
||||||
|
describe: 'Uninstalls an extension.',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional('name', {
|
||||||
|
describe: 'The name of the extension to uninstall.',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
.check((argv) => {
|
||||||
|
if (!argv.name) {
|
||||||
|
throw new Error(
|
||||||
|
'Please include the name of the extension to uninstall as a positional argument.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
handler: async (argv) => {
|
||||||
|
await handleUninstall({
|
||||||
|
name: argv['name'] as string,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
annotateActiveExtensions,
|
annotateActiveExtensions,
|
||||||
installExtension,
|
installExtension,
|
||||||
loadExtensions,
|
loadExtensions,
|
||||||
|
uninstallExtension,
|
||||||
} from './extension.js';
|
} from './extension.js';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { SimpleGit, simpleGit } from 'simple-git';
|
import { SimpleGit, simpleGit } from 'simple-git';
|
||||||
@@ -280,6 +281,65 @@ describe('installExtension', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('uninstallExtension', () => {
|
||||||
|
let tempHomeDir: string;
|
||||||
|
let userExtensionsDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempHomeDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||||
|
);
|
||||||
|
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
|
||||||
|
userExtensionsDir = path.join(tempHomeDir, '.gemini', 'extensions');
|
||||||
|
// Clean up before each test
|
||||||
|
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||||
|
|
||||||
|
vi.mocked(execSync).mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should uninstall an extension by name', async () => {
|
||||||
|
const sourceExtDir = createExtension(
|
||||||
|
userExtensionsDir,
|
||||||
|
'my-local-extension',
|
||||||
|
'1.0.0',
|
||||||
|
);
|
||||||
|
|
||||||
|
await uninstallExtension('my-local-extension');
|
||||||
|
|
||||||
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should uninstall an extension by name and retain existing extensions', async () => {
|
||||||
|
const sourceExtDir = createExtension(
|
||||||
|
userExtensionsDir,
|
||||||
|
'my-local-extension',
|
||||||
|
'1.0.0',
|
||||||
|
);
|
||||||
|
const otherExtDir = createExtension(
|
||||||
|
userExtensionsDir,
|
||||||
|
'other-extension',
|
||||||
|
'1.0.0',
|
||||||
|
);
|
||||||
|
|
||||||
|
await uninstallExtension('my-local-extension');
|
||||||
|
|
||||||
|
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
||||||
|
expect(loadExtensions(tempHomeDir)).toHaveLength(1);
|
||||||
|
expect(fs.existsSync(otherExtDir)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the extension does not exist', async () => {
|
||||||
|
await expect(uninstallExtension('nonexistent-extension')).rejects.toThrow(
|
||||||
|
'Error: Extension "nonexistent-extension" not found.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function createExtension(
|
function createExtension(
|
||||||
extensionsDir: string,
|
extensionsDir: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
|||||||
@@ -338,3 +338,19 @@ export async function installExtension(
|
|||||||
|
|
||||||
return newExtensionName;
|
return newExtensionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uninstallExtension(extensionName: string): Promise<void> {
|
||||||
|
const installedExtensions = loadUserExtensions();
|
||||||
|
if (
|
||||||
|
!installedExtensions.some(
|
||||||
|
(installed) => installed.config.name === extensionName,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(`Error: Extension "${extensionName}" not found.`);
|
||||||
|
}
|
||||||
|
const storage = new ExtensionStorage(extensionName);
|
||||||
|
return await fs.promises.rm(storage.getExtensionDir(), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user