Files

1 line
40 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "alexxxcoelho/ccswitcher", "files": {"src/lib/wizard.js": {"bac64abbae6f101b8439f98b74f9601566f21d9c": {"t": 1785746009.2039893, "n": 39458, "h": "b6c47214c7b2b862", "c": "import inquirer from 'inquirer';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport terminalLink from 'terminal-link';\nimport { configManager } from './config.js';\nimport { toolManager, SUPPORTED_TOOLS } from './tool-manager.js';\nimport { mcpManager } from './mcp-manager.js';\nimport { claudeCodeManager } from './claude-code-manager.js';\nimport { i18n } from './i18n.js';\nimport { createBorderLine, createContentLine } from '../utils/string-width.js';\nimport { execSync } from 'child_process';\nimport { validateApiKey } from './api-validator.js';\nexport class Wizard {\n static instance;\n BOX_WIDTH = 63; // Default box width for UI elements\n constructor() { }\n static getInstance() {\n if (!Wizard.instance) {\n Wizard.instance = new Wizard();\n }\n return Wizard.instance;\n }\n /**\n * Create a simple box with title using double-line border style\n */\n createBox(title) {\n console.log(chalk.cyan.bold('\\n' + createBorderLine('╔', '╗', '═', this.BOX_WIDTH)));\n console.log(chalk.cyan.bold(createContentLine(title, '║', '║', this.BOX_WIDTH, 'center')));\n console.log(chalk.cyan.bold(createBorderLine('╚', '╝', '═', this.BOX_WIDTH)));\n console.log('');\n }\n /**\n * Display operation hints\n */\n showOperationHints() {\n const hints = [\n chalk.gray(i18n.t('wizard.hint_navigate')),\n chalk.gray(i18n.t('wizard.hint_confirm'))\n ];\n console.log(chalk.gray('💡 ') + hints.join(chalk.gray(' | ')) + '\\n');\n }\n /**\n * Prompt wrapper that shows operation hints\n */\n async promptWithHints(questions) {\n this.showOperationHints();\n return inquirer.prompt(questions);\n }\n printBanner() {\n const BANNER_WIDTH = 75; \n \n const logoLines = [\n ' _|_|_| _|_|_| _|_|_| _| _| _|_|_| _|_|_|_|_| _|_|_| _| _| _|_|_|_| _|_|_| ',\n ' _| _| _| _| _| _| _| _| _| _| _| _| _| ',\n ' _| _| _|_| _| _| _| _| _| _| _|_|_|_| _|_|_| _|_|_| ',\n ' _| _| _| _| _| _| _| _| _| _| _| _| _| _| ',\n ' _|_|_| _|_|_| _|_|_| _| _| _|_|_| _| _|_|_| _| _| _|_|_|_| _| _| '\n ];\n\n const subtitle = i18n.t('wizard.banner_subtitle');\n const subtitleLine = createContentLine(subtitle, '║', '║', BANNER_WIDTH, 'center');\n const emptyLine = createContentLine('', '║', '║', BANNER_WIDTH, 'center');\n const versionLine = createContentLine('v0.0.2', '║', '║', BANNER_WIDTH, 'center');\n\n console.log(chalk.cyan.bold('\\n' + createBorderLine('╔', '╗', '═', BANNER_WIDTH)));\n console.log(chalk.cyan.bold(emptyLine));\n \n // Gradient Colors for Logo\n const colors = [chalk.hex('#00ffff'), chalk.hex('#00dfff'), chalk.hex('#00bfff'), chalk.hex('#009fff'), chalk.hex('#007fff')];\n logoLines.forEach((line, i) => {\n // Center the logo line within the box width manually or using helper\n // createContentLine adds borders, which we don't want to color with the gradient\n // So we construct the content line but color the text part\n const content = createContentLine(line, '║', '║', BANNER_WIDTH, 'center');\n // Hack: replace the text content with colored version. \n // Since createContentLine pads with spaces, simple coloring of the whole string inside borders works\n // But let's just color the *whole line* for simplicity as borders will be colored too.\n // Or better: print borders separately. \n // Let's stick to coloring the whole line for the cool effect.\n console.log(colors[i % colors.length].bold(content));\n });\n\n console.log(chalk.cyan.bold(emptyLine));\n console.log(chalk.white.bold(versionLine));\n console.log(chalk.gray(subtitleLine));\n console.log(chalk.cyan.bold(createBorderLine('╚', '╝', '═', BANNER_WIDTH)));\n console.log('');\n }\n\n resetScreen() {\n console.clear();\n this.printBanner();\n }\n\n async runFirstTimeSetup() {\n this.resetScreen();\n console.log(chalk.cyan.bold('\\n' + (i18n.t('wizard.welcome') || 'Welcome to Claude Code Switcher!')));\n console.log(chalk.gray(i18n.t('wizard.privacy_note') + '\\n'));\n \n await this.showMainMenu();\n }\n\n async showMainMenu() {\n while (true) {\n this.resetScreen();\n \n const providers = configManager.getProviders();\n const selectedId = configManager.getSelectedProviderId();\n\n this.createBox(i18n.t('wizard.select_provider'));\n \n if (providers.length === 0) {\n console.log(chalk.yellow(` ${i18n.t('wizard.not_configured')}\\n`));\n }\n\n const choices = providers.map(p => ({\n name: (p.id === selectedId ? chalk.green('● ') : '○ ') + \n `${chalk.bold(p.name)} ` + \n chalk.gray(`(${p.model || 'Auto'} - ${p.contextWindow || 'Default'} ctx)`),\n value: p.id\n }));\n\n choices.push(new inquirer.Separator());\n choices.push({ name: `+ ${i18n.t('wizard.add_provider')}`, value: 'add_new' });\n choices.push(new inquirer.Separator());\n choices.push({ name: 'x ' + i18n.t('wizard.action_exit'), value: 'exit' });\n\n // Visual Hints Footer\n console.log(\n ' ' + \n chalk.hex('#FFA500').bold('ENTER') + chalk.gray(' = Set/Manage Provider ') + \n chalk.hex('#FFA500').bold('TAB') + chalk.gray(' = Edit (in menu) ') + \n chalk.hex('#FFA500').bold('DEL') + chalk.gray(' = Delete (in menu)')\n );\n console.log('');\n\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices,\n pageSize: 10\n }\n ]);\n\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye')));\n process.exit(0);\n } else if (action === 'add_new') {\n await this.addNewProvider();\n } else {\n await this.manageProvider(action);\n }\n }\n }\n \n async addNewProvider() {\n this.resetScreen();\n this.createBox(i18n.t('wizard.add_provider'));\n \n const { type } = await this.promptWithHints([{\n type: 'list',\n name: 'type',\n message: i18n.t('wizard.select_provider_type'),\n choices: [\n { name: i18n.t('wizard.provider_type_zai'), value: 'zai' },\n { name: i18n.t('wizard.provider_type_deepseek'), value: 'deepseek' },\n { name: i18n.t('wizard.provider_type_custom'), value: 'custom' }\n ]\n }]);\n \n let defaultName = '';\n let defaultUrl = '';\n let defaultCtx = '128000';\n let defaultModel = '';\n let region = '';\n \n if (type === 'zai') {\n // Z.AI Specific Flow\n const { zaiRegion } = await this.promptWithHints([{\n type: 'list',\n name: 'zaiRegion',\n message: i18n.t('wizard.select_region'),\n choices: [\n { name: i18n.t('wizard.region_global'), value: 'global' },\n { name: i18n.t('wizard.region_china'), value: 'china' }\n ]\n }]);\n \n region = zaiRegion;\n if (region === 'global') {\n defaultName = 'Z.AI Global';\n defaultUrl = 'https://api.z.ai/api/anthropic';\n } else {\n defaultName = 'Z.AI China';\n defaultUrl = 'https://open.bigmodel.cn/api/anthropic';\n }\n defaultCtx = '128000';\n defaultModel = 'claude-3-5-sonnet-20240620';\n \n // For Z.AI, we only ask for API Key, other fields are fixed/hidden or confirmable?\n // User asked: \"c'est le seul qu on peut pas changer les endpoints et tout ça\"\n // So we ask for API Key only.\n \n const { apiKey } = await inquirer.prompt([\n {\n type: 'password',\n name: 'apiKey',\n message: i18n.t('wizard.input_api_key'),\n mask: '●',\n validate: input => input.length > 0 ? true : i18n.t('wizard.api_key_required')\n }\n ]);\n \n configManager.addProvider({\n id: Date.now().toString(),\n type,\n name: defaultName,\n baseUrl: defaultUrl,\n model: defaultModel,\n contextWindow: defaultCtx,\n apiKey: apiKey,\n region: region // Store region just in case\n });\n \n console.log(chalk.green(`\\n✅ ${i18n.t('wizard.provider_added')}`));\n await new Promise(resolve => setTimeout(resolve, 1000));\n return;\n } \n \n // DeepSeek & Custom Flow\n if (type === 'deepseek') {\n defaultName = 'DeepSeek';\n defaultUrl = 'https://api.deepseek.com';\n defaultCtx = '64000'; \n defaultModel = 'deepseek-coder';\n }\n \n const answers = await inquirer.prompt([\n {\n type: 'input',\n name: 'name',\n message: i18n.t('wizard.input_provider_name'),\n default: defaultName,\n validate: input => input.length > 0 ? true : 'Name is required'\n },\n {\n type: 'input',\n name: 'baseUrl',\n message: i18n.t('wizard.input_api_url'),\n default: defaultUrl,\n validate: input => input.startsWith('http') ? true : 'Must be a valid URL'\n },\n {\n type: 'input',\n name: 'model',\n message: i18n.t('wizard.input_model'),\n default: defaultModel\n },\n {\n type: 'input',\n name: 'contextWindow',\n message: i18n.t('wizard.input_context'),\n default: defaultCtx\n },\n {\n type: 'password',\n name: 'apiKey',\n message: i18n.t('wizard.input_api_key'),\n mask: '●',\n validate: input => input.length > 0 ? true : i18n.t('wizard.api_key_required')\n }\n ]);\n \n configManager.addProvider({\n id: Date.now().toString(),\n type,\n name: answers.name,\n baseUrl: answers.baseUrl,\n model: answers.model,\n contextWindow: answers.contextWindow,\n apiKey: answers.apiKey\n });\n \n console.log(chalk.green(`\\n✅ ${i18n.t('wizard.provider_added')}`));\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n async manageProvider(providerId) {\n const provider = configManager.getProviders().find(p => p.id === providerId);\n if (!provider) return;\n \n const isActive = configManager.getSelectedProviderId() === providerId;\n \n this.resetScreen();\n this.createBox(`${provider.name} (${isActive ? i18n.t('wizard.active') : ''})`);\n \n const choices = [];\n if (!isActive) {\n choices.push({ name: `> ${i18n.t('wizard.activate_provider')}`, value: 'activate' });\n }\n choices.push({ name: `> ${i18n.t('wizard.delete_provider')}`, value: 'delete' });\n choices.push(new inquirer.Separator());\n choices.push({ name: `< ${i18n.t('wizard.nav_return')}`, value: 'back' });\n \n const { action } = await this.promptWithHints([{\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }]);\n \n if (action === 'back') return;\n \n if (action === 'activate') {\n await this.activateProvider(provider);\n } else if (action === 'delete') {\n configManager.removeProvider(providerId);\n console.log(chalk.yellow(`\\n🗑️ ${i18n.t('wizard.provider_removed')}`));\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n \n async activateProvider(provider) {\n const spinner = ora(i18n.t('auth.reloading', { tool: 'Claude Code' })).start();\n \n try {\n // Use existing claudeCodeManager logic but mapped to new structure\n const options = {\n contextWindow: provider.contextWindow,\n baseUrl: provider.baseUrl,\n model: provider.model\n };\n \n // We reuse 'deepseek' plan name internally for custom logic in claude-code-manager\n // or we just call configurePlan with 'custom' and ensure tool manager handles it.\n // Earlier I changed tool-manager to handle 'custom'.\n \n claudeCodeManager.configurePlan('custom', provider.apiKey, options);\n configManager.setSelectedProviderId(provider.id);\n \n spinner.succeed(chalk.green(i18n.t('wizard.activated')));\n await new Promise(resolve => setTimeout(resolve, 1500));\n } catch (error) {\n spinner.fail(chalk.red(i18n.t('auth.reload_failed')));\n console.error(error);\n await new Promise(resolve => setTimeout(resolve, 2000));\n }\n }\n \n // Deprecated methods stubbed or removed\n async configLanguage() {}\n async configPlan() {}\n async configApiKey() {}\n async configCustomProviderOptions() {}\n async selectAndConfigureTool() {} \n async configureTool() {}\n async showToolMenu() {}\n async startTool() {}\n async loadGLMConfig() {}\n async unloadGLMConfig() {}\n async showMCPMenu() {}\n \n async showToolMenu(toolName) {\n while (true) {\n // for claude code now\n this.resetScreen();\n const title = `${SUPPORTED_TOOLS[toolName].displayName} ${i18n.t('wizard.menu_title')}`;\n this.createBox(title);\n if (toolName === 'claude-code') {\n console.log(chalk.yellow.bold(i18n.t('wizard.global_config_warning', { tool: 'Claude Code' })));\n console.log('');\n }\n let actionText = '';\n const chelperConfig = configManager.getConfig();\n const detectedConfig = claudeCodeManager.detectCurrentConfig();\n // 显示 chelper 配置\n console.log(chalk.cyan.bold(i18n.t('wizard.chelper_config_title') + ':'));\n if (chelperConfig.plan) {\n const planName = chelperConfig.plan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.green(planName));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n if (chelperConfig.api_key) {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.gray(i18n.t('wizard.api_key_set') + ' (' + chelperConfig.api_key.slice(0, 4) + '****)'));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n console.log('');\n // 显示 Claude Code 当前配置\n console.log(chalk.yellow.bold(i18n.t('wizard.claude_code_config_title') + ':'));\n if (detectedConfig.plan) {\n const planName = detectedConfig.plan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.green(planName));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n if (detectedConfig.apiKey) {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.gray(i18n.t('wizard.api_key_set') + ' (' + detectedConfig.apiKey.slice(0, 4) + '****)'));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n console.log('');\n // 判断是否需要刷新配置\n const configMatches = detectedConfig.plan === chelperConfig.plan &&\n detectedConfig.apiKey === chelperConfig.api_key;\n if (detectedConfig.plan && detectedConfig.apiKey && configMatches) {\n // 配置已同步\n console.log(chalk.green('✅ ' + i18n.t('wizard.config_synced')));\n actionText = i18n.t('wizard.action_refresh_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName });\n }\n else if (detectedConfig.plan || detectedConfig.apiKey) {\n // 配置不一致,需要刷新\n console.log(chalk.yellow('⚠️ ' + i18n.t('wizard.config_out_of_sync')));\n actionText = i18n.t('wizard.action_refresh_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName });\n }\n else {\n // 未配置,需要装载\n console.log(chalk.blue('ℹ️ ' + i18n.t('wizard.config_not_loaded')));\n actionText = i18n.t('wizard.action_load_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName });\n }\n console.log('');\n const choices = [];\n choices.push({ name: '> ' + actionText, value: 'load_glm' });\n // 如果已经配置了 GLM Coding Plan,显示卸载选项\n if (detectedConfig.plan && detectedConfig.apiKey) {\n choices.push({ name: '> ' + i18n.t('wizard.action_unload_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName }), value: 'unload_glm' });\n }\n choices.push({ name: '> ' + i18n.t('wizard.action_mcp_config'), value: 'mcp_config' });\n // 如果已经配置了 GLM Coding Plan,显示启动选项\n if (detectedConfig.plan && detectedConfig.apiKey) {\n choices.push({ name: '> ' + i18n.t('wizard.start_tool', { 'tool': SUPPORTED_TOOLS[toolName].displayName, 'shell': SUPPORTED_TOOLS[toolName].command }), value: 'start_tool' });\n }\n choices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }\n ]);\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (action === 'back') {\n return;\n }\n else if (action === 'load_glm') {\n await this.loadGLMConfig(toolName);\n }\n else if (action === 'unload_glm') {\n await this.unloadGLMConfig(toolName);\n }\n else if (action === 'mcp_config') {\n await this.showMCPMenu(toolName);\n }\n else if (action === 'start_tool') {\n await this.startTool(toolName);\n }\n }\n }\n async startTool(toolName) {\n const tool = SUPPORTED_TOOLS[toolName];\n if (!tool) {\n throw new Error(`Unknown tool: ${toolName}`);\n }\n const spinner = ora({\n text: i18n.t('wizard.starting_tool'),\n spinner: 'star2'\n }).start();\n try {\n // 开启新窗口 执行 tool.command\n execSync(tool.command, { stdio: 'inherit' });\n spinner.succeed(i18n.t('wizard.tool_started'));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.tool_start_failed'));\n throw error;\n }\n }\n async loadGLMConfig(toolName) {\n const spinner = ora({\n text: i18n.t('wizard.loading_glm_config'),\n spinner: 'star2'\n }).start();\n try {\n const config = configManager.getConfig();\n if (!config.plan || !config.api_key) {\n spinner.fail(i18n.t('wizard.missing_config'));\n await new Promise(resolve => setTimeout(resolve, 800));\n return;\n }\n toolManager.loadGLMConfig(toolName, config.plan, config.api_key);\n await new Promise(resolve => setTimeout(resolve, 800));\n spinner.succeed(chalk.green(i18n.t('wizard.glm_config_loaded', { tool: SUPPORTED_TOOLS[toolName].displayName })));\n await new Promise(resolve => setTimeout(resolve, 2000));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.glm_config_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n async unloadGLMConfig(toolName) {\n // 确认卸载操作\n const { confirm } = await this.promptWithHints([\n {\n type: 'confirm',\n name: 'confirm',\n message: i18n.t('wizard.confirm_unload_glm', { tool: SUPPORTED_TOOLS[toolName].displayName }),\n default: false\n }\n ]);\n if (!confirm) {\n return;\n }\n const spinner = ora({\n text: i18n.t('wizard.unloading_glm_config'),\n spinner: 'star2'\n }).start();\n try {\n if (toolName === 'claude-code') {\n // 添加短暂延迟,让动画效果更流畅\n await new Promise(resolve => setTimeout(resolve, 300));\n claudeCodeManager.unloadGLMConfig();\n await new Promise(resolve => setTimeout(resolve, 500));\n spinner.succeed(chalk.green(i18n.t('wizard.glm_config_unloaded')));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n else {\n spinner.fail(i18n.t('wizard.tool_not_supported'));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.glm_config_unload_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n async showMCPMenu(toolName) {\n while (true) {\n this.resetScreen();\n const presetServices = mcpManager.getPresetServices();\n const mcpStatus = mcpManager.getMCPStatus(toolName);\n const title = `${i18n.t('wizard.mcp_menu_title')}`;\n this.createBox(title);\n // 检查是否有任何套餐 MCP 已安装\n const hasInstalledPresetMCP = Array.from(mcpStatus.values()).some(installed => installed);\n // 如果没有安装任何套餐 MCP,显示提示信息\n if (!hasInstalledPresetMCP) {\n console.log(chalk.blue('ℹ️ ' + i18n.t('wizard.no_preset_mcp_installed')));\n console.log('');\n }\n // 内置 MCP 服务\n const choices = [];\n // 如果没有安装任何套餐 MCP,添加一键安装选项\n if (!hasInstalledPresetMCP) {\n choices.push({\n name: '> ' + chalk.green.bold(i18n.t('wizard.action_install_all_mcp')),\n value: 'install_all'\n });\n choices.push(new inquirer.Separator());\n }\n // 添加内置 MCP 服务标题\n choices.push({\n name: chalk.yellow.bold(`✨ ${i18n.t('wizard.builtin_mcp_services')}:`),\n value: 'builtin_header',\n disabled: true\n });\n const builtinChoices = presetServices.map(mcp => ({\n name: ` ${mcpStatus.get(mcp.id) ? '[+]' : '[ ]'} ${mcp.name} ${chalk.gray('(' + mcp.protocol + ')')} - ${chalk.gray(mcp.description)}`,\n value: `builtin:${mcp.id}`\n }));\n choices.push(...builtinChoices);\n // 如果是 Claude Code,显示其他 MCP 服务\n if (toolName === 'claude-code') {\n const builtinIds = presetServices.map(mcp => mcp.id);\n const otherMCPs = claudeCodeManager.getOtherMCPs(builtinIds);\n if (otherMCPs.length > 0) {\n // 添加空行分隔\n choices.push(new inquirer.Separator());\n // 添加其他 MCP 服务标题\n choices.push({\n name: chalk.yellow.bold(`* ${i18n.t('wizard.other_mcp_services')}:`),\n value: 'other_header',\n disabled: true\n });\n const otherChoices = otherMCPs.map(({ id, config }) => ({\n name: ` - ${id} ${chalk.gray('(' + config.type + ')')} ${chalk.blue('[' + i18n.t('wizard.other_mcp') + ']')}`,\n value: `other:${id}`\n }));\n choices.push(...otherChoices);\n }\n }\n choices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });\n const { selectedMCP } = await this.promptWithHints([\n {\n type: 'list',\n name: 'selectedMCP',\n message: i18n.t('wizard.select_mcp'),\n choices,\n pageSize: 15\n }\n ]);\n if (selectedMCP === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (selectedMCP === 'back') {\n return;\n }\n else if (selectedMCP === 'install_all') {\n await this.installAllPresetMCPs(toolName);\n }\n else if (selectedMCP.startsWith('builtin:')) {\n const mcpId = selectedMCP.replace('builtin:', '');\n await this.showMCPDetail(toolName, mcpId);\n }\n else if (selectedMCP.startsWith('other:')) {\n const mcpId = selectedMCP.replace('other:', '');\n await this.showOtherMCPDetail(toolName, mcpId);\n }\n }\n }\n async showMCPDetail(toolName, mcpId) {\n const mcp = mcpManager.getPresetServices().find(m => m.id === mcpId);\n if (!mcp)\n return;\n this.resetScreen();\n const isInstalled = mcpManager.isMCPInstalled(toolName, mcpId);\n const title = `${mcp.name}`;\n this.createBox(title);\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_protocol') + ': ') + chalk.white(mcp.protocol));\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_type') + ': ') + chalk.white(mcp.type));\n // 显示套餐类型(如果该 MCP 支持多套餐)\n const config = configManager.getConfig();\n let planName = null;\n // 对于 Vision MCP (stdio 协议且有 envTemplate),根据已安装的环境变量判断\n if (mcp.protocol === 'stdio' && mcp.envTemplate && isInstalled) {\n // 读取已安装的 MCP 配置\n if (toolName === 'claude-code') {\n const allServers = claudeCodeManager.getAllMCPServers();\n const installedConfig = allServers[mcp.id];\n if (installedConfig && installedConfig.env) {\n const zaiMode = installedConfig.env.Z_AI_MODE;\n // 根据 Z_AI_MODE 判断套餐类型\n if (zaiMode === 'ZAI') {\n planName = i18n.t('wizard.plan_global');\n }\n else {\n // 'ZHIPU' 或无值都认为是中国版\n planName = i18n.t('wizard.plan_china');\n }\n }\n }\n }\n // 对于基于 URL 的 MCP (urlTemplate),根据当前配置判断\n if (!planName && mcp.urlTemplate) {\n const currentPlan = config.plan || 'glm_coding_plan_china';\n planName = currentPlan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n }\n // 对于未安装的 Vision MCP,根据当前配置显示将要使用的套餐类型\n if (!planName && mcp.envTemplate && !isInstalled) {\n const currentPlan = config.plan || 'glm_coding_plan_china';\n planName = currentPlan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n }\n // 显示套餐类型\n if (planName) {\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_plan_type') + ': ') + chalk.green(planName));\n }\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_description') + ': ') + chalk.white(mcp.description));\n // 显示 URL 信息\n if (mcp.protocol === 'sse' || mcp.protocol === 'streamable-http') {\n if (mcp.urlTemplate) {\n const currentPlan = config.plan || 'glm_coding_plan_china';\n const currentUrl = mcp.urlTemplate[currentPlan];\n console.log(chalk.gray(' URL (' + currentPlan + '): ') + chalk.white(currentUrl));\n }\n else if (mcp.url) {\n console.log(chalk.gray(' URL: ') + chalk.white(mcp.url));\n }\n }\n const statusIcon = isInstalled ? '[+]' : '[ ]';\n const statusText = isInstalled ? chalk.green(i18n.t('wizard.installed')) : chalk.gray(i18n.t('wizard.not_installed'));\n console.log(chalk.gray(' ' + statusIcon + ' ' + i18n.t('wizard.mcp_status') + ': ') + statusText);\n const choices = [];\n if (!isInstalled) {\n choices.push({ name: '> ' + i18n.t('wizard.action_install'), value: 'install' });\n }\n else {\n choices.push({ name: '* ' + i18n.t('wizard.action_uninstall'), value: 'uninstall' });\n }\n choices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }\n ]);\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (action === 'back') {\n return;\n }\n if (action === 'install') {\n const spinner = ora({\n text: i18n.t('wizard.installing_mcp'),\n spinner: 'star2'\n }).start();\n try {\n const config = configManager.getConfig();\n mcpManager.installMCP(toolName, mcp, config.api_key, config.plan);\n await new Promise(resolve => setTimeout(resolve, 800));\n spinner.succeed(chalk.green(i18n.t('wizard.mcp_installed')));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.mcp_install_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n else if (action === 'uninstall') {\n const spinner = ora({\n text: i18n.t('wizard.uninstalling_mcp'),\n spinner: 'star2'\n }).start();\n try {\n mcpManager.uninstallMCP(toolName, mcpId);\n await new Promise(resolve => setTimeout(resolve, 800));\n spinner.succeed(chalk.green(i18n.t('wizard.mcp_uninstalled')));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.mcp_uninstall_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n }\n async showOtherMCPDetail(toolName, mcpId) {\n if (toolName !== 'claude-code')\n return;\n const allServers = claudeCodeManager.getAllMCPServers();\n const mcpConfig = allServers[mcpId];\n if (!mcpConfig)\n return;\n this.resetScreen();\n const title = `${mcpId}`;\n this.createBox(title);\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_protocol') + ': ') + chalk.white(mcpConfig.type));\n if (mcpConfig.type === 'stdio') {\n console.log(chalk.gray(' Command: ') + chalk.white(mcpConfig.command || 'N/A'));\n if (mcpConfig.args && mcpConfig.args.length > 0) {\n console.log(chalk.gray(' Args: ') + chalk.white(mcpConfig.args.join(' ')));\n }\n }\n else if (mcpConfig.type === 'sse' || mcpConfig.type === 'http') {\n console.log(chalk.gray(' URL: ') + chalk.white(mcpConfig.url || 'N/A'));\n }\n console.log(chalk.blue('\\n [i] ' + i18n.t('wizard.other_mcp_info')));\n console.log('');\n const choices = [\n { name: '* ' + i18n.t('wizard.action_uninstall'), value: 'uninstall' },\n new inquirer.Separator(),\n { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' },\n { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' }\n ];\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }\n ]);\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (action === 'back') {\n return;\n }\n if (action === 'uninstall') {\n const { confirm } = await this.promptWithHints([\n {\n type: 'confirm',\n name: 'confirm',\n message: i18n.t('wizard.confirm_uninstall_other_mcp'),\n default: false\n }\n ]);\n if (confirm) {\n const spinner = ora(i18n.t('wizard.uninstalling_mcp')).start();\n try {\n claudeCodeManager.uninstallMCP(mcpId);\n spinner.succeed(i18n.t('wizard.mcp_uninstalled'));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.mcp_uninstall_failed'));\n console.error(error);\n }\n }\n }\n }\n async installAllPresetMCPs(toolName) {\n const config = configManager.getConfig();\n if (!config.plan || !config.api_key) {\n console.log(chalk.red('\\n[!] ' + i18n.t('wizard.missing_config')));\n await new Promise(resolve => setTimeout(resolve, 1500));\n return;\n }\n const presetServices = mcpManager.getPresetServices();\n // 显示将要安装的服务列表\n this.resetScreen();\n this.createBox(i18n.t('wizard.action_install_all_mcp'));\n console.log(chalk.yellow(i18n.t('wizard.install_all_mcp_confirm')));\n console.log('');\n presetServices.forEach(mcp => {\n console.log(chalk.gray(' • ') + chalk.white(mcp.name) + chalk.gray(` (${mcp.protocol}) - ${mcp.description}`));\n });\n console.log('');\n const { confirm } = await this.promptWithHints([\n {\n type: 'confirm',\n name: 'confirm',\n message: i18n.t('wizard.select_action'),\n default: true\n }\n ]);\n if (!confirm) {\n return;\n }\n const spinner = ora({\n text: i18n.t('wizard.installing_all_mcp'),\n spinner: 'star2'\n }).start();\n let successCount = 0;\n let failCount = 0;\n const errors = [];\n for (const mcp of presetServices) {\n try {\n spinner.text = i18n.t('wizard.installing_all_mcp') + ` [${successCount + failCount + 1}/${presetServices.length}] ${mcp.name}`;\n mcpManager.installMCP(toolName, mcp, config.api_key, config.plan);\n successCount++;\n await new Promise(resolve => setTimeout(resolve, 300));\n }\n catch (error) {\n failCount++;\n errors.push(`${mcp.name}: ${error}`);\n }\n }\n await new Promise(resolve => setTimeout(resolve, 500));\n if (failCount === 0) {\n spinner.succeed(chalk.green(i18n.t('wizard.all_mcp_installed') + ` (${successCount}/${presetServices.length})`));\n }\n else {\n spinner.warn(chalk.yellow(i18n.t('wizard.all_mcp_install_failed') + ` (${successCount}/${presetServices.length})`));\n if (errors.length > 0) {\n console.log(chalk.red('\\n[!] ' + i18n.t('wizard.mcp_install_failed') + ':'));\n errors.forEach(err => console.log(chalk.gray(' • ') + chalk.red(err)));\n }\n }\n await new Promise(resolve => setTimeout(resolve, 1500));\n }\n}\nexport const wizard = Wizard.getInstance();\n"}}}}