Merge branch 'main' into feat/support-permission

This commit is contained in:
LaZzyMan 2026-03-19 11:52:24 +08:00
commit 598c438dde
16 changed files with 2160 additions and 3 deletions

View file

@ -0,0 +1,77 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule, Argv } from 'yargs';
import {
handleQwenAuth,
runInteractiveAuth,
showAuthStatus,
} from './auth/handler.js';
import { t } from '../i18n/index.js';
// Define subcommands separately
const qwenOauthCommand = {
command: 'qwen-oauth',
describe: t('Authenticate using Qwen OAuth'),
handler: async () => {
await handleQwenAuth('qwen-oauth', {});
},
};
const codePlanCommand = {
command: 'coding-plan',
describe: t('Authenticate using Alibaba Cloud Coding Plan'),
builder: (yargs: Argv) =>
yargs
.option('region', {
alias: 'r',
describe: t('Region for Coding Plan (china/global)'),
type: 'string',
})
.option('key', {
alias: 'k',
describe: t('API key for Coding Plan'),
type: 'string',
}),
handler: async (argv: { region?: string; key?: string }) => {
const region = argv['region'] as string | undefined;
const key = argv['key'] as string | undefined;
// If region and key are provided, use them directly
if (region && key) {
await handleQwenAuth('coding-plan', { region, key });
} else {
// Otherwise, prompt interactively
await handleQwenAuth('coding-plan', {});
}
},
};
const statusCommand = {
command: 'status',
describe: t('Show current authentication status'),
handler: async () => {
await showAuthStatus();
},
};
export const authCommand: CommandModule = {
command: 'auth',
describe: t(
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan',
),
builder: (yargs: Argv) =>
yargs
.command(qwenOauthCommand)
.command(codePlanCommand)
.command(statusCommand)
.demandCommand(0) // Don't require a subcommand
.version(false),
handler: async () => {
// This handler is for when no subcommand is provided - show interactive menu
await runInteractiveAuth();
},
};

View file

@ -0,0 +1,500 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
AuthType,
getErrorMessage,
type Config,
type ProviderModelConfig as ModelConfig,
} from '@qwen-code/qwen-code-core';
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
import { t } from '../../i18n/index.js';
import {
getCodingPlanConfig,
isCodingPlanConfig,
CodingPlanRegion,
CODING_PLAN_ENV_KEY,
} from '../../constants/codingPlan.js';
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
import { backupSettingsFile } from '../../utils/settingsUtils.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { loadCliConfig } from '../../config/config.js';
import type { CliArgs } from '../../config/config.js';
import { InteractiveSelector } from './interactiveSelector.js';
interface QwenAuthOptions {
region?: string;
key?: string;
}
interface CodingPlanSettings {
region?: CodingPlanRegion;
version?: string;
}
interface MergedSettingsWithCodingPlan {
security?: {
auth?: {
selectedType?: string;
};
};
codingPlan?: CodingPlanSettings;
model?: {
name?: string;
};
modelProviders?: Record<string, ModelConfig[]>;
env?: Record<string, string>;
}
/**
* Handles the authentication process based on the specified command and options
*/
export async function handleQwenAuth(
command: 'qwen-oauth' | 'coding-plan',
options: QwenAuthOptions,
) {
try {
const settings = loadSettings();
// Create a minimal argv for config loading
const minimalArgv: CliArgs = {
query: undefined,
model: undefined,
sandbox: undefined,
sandboxImage: undefined,
debug: undefined,
prompt: undefined,
promptInteractive: undefined,
yolo: undefined,
approvalMode: undefined,
telemetry: undefined,
checkpointing: undefined,
telemetryTarget: undefined,
telemetryOtlpEndpoint: undefined,
telemetryOtlpProtocol: undefined,
telemetryLogPrompts: undefined,
telemetryOutfile: undefined,
allowedMcpServerNames: undefined,
allowedTools: undefined,
acp: undefined,
experimentalAcp: undefined,
experimentalLsp: undefined,
experimentalHooks: undefined,
extensions: [],
listExtensions: undefined,
openaiLogging: undefined,
openaiApiKey: undefined,
openaiBaseUrl: undefined,
openaiLoggingDir: undefined,
proxy: undefined,
includeDirectories: undefined,
tavilyApiKey: undefined,
googleApiKey: undefined,
googleSearchEngineId: undefined,
webSearchDefault: undefined,
screenReader: undefined,
inputFormat: undefined,
outputFormat: undefined,
includePartialMessages: undefined,
chatRecording: undefined,
continue: undefined,
resume: undefined,
sessionId: undefined,
maxSessionTurns: undefined,
coreTools: undefined,
excludeTools: undefined,
authType: undefined,
channel: undefined,
systemPrompt: undefined,
appendSystemPrompt: undefined,
};
// Create a minimal config to access settings and storage
const config = await loadCliConfig(
settings.merged,
minimalArgv,
process.cwd(),
[], // No extensions for auth command
);
if (command === 'qwen-oauth') {
await handleQwenOAuth(config, settings);
} else if (command === 'coding-plan') {
await handleCodePlanAuth(config, settings, options);
}
// Exit after authentication is complete
writeStdoutLine(t('Authentication completed successfully.'));
process.exit(0);
} catch (error) {
writeStderrLine(getErrorMessage(error));
process.exit(1);
}
}
/**
* Handles Qwen OAuth authentication
*/
async function handleQwenOAuth(
config: Config,
settings: LoadedSettings,
): Promise<void> {
writeStdoutLine(t('Starting Qwen OAuth authentication...'));
try {
await config.refreshAuth(AuthType.QWEN_OAUTH);
// Persist the auth type
const authTypeScope = getPersistScopeForModelSelection(settings);
settings.setValue(
authTypeScope,
'security.auth.selectedType',
AuthType.QWEN_OAUTH,
);
writeStdoutLine(t('Successfully authenticated with Qwen OAuth.'));
process.exit(0);
} catch (error) {
writeStderrLine(
t('Failed to authenticate with Qwen OAuth: {{error}}', {
error: getErrorMessage(error),
}),
);
process.exit(1);
}
}
/**
* Handles Alibaba Cloud Coding Plan authentication
*/
async function handleCodePlanAuth(
config: Config,
settings: LoadedSettings,
options: QwenAuthOptions,
): Promise<void> {
const { region, key } = options;
let selectedRegion: CodingPlanRegion;
let selectedKey: string;
// If region and key are provided as options, use them
if (region && key) {
selectedRegion =
region.toLowerCase() === 'global'
? CodingPlanRegion.GLOBAL
: CodingPlanRegion.CHINA;
selectedKey = key;
} else {
// Otherwise, prompt interactively
selectedRegion = await promptForRegion();
selectedKey = await promptForKey();
}
writeStdoutLine(t('Processing Alibaba Cloud Coding Plan authentication...'));
try {
// Get configuration based on region
const { template, version } = getCodingPlanConfig(selectedRegion);
// Get persist scope
const authTypeScope = getPersistScopeForModelSelection(settings);
// Backup settings file before modification
const settingsFile = settings.forScope(authTypeScope);
backupSettingsFile(settingsFile.path);
// Store api-key in settings.env (unified env key)
settings.setValue(authTypeScope, `env.${CODING_PLAN_ENV_KEY}`, selectedKey);
// Sync to process.env immediately so refreshAuth can read the apiKey
process.env[CODING_PLAN_ENV_KEY] = selectedKey;
// Generate model configs from template
const newConfigs = template.map((templateConfig) => ({
...templateConfig,
envKey: CODING_PLAN_ENV_KEY,
}));
// Get existing configs
const existingConfigs =
(settings.merged.modelProviders as Record<string, ModelConfig[]>)?.[
AuthType.USE_OPENAI
] || [];
// Filter out all existing Coding Plan configs (mutually exclusive)
const nonCodingPlanConfigs = existingConfigs.filter(
(existing) => !isCodingPlanConfig(existing.baseUrl, existing.envKey),
);
// Add new Coding Plan configs at the beginning
const updatedConfigs = [...newConfigs, ...nonCodingPlanConfigs];
// Persist to modelProviders
settings.setValue(
authTypeScope,
`modelProviders.${AuthType.USE_OPENAI}`,
updatedConfigs,
);
// Also persist authType
settings.setValue(
authTypeScope,
'security.auth.selectedType',
AuthType.USE_OPENAI,
);
// Persist coding plan region
settings.setValue(authTypeScope, 'codingPlan.region', selectedRegion);
// Persist coding plan version (single field for backward compatibility)
settings.setValue(authTypeScope, 'codingPlan.version', version);
// If there are configs, use the first one as the model
if (updatedConfigs.length > 0 && updatedConfigs[0]?.id) {
settings.setValue(
authTypeScope,
'model.name',
(updatedConfigs[0] as ModelConfig).id,
);
}
// Refresh auth with the new configuration
await config.refreshAuth(AuthType.USE_OPENAI);
writeStdoutLine(
t('Successfully authenticated with Alibaba Cloud Coding Plan.'),
);
} catch (error) {
writeStderrLine(
t('Failed to authenticate with Coding Plan: {{error}}', {
error: getErrorMessage(error),
}),
);
process.exit(1);
}
}
/**
* Prompts the user to select a region using an interactive selector
*/
async function promptForRegion(): Promise<CodingPlanRegion> {
const selector = new InteractiveSelector(
[
{
value: CodingPlanRegion.CHINA,
label: t('中国 (China)'),
description: t('阿里云百炼 (aliyun.com)'),
},
{
value: CodingPlanRegion.GLOBAL,
label: t('Global'),
description: t('Alibaba Cloud (alibabacloud.com)'),
},
],
t('Select region for Coding Plan:'),
);
return await selector.select();
}
/**
* Prompts the user to enter an API key
*/
async function promptForKey(): Promise<string> {
// Create a simple password-style input (without echoing characters)
const stdin = process.stdin;
const stdout = process.stdout;
stdout.write(t('Enter your Coding Plan API key: '));
// Set raw mode to capture keystrokes
const wasRaw = stdin.isRaw;
if (stdin.setRawMode) {
stdin.setRawMode(true);
}
stdin.resume();
return new Promise<string>((resolve, reject) => {
let input = '';
const onData = (chunk: string) => {
for (const char of chunk) {
switch (char) {
case '\r': // Enter
case '\n':
stdin.removeListener('data', onData);
if (stdin.setRawMode) {
stdin.setRawMode(wasRaw);
}
stdout.write('\n'); // New line after input
resolve(input);
return;
case '\x03': // Ctrl+C
stdin.removeListener('data', onData);
if (stdin.setRawMode) {
stdin.setRawMode(wasRaw);
}
stdout.write('^C\n');
reject(new Error('Interrupted'));
return;
case '\x08': // Backspace
case '\x7F': // Delete
if (input.length > 0) {
input = input.slice(0, -1);
// Move cursor back, print space, move back again
stdout.write('\x1B[D \x1B[D');
}
break;
default:
// Add character to input
input += char;
// Print asterisk instead of the actual character for security
stdout.write('*');
break;
}
}
};
stdin.on('data', onData);
});
}
/**
* Runs the interactive authentication flow
*/
export async function runInteractiveAuth() {
const selector = new InteractiveSelector(
[
{
value: 'qwen-oauth' as const,
label: t('Qwen OAuth'),
description: t('Free · Up to 1,000 requests/day · Qwen latest models'),
},
{
value: 'coding-plan' as const,
label: t('Alibaba Cloud Coding Plan'),
description: t(
'Paid · Up to 6,000 requests/5 hrs · All Alibaba Cloud Coding Plan Models',
),
},
],
t('Select authentication method:'),
);
const choice = await selector.select();
if (choice === 'coding-plan') {
await handleQwenAuth('coding-plan', {});
} else {
await handleQwenAuth('qwen-oauth', {});
}
}
/**
* Shows the current authentication status
*/
export async function showAuthStatus(): Promise<void> {
try {
const settings = loadSettings();
const mergedSettings = settings.merged as MergedSettingsWithCodingPlan;
writeStdoutLine(t('\n=== Authentication Status ===\n'));
// Check for selected auth type
const selectedType = mergedSettings.security?.auth?.selectedType;
if (!selectedType) {
writeStdoutLine(t('⚠️ No authentication method configured.\n'));
writeStdoutLine(t('Run one of the following commands to get started:\n'));
writeStdoutLine(
t(
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)',
),
);
writeStdoutLine(
t(
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n',
),
);
writeStdoutLine(t('Or simply run:'));
writeStdoutLine(
t(' qwen auth - Interactive authentication setup\n'),
);
process.exit(0);
}
// Display status based on auth type
if (selectedType === AuthType.QWEN_OAUTH) {
writeStdoutLine(t('✓ Authentication Method: Qwen OAuth'));
writeStdoutLine(t(' Type: Free tier'));
writeStdoutLine(t(' Limit: Up to 1,000 requests/day'));
writeStdoutLine(t(' Models: Qwen latest models\n'));
} else if (selectedType === AuthType.USE_OPENAI) {
// Check for Coding Plan configuration
const codingPlanRegion = mergedSettings.codingPlan?.region;
const codingPlanVersion = mergedSettings.codingPlan?.version;
const modelName = mergedSettings.model?.name;
// Check if API key is set in environment
const hasApiKey =
!!process.env[CODING_PLAN_ENV_KEY] ||
!!mergedSettings.env?.[CODING_PLAN_ENV_KEY];
if (hasApiKey) {
writeStdoutLine(
t('✓ Authentication Method: Alibaba Cloud Coding Plan'),
);
if (codingPlanRegion) {
const regionDisplay =
codingPlanRegion === CodingPlanRegion.CHINA
? t('中国 (China) - 阿里云百炼')
: t('Global - Alibaba Cloud');
writeStdoutLine(t(' Region: {{region}}', { region: regionDisplay }));
}
if (modelName) {
writeStdoutLine(
t(' Current Model: {{model}}', { model: modelName }),
);
}
if (codingPlanVersion) {
writeStdoutLine(
t(' Config Version: {{version}}', {
version: codingPlanVersion.substring(0, 8) + '...',
}),
);
}
writeStdoutLine(t(' Status: API key configured\n'));
} else {
writeStdoutLine(
t(
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)',
),
);
writeStdoutLine(
t(' Issue: API key not found in environment or settings\n'),
);
writeStdoutLine(t(' Run `qwen auth coding-plan` to re-configure.\n'));
}
} else {
writeStdoutLine(
t('✓ Authentication Method: {{type}}', { type: selectedType }),
);
writeStdoutLine(t(' Status: Configured\n'));
}
process.exit(0);
} catch (error) {
writeStderrLine(
t('Failed to check authentication status: {{error}}', {
error: getErrorMessage(error),
}),
);
process.exit(1);
}
}

View file

@ -0,0 +1,421 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { InteractiveSelector } from './interactiveSelector.js';
import { stdin, stdout } from 'node:process';
describe('InteractiveSelector', () => {
const mockOptions = [
{ value: 'option1', label: 'Option 1', description: 'First option' },
{ value: 'option2', label: 'Option 2', description: 'Second option' },
{ value: 'option3', label: 'Option 3', description: 'Third option' },
];
const mockPrompt = 'Select an option:';
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('constructor', () => {
it('should create an instance with default prompt', () => {
const selector = new InteractiveSelector(mockOptions);
expect(selector).toBeInstanceOf(InteractiveSelector);
});
it('should create an instance with custom prompt', () => {
const selector = new InteractiveSelector(mockOptions, mockPrompt);
expect(selector).toBeInstanceOf(InteractiveSelector);
});
});
describe('select', () => {
it('should reject if raw mode is not available', async () => {
// Mock stdin without setRawMode
const originalSetRawMode = stdin.setRawMode;
(stdin as any).setRawMode = undefined;
const selector = new InteractiveSelector(mockOptions, mockPrompt);
await expect(selector.select()).rejects.toThrow(
'Raw mode not available. Please run in an interactive terminal.',
);
// Restore
(stdin as any).setRawMode = originalSetRawMode;
});
it('should select first option with Enter key', async () => {
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockSetEncoding = vi.fn();
const mockRemoveListener = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
// Simulate Enter key press
setTimeout(() => callback('\r'), 0);
return stdin;
});
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).setEncoding = mockSetEncoding;
(stdin as any).removeListener = mockRemoveListener;
(stdin as any).on = mockOn;
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const result = await selector.select();
expect(result).toBe('option1');
expect(mockSetRawMode).toHaveBeenCalledWith(true);
expect(mockResume).toHaveBeenCalled();
stdoutWriteSpy.mockRestore();
});
it('should select second option after arrow down then Enter', async () => {
let dataCallback!: (chunk: string) => void;
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
dataCallback = callback;
return stdin;
});
const mockRemoveListener = vi.fn();
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).on = mockOn;
(stdin as any).removeListener = mockRemoveListener;
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const selectPromise = selector.select();
// Simulate arrow down
dataCallback('\x1B[B');
// Simulate Enter
setTimeout(() => dataCallback('\r'), 0);
const result = await selectPromise;
expect(result).toBe('option2');
stdoutWriteSpy.mockRestore();
});
it('should handle arrow up navigation', async () => {
let dataCallback!: (chunk: string) => void;
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
dataCallback = callback;
return stdin;
});
const mockRemoveListener = vi.fn();
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).on = mockOn;
(stdin as any).removeListener = mockRemoveListener;
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const selectPromise = selector.select();
// Move down twice
dataCallback('\x1B[B');
dataCallback('\x1B[B');
// Move up once
dataCallback('\x1B[A');
// Simulate Enter
setTimeout(() => dataCallback('\r'), 0);
const result = await selectPromise;
expect(result).toBe('option2');
stdoutWriteSpy.mockRestore();
});
it('should reject with Ctrl+C', async () => {
let dataCallback!: (chunk: string) => void;
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
dataCallback = callback;
return stdin;
});
const mockRemoveListener = vi.fn();
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).on = mockOn;
(stdin as any).removeListener = mockRemoveListener;
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const selectPromise = selector.select();
// Simulate Ctrl+C
setTimeout(() => dataCallback('\x03'), 0);
await expect(selectPromise).rejects.toThrow('Interrupted');
});
it('should wrap around when navigating past last option', async () => {
let dataCallback!: (chunk: string) => void;
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
dataCallback = callback;
return stdin;
});
const mockRemoveListener = vi.fn();
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).on = mockOn;
(stdin as any).removeListener = mockRemoveListener;
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const selectPromise = selector.select();
// Move down past last option (should wrap to first)
dataCallback('\x1B[B');
dataCallback('\x1B[B');
dataCallback('\x1B[B'); // Now at option1 again (wrapped)
// Simulate Enter
setTimeout(() => dataCallback('\r'), 0);
const result = await selectPromise;
expect(result).toBe('option1');
stdoutWriteSpy.mockRestore();
});
it('should wrap around when navigating before first option', async () => {
let dataCallback!: (chunk: string) => void;
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
dataCallback = callback;
return stdin;
});
const mockRemoveListener = vi.fn();
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).on = mockOn;
(stdin as any).removeListener = mockRemoveListener;
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const selectPromise = selector.select();
// Move up from first option (should wrap to last)
dataCallback('\x1B[A');
// Simulate Enter
setTimeout(() => dataCallback('\r'), 0);
const result = await selectPromise;
expect(result).toBe('option3');
stdoutWriteSpy.mockRestore();
});
it('should ignore arrow left/right keys', async () => {
let dataCallback!: (chunk: string) => void;
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
dataCallback = callback;
return stdin;
});
const mockRemoveListener = vi.fn();
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).on = mockOn;
(stdin as any).removeListener = mockRemoveListener;
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const selectPromise = selector.select();
// Press arrow right (should be ignored)
dataCallback('\x1B[C');
// Press arrow left (should be ignored)
dataCallback('\x1B[D');
// Press Enter - should still select first option
setTimeout(() => dataCallback('\r'), 0);
const result = await selectPromise;
expect(result).toBe('option1');
stdoutWriteSpy.mockRestore();
});
it('should handle newline character as Enter', async () => {
let dataCallback!: (chunk: string) => void;
const mockSetRawMode = vi.fn();
const mockResume = vi.fn();
const mockOn = vi.fn((event: any, callback: any) => {
dataCallback = callback;
return stdin;
});
const mockRemoveListener = vi.fn();
(stdin as any).isRaw = false;
(stdin as any).setRawMode = mockSetRawMode;
(stdin as any).resume = mockResume;
(stdin as any).on = mockOn;
(stdin as any).removeListener = mockRemoveListener;
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
const selectPromise = selector.select();
// Simulate newline
setTimeout(() => dataCallback('\n'), 0);
const result = await selectPromise;
expect(result).toBe('option1');
stdoutWriteSpy.mockRestore();
});
});
describe('renderMenu', () => {
it('should render menu with correct formatting', () => {
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
// Access private method for testing
(selector as any).renderMenu();
expect(stdoutWriteSpy).toHaveBeenCalled();
const output = stdoutWriteSpy.mock.calls.map((call) => call[0]).join('');
expect(output).toContain('Select an option:');
expect(output).toContain('Option 1');
expect(output).toContain('Option 2');
expect(output).toContain('Option 3');
expect(output).toContain('First option');
expect(output).toContain('Second option');
expect(output).toContain('Third option');
expect(output).toContain('↑ ↓');
expect(output).toContain('Enter');
expect(output).toContain('Ctrl+C');
stdoutWriteSpy.mockRestore();
});
it('should highlight selected option', () => {
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(mockOptions, mockPrompt);
(selector as any).selectedIndex = 1;
(selector as any).renderMenu();
const output = stdoutWriteSpy.mock.calls.map((call) => call[0]).join('');
// Selected option should have cyan color code
expect(output).toContain('\x1B[36m');
stdoutWriteSpy.mockRestore();
});
it('should calculate correct total lines', () => {
const selector = new InteractiveSelector(mockOptions, mockPrompt);
// Access private method for testing
(selector as any).calculateTotalLines();
// Expected: 4 (prompt + empty + empty + instructions) + 3 (options) = 7
expect((selector as any).calculateTotalLines()).toBe(7);
});
it('should handle options without descriptions', () => {
const simpleOptions = [
{ value: 'a', label: 'A' },
{ value: 'b', label: 'B' },
];
const stdoutWriteSpy = vi
.spyOn(stdout, 'write')
.mockImplementation(() => true);
const selector = new InteractiveSelector(simpleOptions, mockPrompt);
(selector as any).renderMenu();
const output = stdoutWriteSpy.mock.calls.map((call) => call[0]).join('');
expect(output).toContain('A');
expect(output).toContain('B');
stdoutWriteSpy.mockRestore();
});
});
});

View file

@ -0,0 +1,166 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { stdin, stdout } from 'node:process';
import { t } from '../../i18n/index.js';
/**
* Represents an option in the interactive selector
*/
interface Option<T> {
value: T;
label: string;
description?: string;
}
/**
* Interactive selector that allows users to navigate with arrow keys
*/
export class InteractiveSelector<T> {
private selectedIndex = 0;
private isListening = false;
constructor(
private options: Array<Option<T>>,
private prompt: string = t('Select an option:'),
) {}
/**
* Shows the interactive menu and waits for user selection
*/
async select(): Promise<T> {
return new Promise((resolve, reject) => {
this.isListening = true;
// Display initial menu
this.renderMenu();
// Check if stdin supports raw mode
if (!stdin.setRawMode) {
// Fallback to readline if raw mode is not available (e.g., when piped)
reject(
new Error(
t('Raw mode not available. Please run in an interactive terminal.'),
),
);
return;
}
const wasRaw = stdin.isRaw;
stdin.setRawMode(true);
stdin.resume();
stdin.setEncoding('utf8');
const onData = (chunk: string) => {
if (!this.isListening) return;
for (const char of chunk) {
switch (char) {
case '\x03': // Ctrl+C
stdin.removeListener('data', onData);
stdin.setRawMode(wasRaw);
reject(new Error('Interrupted'));
return;
case '\r': // Enter
case '\n': // Newline
stdin.removeListener('data', onData);
stdin.setRawMode(wasRaw);
resolve(this.options[this.selectedIndex].value);
return;
case '\x1B': // ESC sequence
// Next character will be [, then A, B, C, or D
break;
default:
// Handle other characters if needed
break;
}
}
// Handle escape sequences
if (chunk.startsWith('\x1B')) {
if (chunk === '\x1B[A') {
// Arrow up
this.moveUp();
} else if (chunk === '\x1B[B') {
// Arrow down
this.moveDown();
} else if (chunk === '\x1B[C') {
// Arrow right
// Do nothing for now
} else if (chunk === '\x1B[D') {
// Arrow left
// Do nothing for now
}
}
};
stdin.on('data', onData);
});
}
/**
* Renders the menu to stdout
*/
private renderMenu(): void {
// Calculate how many lines we need to clear
const totalLines = this.calculateTotalLines();
// Clear the screen area we'll be using
if (totalLines > 0) {
stdout.write(`\x1B[${totalLines}A\x1B[J`); // Move up and clear from cursor down
}
// Write the prompt
stdout.write(`${this.prompt}\n\n`);
// Write each option - combine label and description on same line
this.options.forEach((option, index) => {
const isSelected = index === this.selectedIndex;
const indicator = isSelected ? '> ' : ' ';
const color = isSelected ? '\x1B[36m' : '\x1B[0m'; // Cyan for selected, default for others
const reset = '\x1B[0m';
// Combine label and description in one line
let line = `${indicator}${color}${option.label}`;
if (option.description) {
line += ` - ${option.description}`;
}
line += `${reset}\n`;
stdout.write(line);
});
// Add instructions
stdout.write(
`\n${t('(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n')}`,
);
}
/**
* Calculates the total number of lines to clear
*/
private calculateTotalLines(): number {
// Lines for: prompt (1) + empty line (1) + options (each option takes 1 line) + empty line (1) + instructions (1)
return 4 + this.options.length;
}
/**
* Moves selection up
*/
private moveUp(): void {
this.selectedIndex =
(this.selectedIndex - 1 + this.options.length) % this.options.length;
this.renderMenu();
}
/**
* Moves selection down
*/
private moveDown(): void {
this.selectedIndex = (this.selectedIndex + 1) % this.options.length;
this.renderMenu();
}
}

View file

@ -0,0 +1,266 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { showAuthStatus } from './handler.js';
import { AuthType } from '@qwen-code/qwen-code-core';
import { CODING_PLAN_ENV_KEY } from '../../constants/codingPlan.js';
import type { LoadedSettings } from '../../config/settings.js';
vi.mock('../../config/settings.js', () => ({
loadSettings: vi.fn(),
}));
vi.mock('../../utils/stdioHelpers.js', () => ({
writeStdoutLine: vi.fn(),
writeStderrLine: vi.fn(),
}));
import { loadSettings } from '../../config/settings.js';
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
describe('showAuthStatus', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
delete process.env[CODING_PLAN_ENV_KEY];
});
afterEach(() => {
vi.restoreAllMocks();
delete process.env[CODING_PLAN_ENV_KEY];
});
const createMockSettings = (
merged: Record<string, unknown>,
): LoadedSettings =>
({
merged,
system: { settings: {}, path: '/system.json' },
systemDefaults: { settings: {}, path: '/system-defaults.json' },
user: { settings: {}, path: '/user.json' },
workspace: { settings: {}, path: '/workspace.json' },
forScope: vi.fn(),
setValue: vi.fn(),
isTrusted: true,
}) as unknown as LoadedSettings;
it('should show message when no authentication is configured', async () => {
vi.mocked(loadSettings).mockReturnValue(createMockSettings({}));
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('No authentication method configured'),
);
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('qwen auth qwen-oauth'),
);
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('qwen auth coding-plan'),
);
expect(process.exit).toHaveBeenCalledWith(0);
});
it('should show Qwen OAuth status when configured', async () => {
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
security: {
auth: {
selectedType: AuthType.QWEN_OAUTH,
},
},
}),
);
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('Qwen OAuth'),
);
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('Free tier'),
);
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('1,000 requests/day'),
);
expect(process.exit).toHaveBeenCalledWith(0);
});
it('should show Coding Plan status when configured with API key', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
codingPlan: {
region: 'china',
version: 'abc123def456',
},
model: {
name: 'qwen3.5-plus',
},
}),
);
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('Alibaba Cloud Coding Plan'),
);
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('API key configured'),
);
expect(process.exit).toHaveBeenCalledWith(0);
});
it('should show Coding Plan as incomplete when API key is missing', async () => {
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
codingPlan: {
region: 'global',
},
}),
);
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('Incomplete'),
);
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('API key not found'),
);
});
it('should show Coding Plan region for china', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
codingPlan: {
region: 'china',
},
model: {
name: 'qwen3.5-plus',
},
}),
);
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('中国 (China)'),
);
});
it('should show Coding Plan region for global', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
codingPlan: {
region: 'global',
},
model: {
name: 'qwen3-coder-plus',
},
}),
);
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('Global'),
);
});
it('should show current model name', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
codingPlan: {
region: 'china',
},
model: {
name: 'qwen3.5-plus',
},
}),
);
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('qwen3.5-plus'),
);
});
it('should show config version (truncated)', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
codingPlan: {
region: 'china',
version: 'abc123def456789',
},
model: {
name: 'qwen3.5-plus',
},
}),
);
await showAuthStatus();
expect(writeStdoutLine).toHaveBeenCalledWith(
expect.stringContaining('abc123de...'),
);
});
it('should handle errors and exit with code 1', async () => {
const error = new Error('Settings load failed');
vi.mocked(loadSettings).mockImplementation(() => {
throw error;
});
await showAuthStatus();
expect(writeStderrLine).toHaveBeenCalledWith(
expect.stringContaining('Failed to check authentication status'),
);
expect(process.exit).toHaveBeenCalledWith(1);
});
});

View file

@ -35,6 +35,7 @@ import { extensionsCommand } from '../commands/extensions.js';
import { hooksCommand } from '../commands/hooks.js';
import type { Settings, LoadedSettings } from './settings.js';
import { SettingScope } from './settings.js';
import { authCommand } from '../commands/auth.js';
import {
resolveCliGenerationConfig,
getAuthTypeFromEnv,
@ -586,6 +587,8 @@ export async function parseArguments(): Promise<CliArgs> {
.command(mcpCommand)
// Register Extension subcommands
.command(extensionsCommand)
// Register Auth subcommands
.command(authCommand)
// Register Hooks subcommands
.command(hooksCommand);

View file

@ -1756,4 +1756,80 @@ export default {
'↑/↓: Navigieren | Space/Enter: Umschalten | Esc: Abbrechen',
'↑/↓: Navigate | Enter: Select | Esc: Cancel':
'↑/↓: Navigieren | Enter: Auswählen | Esc: Abbrechen',
// ============================================================================
// Commands - Auth
// ============================================================================
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan':
'Qwen-Authentifizierung mit Qwen-OAuth oder Alibaba Cloud Coding Plan konfigurieren',
'Authenticate using Qwen OAuth': 'Mit Qwen OAuth authentifizieren',
'Authenticate using Alibaba Cloud Coding Plan':
'Mit Alibaba Cloud Coding Plan authentifizieren',
'Region for Coding Plan (china/global)':
'Region für Coding Plan (china/global)',
'API key for Coding Plan': 'API-Schlüssel für Coding Plan',
'Show current authentication status':
'Aktuellen Authentifizierungsstatus anzeigen',
'Authentication completed successfully.':
'Authentifizierung erfolgreich abgeschlossen.',
'Starting Qwen OAuth authentication...':
'Qwen OAuth-Authentifizierung wird gestartet...',
'Successfully authenticated with Qwen OAuth.':
'Erfolgreich mit Qwen OAuth authentifiziert.',
'Failed to authenticate with Qwen OAuth: {{error}}':
'Authentifizierung mit Qwen OAuth fehlgeschlagen: {{error}}',
'Processing Alibaba Cloud Coding Plan authentication...':
'Alibaba Cloud Coding Plan-Authentifizierung wird verarbeitet...',
'Successfully authenticated with Alibaba Cloud Coding Plan.':
'Erfolgreich mit Alibaba Cloud Coding Plan authentifiziert.',
'Failed to authenticate with Coding Plan: {{error}}':
'Authentifizierung mit Coding Plan fehlgeschlagen: {{error}}',
'中国 (China)': '中国 (China)',
'阿里云百炼 (aliyun.com)': '阿里云百炼 (aliyun.com)',
Global: 'Global',
'Alibaba Cloud (alibabacloud.com)': 'Alibaba Cloud (alibabacloud.com)',
'Select region for Coding Plan:': 'Region für Coding Plan auswählen:',
'Enter your Coding Plan API key: ':
'Geben Sie Ihren Coding Plan API-Schlüssel ein: ',
'Select authentication method:': 'Authentifizierungsmethode auswählen:',
'\n=== Authentication Status ===\n': '\n=== Authentifizierungsstatus ===\n',
'⚠️ No authentication method configured.\n':
'⚠️ Keine Authentifizierungsmethode konfiguriert.\n',
'Run one of the following commands to get started:\n':
'Führen Sie einen der folgenden Befehle aus, um zu beginnen:\n',
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)':
' qwen auth qwen-oauth - Mit Qwen OAuth authentifizieren (kostenlos)',
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n':
' qwen auth coding-plan - Mit Alibaba Cloud Coding Plan authentifizieren\n',
'Or simply run:': 'Oder einfach ausführen:',
' qwen auth - Interactive authentication setup\n':
' qwen auth - Interaktive Authentifizierungseinrichtung\n',
'✓ Authentication Method: Qwen OAuth':
'✓ Authentifizierungsmethode: Qwen OAuth',
' Type: Free tier': ' Typ: Kostenlos',
' Limit: Up to 1,000 requests/day': ' Limit: Bis zu 1.000 Anfragen/Tag',
' Models: Qwen latest models\n': ' Modelle: Qwen neueste Modelle\n',
'✓ Authentication Method: Alibaba Cloud Coding Plan':
'✓ Authentifizierungsmethode: Alibaba Cloud Coding Plan',
'中国 (China) - 阿里云百炼': '中国 (China) - 阿里云百炼',
'Global - Alibaba Cloud': 'Global - Alibaba Cloud',
' Region: {{region}}': ' Region: {{region}}',
' Current Model: {{model}}': ' Aktuelles Modell: {{model}}',
' Config Version: {{version}}': ' Konfigurationsversion: {{version}}',
' Status: API key configured\n': ' Status: API-Schlüssel konfiguriert\n',
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)':
'⚠️ Authentifizierungsmethode: Alibaba Cloud Coding Plan (Unvollständig)',
' Issue: API key not found in environment or settings\n':
' Problem: API-Schlüssel nicht in Umgebung oder Einstellungen gefunden\n',
' Run `qwen auth coding-plan` to re-configure.\n':
' Führen Sie `qwen auth coding-plan` aus, um neu zu konfigurieren.\n',
'✓ Authentication Method: {{type}}': '✓ Authentifizierungsmethode: {{type}}',
' Status: Configured\n': ' Status: Konfiguriert\n',
'Failed to check authentication status: {{error}}':
'Authentifizierungsstatus konnte nicht überprüft werden: {{error}}',
'Select an option:': 'Option auswählen:',
'Raw mode not available. Please run in an interactive terminal.':
'Raw-Modus nicht verfügbar. Bitte in einem interaktiven Terminal ausführen.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ Pfeiltasten zum Navigieren, Enter zum Auswählen, Strg+C zum Beenden)\n',
};

View file

@ -1803,4 +1803,77 @@ export default {
'↑/↓: Navigate | Space/Enter: Toggle | Esc: Cancel',
'↑/↓: Navigate | Enter: Select | Esc: Cancel':
'↑/↓: Navigate | Enter: Select | Esc: Cancel',
// ============================================================================
// Commands - Auth
// ============================================================================
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan':
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan',
'Authenticate using Qwen OAuth': 'Authenticate using Qwen OAuth',
'Authenticate using Alibaba Cloud Coding Plan':
'Authenticate using Alibaba Cloud Coding Plan',
'Region for Coding Plan (china/global)':
'Region for Coding Plan (china/global)',
'API key for Coding Plan': 'API key for Coding Plan',
'Show current authentication status': 'Show current authentication status',
'Authentication completed successfully.':
'Authentication completed successfully.',
'Starting Qwen OAuth authentication...':
'Starting Qwen OAuth authentication...',
'Successfully authenticated with Qwen OAuth.':
'Successfully authenticated with Qwen OAuth.',
'Failed to authenticate with Qwen OAuth: {{error}}':
'Failed to authenticate with Qwen OAuth: {{error}}',
'Processing Alibaba Cloud Coding Plan authentication...':
'Processing Alibaba Cloud Coding Plan authentication...',
'Successfully authenticated with Alibaba Cloud Coding Plan.':
'Successfully authenticated with Alibaba Cloud Coding Plan.',
'Failed to authenticate with Coding Plan: {{error}}':
'Failed to authenticate with Coding Plan: {{error}}',
'中国 (China)': '中国 (China)',
'阿里云百炼 (aliyun.com)': '阿里云百炼 (aliyun.com)',
Global: 'Global',
'Alibaba Cloud (alibabacloud.com)': 'Alibaba Cloud (alibabacloud.com)',
'Select region for Coding Plan:': 'Select region for Coding Plan:',
'Enter your Coding Plan API key: ': 'Enter your Coding Plan API key: ',
'Select authentication method:': 'Select authentication method:',
'\n=== Authentication Status ===\n': '\n=== Authentication Status ===\n',
'⚠️ No authentication method configured.\n':
'⚠️ No authentication method configured.\n',
'Run one of the following commands to get started:\n':
'Run one of the following commands to get started:\n',
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)':
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)',
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n':
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n',
'Or simply run:': 'Or simply run:',
' qwen auth - Interactive authentication setup\n':
' qwen auth - Interactive authentication setup\n',
'✓ Authentication Method: Qwen OAuth': '✓ Authentication Method: Qwen OAuth',
' Type: Free tier': ' Type: Free tier',
' Limit: Up to 1,000 requests/day': ' Limit: Up to 1,000 requests/day',
' Models: Qwen latest models\n': ' Models: Qwen latest models\n',
'✓ Authentication Method: Alibaba Cloud Coding Plan':
'✓ Authentication Method: Alibaba Cloud Coding Plan',
'中国 (China) - 阿里云百炼': '中国 (China) - 阿里云百炼',
'Global - Alibaba Cloud': 'Global - Alibaba Cloud',
' Region: {{region}}': ' Region: {{region}}',
' Current Model: {{model}}': ' Current Model: {{model}}',
' Config Version: {{version}}': ' Config Version: {{version}}',
' Status: API key configured\n': ' Status: API key configured\n',
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)':
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)',
' Issue: API key not found in environment or settings\n':
' Issue: API key not found in environment or settings\n',
' Run `qwen auth coding-plan` to re-configure.\n':
' Run `qwen auth coding-plan` to re-configure.\n',
'✓ Authentication Method: {{type}}': '✓ Authentication Method: {{type}}',
' Status: Configured\n': ' Status: Configured\n',
'Failed to check authentication status: {{error}}':
'Failed to check authentication status: {{error}}',
'Select an option:': 'Select an option:',
'Raw mode not available. Please run in an interactive terminal.':
'Raw mode not available. Please run in an interactive terminal.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n',
};

View file

@ -1257,4 +1257,76 @@ export default {
'↑/↓: ナビゲート | Space/Enter: 切り替え | Esc: キャンセル',
'↑/↓: Navigate | Enter: Select | Esc: Cancel':
'↑/↓: ナビゲート | Enter: 選択 | Esc: キャンセル',
// ============================================================================
// Commands - Auth
// ============================================================================
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan':
'Qwen-OAuth または Alibaba Cloud Coding Plan で Qwen 認証情報を設定する',
'Authenticate using Qwen OAuth': 'Qwen OAuth で認証する',
'Authenticate using Alibaba Cloud Coding Plan':
'Alibaba Cloud Coding Plan で認証する',
'Region for Coding Plan (china/global)':
'Coding Plan のリージョン (china/global)',
'API key for Coding Plan': 'Coding Plan の API キー',
'Show current authentication status': '現在の認証ステータスを表示',
'Authentication completed successfully.': '認証が正常に完了しました。',
'Starting Qwen OAuth authentication...': 'Qwen OAuth 認証を開始しています...',
'Successfully authenticated with Qwen OAuth.':
'Qwen OAuth での認証に成功しました。',
'Failed to authenticate with Qwen OAuth: {{error}}':
'Qwen OAuth での認証に失敗しました: {{error}}',
'Processing Alibaba Cloud Coding Plan authentication...':
'Alibaba Cloud Coding Plan 認証を処理しています...',
'Successfully authenticated with Alibaba Cloud Coding Plan.':
'Alibaba Cloud Coding Plan での認証に成功しました。',
'Failed to authenticate with Coding Plan: {{error}}':
'Coding Plan での認証に失敗しました: {{error}}',
'中国 (China)': '中国 (China)',
'阿里云百炼 (aliyun.com)': '阿里云百炼 (aliyun.com)',
Global: 'グローバル',
'Alibaba Cloud (alibabacloud.com)': 'Alibaba Cloud (alibabacloud.com)',
'Select region for Coding Plan:': 'Coding Plan のリージョンを選択:',
'Enter your Coding Plan API key: ':
'Coding Plan の API キーを入力してください: ',
'Select authentication method:': '認証方法を選択:',
'\n=== Authentication Status ===\n': '\n=== 認証ステータス ===\n',
'⚠️ No authentication method configured.\n':
'⚠️ 認証方法が設定されていません。\n',
'Run one of the following commands to get started:\n':
'以下のコマンドのいずれかを実行して開始してください:\n',
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)':
' qwen auth qwen-oauth - Qwen OAuth で認証(無料)',
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n':
' qwen auth coding-plan - Alibaba Cloud Coding Plan で認証\n',
'Or simply run:': 'または以下を実行:',
' qwen auth - Interactive authentication setup\n':
' qwen auth - インタラクティブ認証セットアップ\n',
'✓ Authentication Method: Qwen OAuth': '✓ 認証方法: Qwen OAuth',
' Type: Free tier': ' タイプ: 無料プラン',
' Limit: Up to 1,000 requests/day': ' 制限: 1日最大1,000リクエスト',
' Models: Qwen latest models\n': ' モデル: Qwen 最新モデル\n',
'✓ Authentication Method: Alibaba Cloud Coding Plan':
'✓ 認証方法: Alibaba Cloud Coding Plan',
'中国 (China) - 阿里云百炼': '中国 (China) - 阿里云百炼',
'Global - Alibaba Cloud': 'グローバル - Alibaba Cloud',
' Region: {{region}}': ' リージョン: {{region}}',
' Current Model: {{model}}': ' 現在のモデル: {{model}}',
' Config Version: {{version}}': ' 設定バージョン: {{version}}',
' Status: API key configured\n': ' ステータス: APIキー設定済み\n',
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)':
'⚠️ 認証方法: Alibaba Cloud Coding Plan不完全',
' Issue: API key not found in environment or settings\n':
' 問題: 環境変数または設定にAPIキーが見つかりません\n',
' Run `qwen auth coding-plan` to re-configure.\n':
' `qwen auth coding-plan` を実行して再設定してください。\n',
'✓ Authentication Method: {{type}}': '✓ 認証方法: {{type}}',
' Status: Configured\n': ' ステータス: 設定済み\n',
'Failed to check authentication status: {{error}}':
'認証ステータスの確認に失敗しました: {{error}}',
'Select an option:': 'オプションを選択:',
'Raw mode not available. Please run in an interactive terminal.':
'Rawモードが利用できません。インタラクティブターミナルで実行してください。',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ 矢印キーで移動、Enter で選択、Ctrl+C で終了)\n',
};

View file

@ -1749,4 +1749,78 @@ export default {
'↑/↓: Navegar | Space/Enter: Alternar | Esc: Cancelar',
'↑/↓: Navigate | Enter: Select | Esc: Cancel':
'↑/↓: Navegar | Enter: Selecionar | Esc: Cancelar',
// ============================================================================
// Commands - Auth
// ============================================================================
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan':
'Configurar autenticação Qwen com Qwen-OAuth ou Alibaba Cloud Coding Plan',
'Authenticate using Qwen OAuth': 'Autenticar usando Qwen OAuth',
'Authenticate using Alibaba Cloud Coding Plan':
'Autenticar usando Alibaba Cloud Coding Plan',
'Region for Coding Plan (china/global)':
'Região para Coding Plan (china/global)',
'API key for Coding Plan': 'Chave de API para Coding Plan',
'Show current authentication status': 'Mostrar status atual de autenticação',
'Authentication completed successfully.':
'Autenticação concluída com sucesso.',
'Starting Qwen OAuth authentication...':
'Iniciando autenticação Qwen OAuth...',
'Successfully authenticated with Qwen OAuth.':
'Autenticado com sucesso via Qwen OAuth.',
'Failed to authenticate with Qwen OAuth: {{error}}':
'Falha ao autenticar com Qwen OAuth: {{error}}',
'Processing Alibaba Cloud Coding Plan authentication...':
'Processando autenticação Alibaba Cloud Coding Plan...',
'Successfully authenticated with Alibaba Cloud Coding Plan.':
'Autenticado com sucesso via Alibaba Cloud Coding Plan.',
'Failed to authenticate with Coding Plan: {{error}}':
'Falha ao autenticar com Coding Plan: {{error}}',
'中国 (China)': '中国 (China)',
'阿里云百炼 (aliyun.com)': '阿里云百炼 (aliyun.com)',
Global: 'Global',
'Alibaba Cloud (alibabacloud.com)': 'Alibaba Cloud (alibabacloud.com)',
'Select region for Coding Plan:': 'Selecione a região para Coding Plan:',
'Enter your Coding Plan API key: ':
'Insira sua chave de API do Coding Plan: ',
'Select authentication method:': 'Selecione o método de autenticação:',
'\n=== Authentication Status ===\n': '\n=== Status de Autenticação ===\n',
'⚠️ No authentication method configured.\n':
'⚠️ Nenhum método de autenticação configurado.\n',
'Run one of the following commands to get started:\n':
'Execute um dos seguintes comandos para começar:\n',
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)':
' qwen auth qwen-oauth - Autenticar com Qwen OAuth (gratuito)',
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n':
' qwen auth coding-plan - Autenticar com Alibaba Cloud Coding Plan\n',
'Or simply run:': 'Ou simplesmente execute:',
' qwen auth - Interactive authentication setup\n':
' qwen auth - Configuração interativa de autenticação\n',
'✓ Authentication Method: Qwen OAuth': '✓ Método de autenticação: Qwen OAuth',
' Type: Free tier': ' Tipo: Gratuito',
' Limit: Up to 1,000 requests/day': ' Limite: Até 1.000 solicitações/dia',
' Models: Qwen latest models\n': ' Modelos: Modelos Qwen mais recentes\n',
'✓ Authentication Method: Alibaba Cloud Coding Plan':
'✓ Método de autenticação: Alibaba Cloud Coding Plan',
'中国 (China) - 阿里云百炼': '中国 (China) - 阿里云百炼',
'Global - Alibaba Cloud': 'Global - Alibaba Cloud',
' Region: {{region}}': ' Região: {{region}}',
' Current Model: {{model}}': ' Modelo atual: {{model}}',
' Config Version: {{version}}': ' Versão da configuração: {{version}}',
' Status: API key configured\n': ' Status: Chave de API configurada\n',
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)':
'⚠️ Método de autenticação: Alibaba Cloud Coding Plan (Incompleto)',
' Issue: API key not found in environment or settings\n':
' Problema: Chave de API não encontrada no ambiente ou configurações\n',
' Run `qwen auth coding-plan` to re-configure.\n':
' Execute `qwen auth coding-plan` para reconfigurar.\n',
'✓ Authentication Method: {{type}}': '✓ Método de autenticação: {{type}}',
' Status: Configured\n': ' Status: Configurado\n',
'Failed to check authentication status: {{error}}':
'Falha ao verificar status de autenticação: {{error}}',
'Select an option:': 'Selecione uma opção:',
'Raw mode not available. Please run in an interactive terminal.':
'Modo raw não disponível. Execute em um terminal interativo.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(Use ↑ ↓ para navegar, Enter para selecionar, Ctrl+C para sair)\n',
};

View file

@ -1758,4 +1758,77 @@ export default {
'↑/↓: Навигация | Space/Enter: Переключить | Esc: Отмена',
'↑/↓: Navigate | Enter: Select | Esc: Cancel':
'↑/↓: Навигация | Enter: Выбор | Esc: Отмена',
// ============================================================================
// Commands - Auth
// ============================================================================
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan':
'Настроить аутентификацию Qwen через Qwen-OAuth или Alibaba Cloud Coding Plan',
'Authenticate using Qwen OAuth': 'Аутентификация через Qwen OAuth',
'Authenticate using Alibaba Cloud Coding Plan':
'Аутентификация через Alibaba Cloud Coding Plan',
'Region for Coding Plan (china/global)':
'Регион для Coding Plan (china/global)',
'API key for Coding Plan': 'API-ключ для Coding Plan',
'Show current authentication status':
'Показать текущий статус аутентификации',
'Authentication completed successfully.': 'Аутентификация успешно завершена.',
'Starting Qwen OAuth authentication...':
'Запуск аутентификации Qwen OAuth...',
'Successfully authenticated with Qwen OAuth.':
'Успешная аутентификация через Qwen OAuth.',
'Failed to authenticate with Qwen OAuth: {{error}}':
'Ошибка аутентификации через Qwen OAuth: {{error}}',
'Processing Alibaba Cloud Coding Plan authentication...':
'Обработка аутентификации Alibaba Cloud Coding Plan...',
'Successfully authenticated with Alibaba Cloud Coding Plan.':
'Успешная аутентификация через Alibaba Cloud Coding Plan.',
'Failed to authenticate with Coding Plan: {{error}}':
'Ошибка аутентификации через Coding Plan: {{error}}',
'中国 (China)': '中国 (China)',
'阿里云百炼 (aliyun.com)': '阿里云百炼 (aliyun.com)',
Global: 'Глобальный',
'Alibaba Cloud (alibabacloud.com)': 'Alibaba Cloud (alibabacloud.com)',
'Select region for Coding Plan:': 'Выберите регион для Coding Plan:',
'Enter your Coding Plan API key: ': 'Введите ваш API-ключ Coding Plan: ',
'Select authentication method:': 'Выберите метод аутентификации:',
'\n=== Authentication Status ===\n': '\n=== Статус аутентификации ===\n',
'⚠️ No authentication method configured.\n':
'⚠️ Метод аутентификации не настроен.\n',
'Run one of the following commands to get started:\n':
'Выполните одну из следующих команд для начала:\n',
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)':
' qwen auth qwen-oauth - Аутентификация через Qwen OAuth (бесплатно)',
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n':
' qwen auth coding-plan - Аутентификация через Alibaba Cloud Coding Plan\n',
'Or simply run:': 'Или просто выполните:',
' qwen auth - Interactive authentication setup\n':
' qwen auth - Интерактивная настройка аутентификации\n',
'✓ Authentication Method: Qwen OAuth': '✓ Метод аутентификации: Qwen OAuth',
' Type: Free tier': ' Тип: Бесплатный',
' Limit: Up to 1,000 requests/day': ' Лимит: До 1 000 запросов/день',
' Models: Qwen latest models\n': ' Модели: Последние модели Qwen\n',
'✓ Authentication Method: Alibaba Cloud Coding Plan':
'✓ Метод аутентификации: Alibaba Cloud Coding Plan',
'中国 (China) - 阿里云百炼': '中国 (China) - 阿里云百炼',
'Global - Alibaba Cloud': 'Глобальный - Alibaba Cloud',
' Region: {{region}}': ' Регион: {{region}}',
' Current Model: {{model}}': ' Текущая модель: {{model}}',
' Config Version: {{version}}': ' Версия конфигурации: {{version}}',
' Status: API key configured\n': ' Статус: API-ключ настроен\n',
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)':
'⚠️ Метод аутентификации: Alibaba Cloud Coding Plan (Не завершён)',
' Issue: API key not found in environment or settings\n':
' Проблема: API-ключ не найден в окружении или настройках\n',
' Run `qwen auth coding-plan` to re-configure.\n':
' Выполните `qwen auth coding-plan` для повторной настройки.\n',
'✓ Authentication Method: {{type}}': '✓ Метод аутентификации: {{type}}',
' Status: Configured\n': ' Статус: Настроено\n',
'Failed to check authentication status: {{error}}':
'Не удалось проверить статус аутентификации: {{error}}',
'Select an option:': 'Выберите вариант:',
'Raw mode not available. Please run in an interactive terminal.':
'Raw-режим недоступен. Пожалуйста, запустите в интерактивном терминале.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ стрелки для навигации, Enter для выбора, Ctrl+C для выхода)\n',
};

View file

@ -1620,4 +1620,72 @@ export default {
'↑/↓: 导航 | Space/Enter: 切换 | Esc: 取消',
'↑/↓: Navigate | Enter: Select | Esc: Cancel':
'↑/↓: 导航 | Enter: 选择 | Esc: 取消',
// ============================================================================
// Commands - Auth
// ============================================================================
'Configure Qwen authentication information with Qwen-OAuth or Alibaba Cloud Coding Plan':
'使用 Qwen OAuth 或阿里云百炼 Coding Plan 配置 Qwen 认证信息',
'Authenticate using Qwen OAuth': '使用 Qwen OAuth 进行认证',
'Authenticate using Alibaba Cloud Coding Plan':
'使用阿里云百炼 Coding Plan 进行认证',
'Region for Coding Plan (china/global)': 'Coding Plan 区域 (china/global)',
'API key for Coding Plan': 'Coding Plan 的 API 密钥',
'Show current authentication status': '显示当前认证状态',
'Authentication completed successfully.': '认证完成。',
'Starting Qwen OAuth authentication...': '正在启动 Qwen OAuth 认证...',
'Successfully authenticated with Qwen OAuth.': '已成功通过 Qwen OAuth 认证。',
'Failed to authenticate with Qwen OAuth: {{error}}':
'Qwen OAuth 认证失败:{{error}}',
'Processing Alibaba Cloud Coding Plan authentication...':
'正在处理阿里云百炼 Coding Plan 认证...',
'Successfully authenticated with Alibaba Cloud Coding Plan.':
'已成功通过阿里云百炼 Coding Plan 认证。',
'Failed to authenticate with Coding Plan: {{error}}':
'Coding Plan 认证失败:{{error}}',
'中国 (China)': '中国 (China)',
'阿里云百炼 (aliyun.com)': '阿里云百炼 (aliyun.com)',
Global: '全球',
'Alibaba Cloud (alibabacloud.com)': 'Alibaba Cloud (alibabacloud.com)',
'Select region for Coding Plan:': '选择 Coding Plan 区域:',
'Enter your Coding Plan API key: ': '请输入您的 Coding Plan API 密钥:',
'Select authentication method:': '选择认证方式:',
'\n=== Authentication Status ===\n': '\n=== 认证状态 ===\n',
'⚠️ No authentication method configured.\n': '⚠️ 未配置认证方式。\n',
'Run one of the following commands to get started:\n':
'运行以下命令之一开始配置:\n',
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)':
' qwen auth qwen-oauth - 使用 Qwen OAuth 认证(免费)',
' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n':
' qwen auth coding-plan - 使用阿里云百炼 Coding Plan 认证\n',
'Or simply run:': '或者直接运行:',
' qwen auth - Interactive authentication setup\n':
' qwen auth - 交互式认证配置\n',
'✓ Authentication Method: Qwen OAuth': '✓ 认证方式Qwen OAuth',
' Type: Free tier': ' 类型:免费版',
' Limit: Up to 1,000 requests/day': ' 限额:每天最多 1,000 次请求',
' Models: Qwen latest models\n': ' 模型Qwen 最新模型\n',
'✓ Authentication Method: Alibaba Cloud Coding Plan':
'✓ 认证方式:阿里云百炼 Coding Plan',
'中国 (China) - 阿里云百炼': '中国 (China) - 阿里云百炼',
'Global - Alibaba Cloud': '全球 - Alibaba Cloud',
' Region: {{region}}': ' 区域:{{region}}',
' Current Model: {{model}}': ' 当前模型:{{model}}',
' Config Version: {{version}}': ' 配置版本:{{version}}',
' Status: API key configured\n': ' 状态API 密钥已配置\n',
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)':
'⚠️ 认证方式:阿里云百炼 Coding Plan不完整',
' Issue: API key not found in environment or settings\n':
' 问题:在环境变量或设置中未找到 API 密钥\n',
' Run `qwen auth coding-plan` to re-configure.\n':
' 运行 `qwen auth coding-plan` 重新配置。\n',
'✓ Authentication Method: {{type}}': '✓ 认证方式:{{type}}',
' Status: Configured\n': ' 状态:已配置\n',
'Failed to check authentication status: {{error}}':
'检查认证状态失败:{{error}}',
'Select an option:': '请选择:',
'Raw mode not available. Please run in an interactive terminal.':
'原始模式不可用。请在交互式终端中运行。',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(使用 ↑ ↓ 箭头导航Enter 选择Ctrl+C 退出)\n',
};