mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +00:00
Merge branch 'main' into feat/debug-logging-refactor
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
commit
135df54f27
378 changed files with 40051 additions and 6776 deletions
|
|
@ -5,8 +5,16 @@
|
|||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { extensionConsentString, requestConsentOrFail } from './consent.js';
|
||||
import type { ExtensionConfig } from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
extensionConsentString,
|
||||
requestConsentOrFail,
|
||||
requestChoicePluginNonInteractive,
|
||||
} from './consent.js';
|
||||
import type {
|
||||
ExtensionConfig,
|
||||
ClaudeMarketplaceConfig,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import prompts from 'prompts';
|
||||
|
||||
vi.mock('../../i18n/index.js', () => ({
|
||||
t: vi.fn((str: string, params?: Record<string, string>) => {
|
||||
|
|
@ -20,6 +28,8 @@ vi.mock('../../i18n/index.js', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
vi.mock('prompts');
|
||||
|
||||
describe('extensionConsentString', () => {
|
||||
it('should include extension name', () => {
|
||||
const config: ExtensionConfig = {
|
||||
|
|
@ -241,3 +251,72 @@ describe('requestConsentOrFail', () => {
|
|||
expect(mockRequestConsent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestChoicePluginNonInteractive', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw error when plugins array is empty', async () => {
|
||||
const marketplace: ClaudeMarketplaceConfig = {
|
||||
name: 'test-marketplace',
|
||||
owner: { name: 'Test Owner', email: 'test@example.com' },
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
await expect(
|
||||
requestChoicePluginNonInteractive(marketplace),
|
||||
).rejects.toThrow('No plugins available in this marketplace.');
|
||||
});
|
||||
|
||||
it('should return selected plugin name', async () => {
|
||||
const marketplace: ClaudeMarketplaceConfig = {
|
||||
name: 'test-marketplace',
|
||||
owner: { name: 'Test Owner', email: 'test@example.com' },
|
||||
plugins: [
|
||||
{
|
||||
name: 'plugin1',
|
||||
description: 'Plugin 1',
|
||||
version: '1.0.0',
|
||||
source: 'src1',
|
||||
},
|
||||
{
|
||||
name: 'plugin2',
|
||||
description: 'Plugin 2',
|
||||
version: '1.0.0',
|
||||
source: 'src2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
vi.mocked(prompts).mockResolvedValueOnce({ plugin: 'plugin2' });
|
||||
|
||||
const result = await requestChoicePluginNonInteractive(marketplace);
|
||||
|
||||
expect(result).toBe('plugin2');
|
||||
expect(prompts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'select',
|
||||
name: 'plugin',
|
||||
choices: expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'plugin1' }),
|
||||
expect.objectContaining({ value: 'plugin2' }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when selection is cancelled', async () => {
|
||||
const marketplace: ClaudeMarketplaceConfig = {
|
||||
name: 'test-marketplace',
|
||||
owner: { name: 'Test Owner', email: 'test@example.com' },
|
||||
plugins: [{ name: 'plugin1', version: '1.0.0', source: 'src1' }],
|
||||
};
|
||||
|
||||
vi.mocked(prompts).mockResolvedValueOnce({ plugin: undefined });
|
||||
|
||||
await expect(
|
||||
requestChoicePluginNonInteractive(marketplace),
|
||||
).rejects.toThrow('Plugin selection cancelled.');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type {
|
||||
ClaudeMarketplaceConfig,
|
||||
ExtensionConfig,
|
||||
ExtensionRequestOptions,
|
||||
SkillConfig,
|
||||
|
|
@ -6,6 +7,7 @@ import type {
|
|||
} from '@qwen-code/qwen-code-core';
|
||||
import type { ConfirmationRequest } from '../../ui/types.js';
|
||||
import chalk from 'chalk';
|
||||
import prompts from 'prompts';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { writeStdoutLine } from '../../utils/stdioHelpers.js';
|
||||
|
||||
|
|
@ -28,6 +30,49 @@ export async function requestConsentNonInteractive(
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests plugin selection from the user in non-interactive mode.
|
||||
* Displays an interactive list with arrow key navigation.
|
||||
*
|
||||
* This should not be called from interactive mode as it will break the CLI.
|
||||
*
|
||||
* @param marketplace The marketplace config containing available plugins.
|
||||
* @returns The name of the selected plugin.
|
||||
*/
|
||||
export async function requestChoicePluginNonInteractive(
|
||||
marketplace: ClaudeMarketplaceConfig,
|
||||
): Promise<string> {
|
||||
const plugins = marketplace.plugins;
|
||||
|
||||
if (plugins.length === 0) {
|
||||
throw new Error(t('No plugins available in this marketplace.'));
|
||||
}
|
||||
|
||||
// Build choices for prompts select
|
||||
|
||||
const choices = plugins.map((plugin) => ({
|
||||
title: chalk.green(chalk.bold(`[${plugin.name}]`)),
|
||||
value: plugin.name,
|
||||
}));
|
||||
|
||||
const response = await prompts({
|
||||
type: 'select',
|
||||
name: 'plugin',
|
||||
message: t('Select a plugin to install from marketplace "{{name}}":', {
|
||||
name: marketplace.name,
|
||||
}),
|
||||
choices,
|
||||
initial: 0,
|
||||
});
|
||||
|
||||
// Handle cancellation (Ctrl+C)
|
||||
if (response.plugin === undefined) {
|
||||
throw new Error(t('Plugin selection cancelled.'));
|
||||
}
|
||||
|
||||
return response.plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests consent from the user to perform an action, in interactive mode.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ vi.mock('@qwen-code/qwen-code-core', () => ({
|
|||
vi.mock('./consent.js', () => ({
|
||||
requestConsentNonInteractive: mockRequestConsentNonInteractive,
|
||||
requestConsentOrFail: mockRequestConsentOrFail,
|
||||
requestChoicePluginNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../config/trustedFolders.js', () => ({
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { loadSettings } from '../../config/settings.js';
|
|||
import {
|
||||
requestConsentOrFail,
|
||||
requestConsentNonInteractive,
|
||||
requestChoicePluginNonInteractive,
|
||||
} from './consent.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ export async function handleInstall(args: InstallArgs) {
|
|||
loadSettings(workspaceDir).merged,
|
||||
),
|
||||
requestConsent,
|
||||
requestChoicePlugin: requestChoicePluginNonInteractive,
|
||||
});
|
||||
await extensionManager.refreshCache();
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ vi.mock('../../config/trustedFolders.js', () => ({
|
|||
vi.mock('./consent.js', () => ({
|
||||
requestConsentOrFail: vi.fn(),
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
requestChoicePluginNonInteractive: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getExtensionManager', () => {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import { loadSettings } from '../../config/settings.js';
|
|||
import {
|
||||
requestConsentOrFail,
|
||||
requestConsentNonInteractive,
|
||||
requestChoicePluginNonInteractive,
|
||||
} from './consent.js';
|
||||
import { isWorkspaceTrusted } from '../../config/trustedFolders.js';
|
||||
import * as os from 'node:os';
|
||||
import chalk from 'chalk';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export async function getExtensionManager(): Promise<ExtensionManager> {
|
||||
const workspaceDir = process.cwd();
|
||||
|
|
@ -22,6 +24,7 @@ export async function getExtensionManager(): Promise<ExtensionManager> {
|
|||
null,
|
||||
requestConsentNonInteractive,
|
||||
),
|
||||
requestChoicePlugin: requestChoicePluginNonInteractive,
|
||||
isWorkspaceTrusted: !!isWorkspaceTrusted(loadSettings(workspaceDir).merged),
|
||||
});
|
||||
await extensionManager.refreshCache();
|
||||
|
|
@ -46,32 +49,44 @@ export function extensionToOutputString(
|
|||
|
||||
const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗');
|
||||
let output = `${inline ? '' : status} ${extension.config.name} (${extension.config.version})`;
|
||||
output += `\n Path: ${extension.path}`;
|
||||
output += `\n ${t('Path:')} ${extension.path}`;
|
||||
if (extension.installMetadata) {
|
||||
output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`;
|
||||
output += `\n ${t('Source:')} ${extension.installMetadata.source} (${t('Type:')} ${extension.installMetadata.type})`;
|
||||
if (extension.installMetadata.ref) {
|
||||
output += `\n Ref: ${extension.installMetadata.ref}`;
|
||||
output += `\n ${t('Ref:')} ${extension.installMetadata.ref}`;
|
||||
}
|
||||
if (extension.installMetadata.releaseTag) {
|
||||
output += `\n Release tag: ${extension.installMetadata.releaseTag}`;
|
||||
output += `\n ${t('Release tag:')} ${extension.installMetadata.releaseTag}`;
|
||||
}
|
||||
}
|
||||
output += `\n Enabled (User): ${userEnabled}`;
|
||||
output += `\n Enabled (Workspace): ${workspaceEnabled}`;
|
||||
output += `\n ${t('Enabled (User):')} ${userEnabled}`;
|
||||
output += `\n ${t('Enabled (Workspace):')} ${workspaceEnabled}`;
|
||||
if (extension.contextFiles.length > 0) {
|
||||
output += `\n Context files:`;
|
||||
output += `\n ${t('Context files:')}`;
|
||||
extension.contextFiles.forEach((contextFile) => {
|
||||
output += `\n ${contextFile}`;
|
||||
});
|
||||
}
|
||||
if (extension.commands && extension.commands.length > 0) {
|
||||
output += `\n Commands:`;
|
||||
output += `\n ${t('Commands:')}`;
|
||||
extension.commands.forEach((command) => {
|
||||
output += `\n /${command}`;
|
||||
});
|
||||
}
|
||||
if (extension.skills && extension.skills.length > 0) {
|
||||
output += `\n ${t('Skills:')}`;
|
||||
extension.skills.forEach((skill) => {
|
||||
output += `\n ${skill.name}`;
|
||||
});
|
||||
}
|
||||
if (extension.agents && extension.agents.length > 0) {
|
||||
output += `\n ${t('Agents:')}`;
|
||||
extension.agents.forEach((agent) => {
|
||||
output += `\n ${agent.name}`;
|
||||
});
|
||||
}
|
||||
if (extension.config.mcpServers) {
|
||||
output += `\n MCP servers:`;
|
||||
output += `\n ${t('MCP servers:')}`;
|
||||
Object.keys(extension.config.mcpServers).forEach((key) => {
|
||||
output += `\n ${key}`;
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue