#!/usr/bin/env node 'use strict'; /** * Bookworm Portable - 全自动安装引擎 v3.0 * @module setup-all */ // W3: Node.js 版本检查 if (parseInt(process.versions.node) < 14) { console.error(' [!!] Node.js 版本过低 (' + process.version + '), 需要 14.0+'); console.error(' 请更新: https://nodejs.org/'); process.exit(1); } const { execSync, spawnSync, spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const readline = require('readline'); const crypto = require('crypto'); const os = require('os'); // ─── 配置 ─── const HOME = os.homedir(); const BOOT_DIR = path.join(HOME, 'bookworm-boot'); const CLAUDE_DIR = path.join(HOME, '.claude'); const GITEA_BOOT = 'https://code.letcareme.com/bookworm/bookworm-boot.git'; const GITEA_CONFIG = 'https://code.letcareme.com/bookworm/bookworm-config.git'; const NPM_MIRROR = 'https://registry.npmmirror.com'; const SCRIPT_DIR = __dirname; // ─── 颜色输出 ─── const c = { reset: '\x1b[0m', bold: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', dim: '\x1b[90m', }; function ok(msg) { console.log(` ${c.green}[OK]${c.reset} ${msg}`); } function warn(msg) { console.log(` ${c.yellow}[!]${c.reset} ${msg}`); } function fail(msg) { console.log(` ${c.red}[!!]${c.reset} ${msg}`); } function info(msg) { console.log(` ${c.dim}[..]${c.reset} ${msg}`); } function step(n, total, msg) { console.log(`\n ${c.bold}[${n}/${total}]${c.reset} ${c.cyan}${msg}${c.reset}`); } // ─── 工具函数 ─── function hasCmd(cmd) { if (!/^[a-zA-Z0-9._-]+$/.test(cmd)) return false; // B3: 防命令注入 try { execSync(`where ${cmd}`, { stdio: 'pipe' }); return true; } catch { return false; } } function run(cmd, opts = {}) { try { return execSync(cmd, { stdio: opts.silent ? 'pipe' : 'inherit', encoding: 'utf8', timeout: opts.timeout || 300000, env: { ...process.env, ...opts.env }, ...opts, }); } catch (e) { if (opts.ignoreError) return ''; throw e; } } function runSilent(cmd) { try { return execSync(cmd, { stdio: 'pipe', encoding: 'utf8', timeout: 60000 }); } catch { return ''; } } function wingetInstall(id, name) { if (!hasCmd('winget')) { warn(`winget 不可用, 请手动安装 ${name}`); return false; } info(`通过 winget 安装 ${name}...`); try { run(`winget install ${id} --accept-source-agreements --accept-package-agreements --silent`, { timeout: 600000 }); refreshPath(); ok(`${name} 安装成功`); return true; } catch (e) { fail(`${name} 安装失败: ${e.message}`); return false; } } function refreshPath() { try { const sysPath = runSilent('reg query "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v Path') .match(/REG_\w+\s+(.+)/)?.[1] || ''; const usrPath = runSilent('reg query "HKCU\\Environment" /v Path') .match(/REG_\w+\s+(.+)/)?.[1] || ''; const extra = [ 'C:\\Program Files\\nodejs', 'C:\\Program Files\\Git\\cmd', 'C:\\Program Files\\Git\\usr\\bin', 'C:\\Program Files\\PowerShell\\7', path.join(HOME, 'AppData\\Local\\Microsoft\\WinGet\\Packages'), path.join(HOME, 'AppData\\Roaming\\npm'), ].join(';'); // I2: 动态扫描 Python 安装路径 const pyBase = path.join(HOME, 'AppData', 'Local', 'Programs', 'Python'); let pyPaths = ''; try { if (fs.existsSync(pyBase)) { for (const d of fs.readdirSync(pyBase)) { pyPaths += ';' + path.join(pyBase, d) + ';' + path.join(pyBase, d, 'Scripts'); } } } catch {} process.env.PATH = `${sysPath};${usrPath};${extra}${pyPaths}`; } catch {} } function askPassword(prompt) { return new Promise((resolve) => { process.stdout.write(prompt); // Windows: 用 PowerShell 隐藏输入 try { const result = execSync( 'powershell -Command "[Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Read-Host -AsSecureString)))"', { stdio: ['inherit', 'pipe', 'pipe'], encoding: 'utf8', timeout: 120000 } ).trim(); resolve(result); } catch { // W5: 回退明文输入, 给出警告 warn('PowerShell 不可用, 密码将以明文显示'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('', (ans) => { rl.close(); resolve(ans.trim()); }); } }); } // ─── 加密/解密 (与 crypto-helper.js 同格式) ─── function decryptSecrets(filePath, password) { const data = fs.readFileSync(filePath); const magic = data.slice(0, 6).toString(); if (magic !== 'BWENC1') throw new Error('WRONG_FORMAT'); const salt = data.slice(6, 22); const encrypted = data.slice(22); const derived = crypto.pbkdf2Sync(password, salt, 600000, 48, 'sha256'); const key = derived.slice(0, 32); const iv = derived.slice(32, 48); const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); try { return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8'); } catch { throw new Error('WRONG_PASSWORD'); } } function escVbs(s) { return s.replace(/"/g, '""'); } // B4: 防 VBS 注入 function createShortcut(name, target, workDir) { const vbs = path.join(os.tmpdir(), `bw_sc_${Date.now()}.vbs`); const desktop = path.join(HOME, 'Desktop'); const lnkPath = escVbs(path.join(desktop, name + '.lnk')); fs.writeFileSync(vbs, `Set ws = CreateObject("WScript.Shell") Set sc = ws.CreateShortcut("${lnkPath}") sc.TargetPath = "${escVbs(target)}" sc.WorkingDirectory = "${escVbs(workDir)}" sc.Description = "Bookworm Smart Assistant" sc.Save `); try { execSync(`cscript //nologo "${vbs}"`, { stdio: 'pipe' }); return true; } catch { return false; } finally { try { fs.unlinkSync(vbs); } catch {} } } // W8: 动态检测 Git Bash 路径 function findGitBash() { const candidates = ['C:\\Program Files\\Git\\bin\\bash.exe', 'C:\\Program Files (x86)\\Git\\bin\\bash.exe']; try { const gitPath = runSilent('where git').trim().split('\n')[0]; if (gitPath) candidates.unshift(path.join(path.dirname(path.dirname(gitPath)), 'bin', 'bash.exe')); } catch {} return candidates.find(p => fs.existsSync(p)) || candidates[0]; } // ─── Banner ─── function banner() { console.log(` ${c.cyan}╔══════════════════════════════════════════════════╗ ║ ____ _ ║ ║ | __ ) ___ ___ | | ____ _____ _ __ ___ ║ ║ | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| ║ ║ | |_) | (_) | (_) | < \\ V V / (_) | | ║ ║ |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| ║ ║ ║ ║ ${c.bold}全自动安装引擎 v3.0${c.cyan} ║ ║ 双击即装: Node + Git + Python + PS7 + Claude + MCP║ ╚══════════════════════════════════════════════════╝${c.reset} `); } // ═══════════════════════════════════════════ // 主流程 // ═══════════════════════════════════════════ async function main() { // ─── 启动模式: 已安装过 → 静默更新 + 直接启动 ─── const isInstalled = hasCmd('claude') && fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md')); const startOnly = process.argv.includes('--start') || process.argv.includes('-s'); if (isInstalled && startOnly) { return quickStart(); } banner(); const TOTAL = 9; let errors = 0; // ─── 1. Git ─── step(1, TOTAL, '安装 Git'); if (hasCmd('git')) { ok('Git 已安装'); } else { if (!wingetInstall('Git.Git', 'Git')) errors++; } // ─── 2. Python ─── step(2, TOTAL, '安装 Python'); if (hasCmd('python') || hasCmd('python3') || hasCmd('py')) { ok('Python 已安装'); } else { if (!wingetInstall('Python.Python.3.12', 'Python 3.12')) errors++; refreshPath(); } // ─── 3. PowerShell 7 ─── step(3, TOTAL, '安装 PowerShell 7'); if (hasCmd('pwsh')) { ok('PowerShell 7 已安装'); } else { if (wingetInstall('Microsoft.PowerShell', 'PowerShell 7')) { // 设 PS7 为 Windows Terminal 默认配置文件 try { const wtSettings = path.join(HOME, 'AppData', 'Local', 'Packages', 'Microsoft.WindowsTerminal_8wekyb3d8bbwe', 'LocalState', 'settings.json'); if (fs.existsSync(wtSettings)) { let wt = JSON.parse(fs.readFileSync(wtSettings, 'utf8')); // 找到 PS7 的 profile GUID const ps7Profile = (wt.profiles?.list || []).find(p => p.source === 'Windows.Terminal.PowershellCore' || (p.name || '').includes('PowerShell 7') ); if (ps7Profile && ps7Profile.guid) { wt.defaultProfile = ps7Profile.guid; fs.writeFileSync(wtSettings, JSON.stringify(wt, null, 4)); ok('PS7 已设为 Windows Terminal 默认终端'); } } } catch { warn('Windows Terminal 默认配置未修改 - 不影响使用'); } } else { errors++; } } // ─── 4. Claude Code ─── step(4, TOTAL, '安装 Claude Code'); // I3: 用 --registry 参数, 不污染全局 .npmrc if (hasCmd('claude')) { ok('Claude Code 已安装'); } else { info('通过 npm 安装 Claude Code - 淘宝镜像加速...'); try { run(`npm i -g @anthropic-ai/claude-code --registry ${NPM_MIRROR}`, { timeout: 600000 }); ok('Claude Code 安装成功'); } catch { fail('Claude Code 安装失败'); errors++; } } // ─── 5. 克隆 Bookworm 配置 ─── step(5, TOTAL, '同步 Bookworm 配置'); // 设置 git credential helper run('git config --global credential.helper manager', { ignoreError: true, silent: true }); // 克隆 bookworm-boot if (fs.existsSync(path.join(BOOT_DIR, '.git'))) { info('bookworm-boot 已存在, 更新...'); run('git pull', { cwd: BOOT_DIR, ignoreError: true }); } else { info('首次下载 bookworm-boot (需输入 Gitea 用户名密码)...'); try { run(`git clone "${GITEA_BOOT}" "${BOOT_DIR}"`); ok('bookworm-boot 克隆成功'); } catch { fail('bookworm-boot 克隆失败 - 检查网络和 Gitea 凭证'); errors++; } } // 克隆 bookworm-config → ~/.claude if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) { info('.claude 配置已存在, 更新...'); try { const stashOut = run('git stash', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }) || ''; run('git pull --rebase', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); if (stashOut.includes('Saved working directory')) { run('git stash pop', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); } } catch {} } else if (!fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'))) { info('首次下载 .claude 配置...'); // 备份现有 if (fs.existsSync(CLAUDE_DIR)) { const backup = CLAUDE_DIR + '.bak.' + Date.now(); fs.renameSync(CLAUDE_DIR, backup); ok(`现有 .claude 已备份到 ${path.basename(backup)}`); } try { run(`git clone --depth 1 "${GITEA_CONFIG}" "${CLAUDE_DIR}"`); ok('.claude 配置克隆成功'); } catch { fail('.claude 配置克隆失败'); errors++; } } else { ok('.claude 配置已存在'); } // 确保本地目录 for (const d of ['debug', 'sessions', 'cache', 'backups', 'memory', 'projects']) { const p = path.join(CLAUDE_DIR, d); if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); } // ─── 6. 凭证解密 ─── step(6, TOTAL, '解密凭证'); const secretsFile = path.join(BOOT_DIR, 'secrets.enc'); if (!fs.existsSync(secretsFile)) { warn('secrets.enc 不存在, 跳过凭证解密'); } else if (process.env.ANTHROPIC_API_KEY) { ok('API Key 已设置 (缓存有效)'); } else { let decrypted = false; for (let attempt = 1; attempt <= 3; attempt++) { const label = attempt > 1 ? ` 重新输入主密码 (第 ${attempt}/3 次): ` : ' 输入主密码解密凭证: '; const password = await askPassword(label); try { const text = decryptSecrets(secretsFile, password); // 注入环境变量 (B5: 不打印 key 名称, 防截屏泄露) let injectedCount = 0; for (const line of text.split('\n')) { const trimmed = line.trim(); if (!trimmed || !trimmed.includes('=')) continue; const eqIdx = trimmed.indexOf('='); const key = trimmed.slice(0, eqIdx).trim(); const val = trimmed.slice(eqIdx + 1).trim(); if (key && val) { process.env[key] = val; injectedCount++; } } ok(`已注入 ${injectedCount} 个环境变量`); decrypted = true; break; } catch (e) { if (e.message === 'WRONG_PASSWORD') { const remaining = 3 - attempt; if (remaining > 0) fail(`密码错误, 剩余重试: ${remaining} 次`); } else if (e.message === 'WRONG_FORMAT') { fail('secrets.enc 格式不兼容, 请联系管理员'); break; } } } if (!decrypted) { fail('凭证解密失败 — Claude Code 将以登录模式启动'); errors++; } } // 渲染 settings.json const templateFile = path.join(CLAUDE_DIR, 'settings.template.json'); const settingsFile = path.join(CLAUDE_DIR, 'settings.json'); if (fs.existsSync(templateFile)) { let tpl = fs.readFileSync(templateFile, 'utf8'); const claudeRoot = CLAUDE_DIR.replace(/\\/g, '/'); tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, claudeRoot); tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠 fs.writeFileSync(settingsFile, tpl); ok('settings.json 已渲染'); } // 渲染 settings.local.template.json const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json'); const localSet = path.join(CLAUDE_DIR, 'settings.local.json'); if (fs.existsSync(localTpl)) { let tpl = fs.readFileSync(localTpl, 'utf8'); tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/')); tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠 tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username); fs.writeFileSync(localSet, tpl); ok('settings.local.json 已渲染'); } // ─── 7. MCP + hooks 依赖 ─── step(7, TOTAL, 'MCP 与 hooks 依赖'); // MCP 配置写入 ~/.claude.json (Claude Code 的全局 MCP 存储位置) const claudeJson = path.join(HOME, '.claude.json'); try { let globalCfg = {}; if (fs.existsSync(claudeJson)) { globalCfg = JSON.parse(fs.readFileSync(claudeJson, 'utf8')); } // 基础 MCP 列表 (npx 方式, 无需预装, 首次调用自动下载) const baseMcps = { 'context7': { command: 'npx.cmd', args: ['-y', '@upstash/context7-mcp@latest'], type: 'stdio' }, 'sequential-thinking': { command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-sequential-thinking@latest'], type: 'stdio' }, 'playwright': { command: 'npx.cmd', args: ['-y', '@playwright/mcp@latest', '--headless'], type: 'stdio' }, 'firecrawl': { command: 'npx.cmd', args: ['-y', 'firecrawl-mcp'], type: 'stdio', env: { FIRECRAWL_API_KEY: '${FIRECRAWL_API_KEY}' } }, 'github': { command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-github'], type: 'stdio', env: { GITHUB_PERSONAL_ACCESS_TOKEN: '${GITHUB_PERSONAL_ACCESS_TOKEN}' } }, 'linear': { type: 'http', url: 'https://mcp.linear.app/mcp' }, 'figma': { type: 'http', url: 'https://mcp.figma.com/mcp' }, 'supabase': { type: 'http', url: 'https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo' }, }; // 合并: 不覆盖用户已有配置 if (!globalCfg.mcpServers) globalCfg.mcpServers = {}; let added = 0; for (const [name, cfg] of Object.entries(baseMcps)) { if (!globalCfg.mcpServers[name]) { globalCfg.mcpServers[name] = cfg; added++; } } fs.writeFileSync(claudeJson, JSON.stringify(globalCfg, null, 2)); ok(`MCP 配置已写入 ~/.claude.json (新增 ${added} 个, 总计 ${Object.keys(globalCfg.mcpServers).length} 个)`); } catch (e) { warn('MCP 配置写入失败: ' + e.message); } // npm install in .claude for hooks if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) { info('安装 hooks 依赖...'); run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); ok('hooks 依赖已安装'); } // Python MCP 依赖 const pyCmd = hasCmd('python') ? 'python' : (hasCmd('python3') ? 'python3' : (hasCmd('py') ? 'py' : null)); if (pyCmd) { info('安装 Python MCP 依赖...'); run(`${pyCmd} -m pip install askui scrapling --quiet`, { ignoreError: true, silent: true }); ok('Python MCP 依赖已安装'); } // ─── 8. 桌面快捷方式 ─── step(8, TOTAL, '创建桌面快捷方式'); // 启动脚本: 用 pwsh/powershell 运行 install.ps1 const launchBat = path.join(BOOT_DIR, '启动Bookworm.bat'); if (fs.existsSync(launchBat)) { if (createShortcut('Bookworm', launchBat, BOOT_DIR)) ok('Bookworm 快捷方式'); } else { // 回退: 直接创建 claude 启动快捷方式 const claudePath = runSilent('where claude').trim().split('\n')[0]; if (claudePath) { if (createShortcut('Bookworm', claudePath, HOME)) ok('Bookworm 快捷方式'); } } const updateBat = path.join(BOOT_DIR, '更新并启动Bookworm.bat'); if (fs.existsSync(updateBat)) { if (createShortcut('更新Bookworm', updateBat, BOOT_DIR)) ok('更新Bookworm 快捷方式'); } // ─── 9. 完成 ─── step(9, TOTAL, '安装完成'); console.log(` ${c.green}╔══════════════════════════════════════════════════╗ ║ ║ ║ 安装完成! ║ ║ ║ ║ [v] Node.js [v] Git [v] Python ║ ║ [v] PS7 [v] Claude [v] MCP ║ ║ [v] Bookworm - 92 Skills / 18 Agents ║ ║ ║ ║ 桌面快捷方式: Bookworm / 更新Bookworm ║ ║ ║ ╚══════════════════════════════════════════════════╝${c.reset} `); if (errors > 0) { warn(`安装过程中有 ${errors} 个警告, 请查看上方日志`); } // 打开使用教程 const guide = path.join(BOOT_DIR, 'guide.html'); if (fs.existsSync(guide)) { try { execSync(`start "" "${guide}"`, { stdio: 'pipe' }); } catch {} } // 询问是否启动 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('\n 按回车启动 Bookworm (或输入 n 退出): ', (ans) => { rl.close(); if (ans.trim().toLowerCase() === 'n') return; console.log(`\n ${c.cyan}正在启动 Claude Code...${c.reset}\n`); // 设置必要环境变量 const env = { ...process.env }; // W10: 追加而非覆盖用户已有 NO_PROXY const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || ''; const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains; env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash(); const child = spawn('claude', [], { stdio: 'inherit', env, cwd: HOME, shell: true, }); child.on('exit', () => process.exit(0)); }); } // ═══════════════════════════════════════════ // 快速启动模式: 静默更新 + 直接启动 (已安装过的机器) // ═══════════════════════════════════════════ async function quickStart() { console.log(` ${c.cyan}Bookworm 快速启动${c.reset} — 检查更新中...`); let updated = false; // 1. 静默更新 bookworm-boot if (fs.existsSync(path.join(BOOT_DIR, '.git'))) { try { const before = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim(); run(`git -C "${BOOT_DIR}" pull --ff-only`, { ignoreError: true, silent: true, timeout: 15000 }); const after = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim(); if (before !== after) { ok('bookworm-boot 已更新'); updated = true; } } catch {} } // 2. 静默更新 bookworm-config (.claude) if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) { try { const before = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim(); const stashOut = run(`git -C "${CLAUDE_DIR}" stash`, { ignoreError: true, silent: true }) || ''; run(`git -C "${CLAUDE_DIR}" pull --rebase`, { ignoreError: true, silent: true, timeout: 15000 }); if (stashOut.includes('Saved working directory')) { run(`git -C "${CLAUDE_DIR}" stash pop`, { ignoreError: true, silent: true }); } const after = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim(); if (before !== after) { ok('.claude 配置已更新'); updated = true; } } catch {} } // 3. 更新后重新渲染模板 if (updated) { const templateFile = path.join(CLAUDE_DIR, 'settings.template.json'); const settingsFile = path.join(CLAUDE_DIR, 'settings.json'); if (fs.existsSync(templateFile)) { let tpl = fs.readFileSync(templateFile, 'utf8'); tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/')); tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); fs.writeFileSync(settingsFile, tpl); } const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json'); const localSet = path.join(CLAUDE_DIR, 'settings.local.json'); if (fs.existsSync(localTpl)) { let tpl = fs.readFileSync(localTpl, 'utf8'); tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/')); tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username); fs.writeFileSync(localSet, tpl); } ok('配置模板已重新渲染'); // hooks 依赖更新 if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) { run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true }); } } if (!updated) ok('已是最新版本'); // 4. 启动 Claude Code console.log(`\n ${c.cyan}启动 Claude Code...${c.reset}\n`); const env = { ...process.env }; const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || ''; const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains; env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash(); const child = spawn('claude', [], { stdio: 'inherit', env, cwd: HOME, shell: true }); child.on('exit', () => process.exit(0)); } main().catch(e => { fail(`安装引擎异常: ${e.message}`); console.error(e.stack); process.exit(1); });