Merge branch 'main' into feat/debug-logging-refactor

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-01 20:47:38 +08:00
commit 135df54f27
378 changed files with 40051 additions and 6776 deletions

View file

@ -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.');
});
});

View file

@ -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.
*

View file

@ -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', () => ({

View file

@ -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();

View file

@ -32,6 +32,7 @@ vi.mock('../../config/trustedFolders.js', () => ({
vi.mock('./consent.js', () => ({
requestConsentOrFail: vi.fn(),
requestConsentNonInteractive: vi.fn(),
requestChoicePluginNonInteractive: vi.fn(),
}));
describe('getExtensionManager', () => {

View file

@ -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}`;
});