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 { installCommand } from './extensions/install.js';
|
||||
import { uninstallCommand } from './extensions/uninstall.js';
|
||||
|
||||
export const extensionsCommand: CommandModule = {
|
||||
command: 'extensions <command>',
|
||||
@@ -13,6 +14,7 @@ export const extensionsCommand: CommandModule = {
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(installCommand)
|
||||
.command(uninstallCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
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,
|
||||
installExtension,
|
||||
loadExtensions,
|
||||
uninstallExtension,
|
||||
} from './extension.js';
|
||||
import { execSync } from 'child_process';
|
||||
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(
|
||||
extensionsDir: string,
|
||||
name: string,
|
||||
|
||||
@@ -338,3 +338,19 @@ export async function installExtension(
|
||||
|
||||
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