bookworm-boot/setup-all.js

617 lines
24 KiB
JavaScript
Raw Normal View History

#!/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);
});