mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0
This commit is contained in:
32
packages/cli/src/commands/extensions.tsx
Normal file
32
packages/cli/src/commands/extensions.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { installCommand } from './extensions/install.js';
|
||||
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 <command>',
|
||||
describe: 'Manage Gemini CLI extensions.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(installCommand)
|
||||
.command(uninstallCommand)
|
||||
.command(listCommand)
|
||||
.command(updateCommand)
|
||||
.command(disableCommand)
|
||||
.command(enableCommand)
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
handler: () => {
|
||||
// This handler is not called when a subcommand is provided.
|
||||
// Yargs will show the help menu.
|
||||
},
|
||||
};
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
59
packages/cli/src/commands/extensions/enable.ts
Normal file
59
packages/cli/src/commands/extensions/enable.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type CommandModule } from 'yargs';
|
||||
import { FatalConfigError, getErrorMessage } from '@qwen-code/qwen-code-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] <name>',
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
31
packages/cli/src/commands/extensions/install.test.ts
Normal file
31
packages/cli/src/commands/extensions/install.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { installCommand } from './install.js';
|
||||
import yargs from 'yargs';
|
||||
|
||||
describe('extensions install command', () => {
|
||||
it('should fail if no source is provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.locale('en')
|
||||
.command(installCommand)
|
||||
.fail(false);
|
||||
expect(() => validationParser.parse('install')).toThrow(
|
||||
'Either a git URL --source or a --path must be provided.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if both git source and local path are provided', () => {
|
||||
const validationParser = yargs([])
|
||||
.locale('en')
|
||||
.command(installCommand)
|
||||
.fail(false);
|
||||
expect(() =>
|
||||
validationParser.parse('install --source some-url --path /some/path'),
|
||||
).toThrow('Arguments source and path are mutually exclusive');
|
||||
});
|
||||
});
|
||||
64
packages/cli/src/commands/extensions/install.ts
Normal file
64
packages/cli/src/commands/extensions/install.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
installExtension,
|
||||
type ExtensionInstallMetadata,
|
||||
} from '../../config/extension.js';
|
||||
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
interface InstallArgs {
|
||||
source?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
try {
|
||||
const installMetadata: ExtensionInstallMetadata = {
|
||||
source: (args.source || args.path) as string,
|
||||
type: args.source ? 'git' : 'local',
|
||||
};
|
||||
const extensionName = await installExtension(installMetadata);
|
||||
console.log(
|
||||
`Extension "${extensionName}" installed successfully and enabled.`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const installCommand: CommandModule = {
|
||||
command: 'install [--source | --path ]',
|
||||
describe: 'Installs an extension from a git repository or a local path.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.option('source', {
|
||||
describe: 'The git URL of the extension to install.',
|
||||
type: 'string',
|
||||
})
|
||||
.option('path', {
|
||||
describe: 'Path to a local extension directory.',
|
||||
type: 'string',
|
||||
})
|
||||
.conflicts('source', 'path')
|
||||
.check((argv) => {
|
||||
if (!argv.source && !argv.path) {
|
||||
throw new Error(
|
||||
'Either a git URL --source or a --path must be provided.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleInstall({
|
||||
source: argv['source'] as string | undefined,
|
||||
path: argv['path'] as string | undefined,
|
||||
});
|
||||
},
|
||||
};
|
||||
36
packages/cli/src/commands/extensions/list.ts
Normal file
36
packages/cli/src/commands/extensions/list.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadUserExtensions, toOutputString } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
export async function handleList() {
|
||||
try {
|
||||
const extensions = loadUserExtensions();
|
||||
if (extensions.length === 0) {
|
||||
console.log('No extensions installed.');
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
extensions
|
||||
.map((extension, _): string => toOutputString(extension))
|
||||
.join('\n\n'),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const listCommand: CommandModule = {
|
||||
command: 'list',
|
||||
describe: 'Lists installed extensions.',
|
||||
builder: (yargs) => yargs,
|
||||
handler: async () => {
|
||||
await handleList();
|
||||
},
|
||||
};
|
||||
21
packages/cli/src/commands/extensions/uninstall.test.ts
Normal file
21
packages/cli/src/commands/extensions/uninstall.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @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([])
|
||||
.locale('en')
|
||||
.command(uninstallCommand)
|
||||
.fail(false);
|
||||
expect(() => validationParser.parse('uninstall')).toThrow(
|
||||
'Not enough non-option arguments: got 0, need at least 1',
|
||||
);
|
||||
});
|
||||
});
|
||||
47
packages/cli/src/commands/extensions/uninstall.ts
Normal file
47
packages/cli/src/commands/extensions/uninstall.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { uninstallExtension } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.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(getErrorMessage(error));
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
47
packages/cli/src/commands/extensions/update.ts
Normal file
47
packages/cli/src/commands/extensions/update.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { updateExtension } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
|
||||
interface UpdateArgs {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function handleUpdate(args: UpdateArgs) {
|
||||
try {
|
||||
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
|
||||
const updatedExtensionInfo = await updateExtension(args.name);
|
||||
if (!updatedExtensionInfo) {
|
||||
console.log(`Extension "${args.name}" failed to update.`);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion} → ${updatedExtensionInfo.updatedVersion}.`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(getErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const updateCommand: CommandModule = {
|
||||
command: 'update <name>',
|
||||
describe: 'Updates an extension.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'The name of the extension to update.',
|
||||
type: 'string',
|
||||
})
|
||||
.check((_argv) => true),
|
||||
handler: async (argv) => {
|
||||
await handleUpdate({
|
||||
name: argv['name'] as string,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
// File for 'gemini mcp add' command
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadSettings, SettingScope } from '../../config/settings.js';
|
||||
import { MCPServerConfig } from '@qwen-code/qwen-code-core';
|
||||
import type { MCPServerConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
async function addMcpServer(
|
||||
name: string,
|
||||
|
||||
@@ -11,9 +11,27 @@ import { loadExtensions } from '../../config/extension.js';
|
||||
import { createTransport } from '@qwen-code/qwen-code-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../config/extension.js');
|
||||
vi.mock('@qwen-code/qwen-code-core');
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
loadSettings: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../config/extension.js', () => ({
|
||||
loadExtensions: vi.fn(),
|
||||
}));
|
||||
vi.mock('@qwen-code/qwen-code-core', () => ({
|
||||
createTransport: vi.fn(),
|
||||
MCPServerStatus: {
|
||||
CONNECTED: 'CONNECTED',
|
||||
CONNECTING: 'CONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
},
|
||||
Storage: vi.fn().mockImplementation((_cwd: string) => ({
|
||||
getGlobalSettingsPath: () => '/tmp/qwen/settings.json',
|
||||
getWorkspaceSettingsPath: () => '/tmp/qwen/workspace-settings.json',
|
||||
getProjectTempDir: () => '/test/home/.qwen/tmp/mocked_hash',
|
||||
})),
|
||||
GEMINI_CONFIG_DIR: '.qwen',
|
||||
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
|
||||
}));
|
||||
vi.mock('@modelcontextprotocol/sdk/client/index.js');
|
||||
|
||||
const mockedLoadSettings = loadSettings as vi.Mock;
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
// File for 'gemini mcp list' command
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import {
|
||||
MCPServerConfig,
|
||||
MCPServerStatus,
|
||||
createTransport,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type { MCPServerConfig } from '@qwen-code/qwen-code-core';
|
||||
import { MCPServerStatus, createTransport } from '@qwen-code/qwen-code-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { loadExtensions } from '../../config/extension.js';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user