#!/usr/bin/env node /** * Bookworm 系统迁移脚本 * 将完整的 Bookworm Smart Assistant 系统复刻到目标用户账户 * * 用法: * node migrate-to-user.js <目标用户名> * node migrate-to-user.js janson9527us * * 可选参数: * --dry-run 仅预览,不实际复制 * --force 覆盖已存在的目标文件 */ const fs = require('fs'); const path = require('path'); // ─── 参数解析 ──────────────────────────────────────────── const args = process.argv.slice(2); const flags = args.filter(a => a.startsWith('--')); const positional = args.filter(a => !a.startsWith('--')); const DRY_RUN = flags.includes('--dry-run'); const FORCE = flags.includes('--force'); const TARGET_USER = positional[0]; if (!TARGET_USER) { console.error('用法: node migrate-to-user.js <目标用户名> [--dry-run] [--force]'); console.error('示例: node migrate-to-user.js janson9527us'); process.exit(1); } // ─── 路径定义 ──────────────────────────────────────────── const SOURCE_ROOT = 'C:\\Users\\janson9527us\\.claude'; const TARGET_ROOT = `C:\\Users\\${TARGET_USER}\\.claude`; const SOURCE_HOME = 'C:\\Users\\janson9527us'; const TARGET_HOME = `C:\\Users\\${TARGET_USER}`; // 检查源目录 if (!fs.existsSync(SOURCE_ROOT)) { console.error(`错误: 源目录不存在: ${SOURCE_ROOT}`); process.exit(1); } // 检查目标用户目录 if (!fs.existsSync(TARGET_HOME)) { console.error(`错误: 目标用户目录不存在: ${TARGET_HOME}`); process.exit(1); } // 检查目标是否已存在 if (fs.existsSync(TARGET_ROOT) && !FORCE) { console.error(`警告: 目标目录已存在: ${TARGET_ROOT}`); console.error('使用 --force 参数覆盖'); process.exit(1); } // ─── 需要复制的目录/文件清单 ────────────────────────────── // 核心配置文件 const CORE_FILES = [ 'CLAUDE.md', 'settings.json', 'skills-index.json', 'stats-compiled.json', 'SKILL-REGISTRY.md', 'stats-cache.json', ]; // 需要完整复制的目录(排除规则在下方) const COPY_DIRS = [ 'hooks', 'scripts', 'agents', 'skills', 'docs', 'mcp-servers', 'plugins', 'cache', 'debug', ]; // 需要排除的目录/文件模式 const EXCLUDE_PATTERNS = [ // 会话相关(不需要复制) /^statsig[/\\]/, /^tasks[/\\]/, /^teams[/\\]/, /^projects[/\\]/, /^plans[/\\]/, /^te[/\\]/, /\.update\.lock$/, // node_modules(太大,目标端重新安装) /node_modules[/\\]/, /node_modules$/, // git 对象(太大,且不需要) /\.git[/\\]objects[/\\]/, /\.git[/\\]index$/, // debug 日志数据(运行时生成,可跳过大文件) /^debug[/\\]weights-history[/\\]/, // 二进制和平台特定文件 /\.exe$/, /\.dll$/, /\.node$/, /esbuild[/\\]/, /@esbuild[/\\]/, /@rollup[/\\]rollup-/, ]; // ─── 工具函数 ──────────────────────────────────────────── function shouldExclude(relativePath) { return EXCLUDE_PATTERNS.some(pattern => pattern.test(relativePath)); } /** * 递归获取目录下所有文件(相对路径) */ function walkDir(dir, baseDir = dir) { const results = []; if (!fs.existsSync(dir)) return results; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const relPath = path.relative(baseDir, fullPath); if (entry.isDirectory()) { // 快速跳过已知大目录 if (entry.name === 'node_modules' || entry.name === '.pnpm') continue; results.push(...walkDir(fullPath, baseDir)); } else { results.push(relPath); } } return results; } /** * 确保目录存在 */ function ensureDir(dirPath) { if (!DRY_RUN) { fs.mkdirSync(dirPath, { recursive: true }); } } /** * 复制文件 */ function copyFile(src, dest) { if (DRY_RUN) { console.log(` [DRY] ${src} → ${dest}`); return; } ensureDir(path.dirname(dest)); fs.copyFileSync(src, dest); } /** * 替换文件中的路径引用 */ function rewritePaths(content) { // 替换各种格式的路径: // C:/Users/janson9527us/.claude → C:/Users//.claude // C:\\Users\\janson9527us\\.claude → C:\\Users\\\\.claude // C:\Users\janson9527us\.claude → C:\Users\\.claude // /c/Users/janson9527us/.claude → /c/Users//.claude (WSL/Git Bash) // /mnt/c/Users/janson9527us/.claude → /mnt/c/Users//.claude (WSL) let result = content; // JSON 双反斜杠格式: C:\\Users\\janson9527us result = result.replace( /C:\\\\Users\\\\home/g, `C:\\\\Users\\\\${TARGET_USER}` ); // 正斜杠格式: C:/Users/janson9527us result = result.replace( /C:\/Users\/home/g, `C:/Users/${TARGET_USER}` ); // 单反斜杠格式: C:\Users\janson9527us (非JSON文本中) result = result.replace( /C:\\Users\\janson9527us(?!\\\\)/g, `C:\\Users\\${TARGET_USER}` ); // WSL 路径: /mnt/c/Users/janson9527us result = result.replace( /\/mnt\/c\/Users\/home/g, `/mnt/c/Users/${TARGET_USER}` ); // Git Bash 路径: /c/Users/janson9527us result = result.replace( /\/c\/Users\/home/g, `/c/Users/${TARGET_USER}` ); return result; } // ─── 主逻辑 ───────────────────────────────────────────── console.log('╔════════════════════════════════════════════════╗'); console.log('║ Bookworm Smart Assistant - 系统迁移工具 ║'); console.log('╠════════════════════════════════════════════════╣'); console.log(`║ 源: ${SOURCE_ROOT.padEnd(40)}║`); console.log(`║ 目标: ${TARGET_ROOT.padEnd(38)}║`); console.log(`║ 模式: ${DRY_RUN ? '预览 (dry-run)' : FORCE ? '覆盖写入' : '正常写入'}${''.padEnd(DRY_RUN ? 24 : FORCE ? 28 : 28)}║`); console.log('╚════════════════════════════════════════════════╝'); console.log(''); let copied = 0; let skipped = 0; let rewritten = 0; let errors = 0; // 需要路径重写的文件扩展名 const REWRITE_EXTENSIONS = new Set(['.json', '.js', '.md', '.sh', '.py', '.yaml', '.yml']); /** * 处理单个文件的复制 */ function processFile(relPath, srcBase, destBase) { const srcFile = path.join(srcBase, relPath); const destFile = path.join(destBase, relPath); // 排除检查 const checkPath = path.relative(SOURCE_ROOT, srcFile).replace(/\\/g, '/'); if (shouldExclude(checkPath)) { skipped++; return; } try { const ext = path.extname(relPath).toLowerCase(); if (REWRITE_EXTENSIONS.has(ext)) { // 文本文件: 读取 → 路径替换 → 写入 const content = fs.readFileSync(srcFile, 'utf-8'); const newContent = rewritePaths(content); if (DRY_RUN) { if (content !== newContent) { console.log(` [REWRITE] ${relPath}`); rewritten++; } else { console.log(` [COPY] ${relPath}`); } } else { ensureDir(path.dirname(destFile)); fs.writeFileSync(destFile, newContent, 'utf-8'); if (content !== newContent) rewritten++; } } else { // 二进制文件: 直接复制 copyFile(srcFile, destFile); } copied++; } catch (err) { console.error(` [ERROR] ${relPath}: ${err.message}`); errors++; } } // Step 1: 复制核心配置文件 console.log('▸ Step 1/3: 复制核心配置文件...'); for (const file of CORE_FILES) { const srcFile = path.join(SOURCE_ROOT, file); if (fs.existsSync(srcFile)) { processFile(file, SOURCE_ROOT, TARGET_ROOT); console.log(` ✓ ${file}`); } else { console.log(` - ${file} (不存在,跳过)`); } } console.log(''); // Step 2: 复制目录 console.log('▸ Step 2/3: 复制目录结构...'); for (const dir of COPY_DIRS) { const srcDir = path.join(SOURCE_ROOT, dir); if (!fs.existsSync(srcDir)) { console.log(` - ${dir}/ (不存在,跳过)`); continue; } const files = walkDir(srcDir, SOURCE_ROOT); const validFiles = files.filter(f => !shouldExclude(f)); console.log(` ▸ ${dir}/ (${validFiles.length} 文件)`); for (const file of files) { processFile(file, SOURCE_ROOT, TARGET_ROOT); } } console.log(''); // Step 3: 创建必要的空目录 console.log('▸ Step 3/3: 初始化运行时目录...'); const INIT_DIRS = [ 'debug', 'debug/weights-history', 'backups', ]; for (const dir of INIT_DIRS) { const target = path.join(TARGET_ROOT, dir); if (!DRY_RUN) { ensureDir(target); console.log(` ✓ ${dir}/`); } else { console.log(` [DRY] mkdir ${dir}/`); } } console.log(''); // ─── 特殊处理: settings.json 的 env.HOME ──────────────── const targetSettings = path.join(TARGET_ROOT, 'settings.json'); if (!DRY_RUN && fs.existsSync(targetSettings)) { try { const settings = JSON.parse(fs.readFileSync(targetSettings, 'utf-8')); // 确保 HOME 指向新用户目录 if (settings.env && settings.env.HOME) { settings.env.HOME = `C:\\Users\\${TARGET_USER}`; } fs.writeFileSync(targetSettings, JSON.stringify(settings, null, 2), 'utf-8'); console.log('▸ 特殊处理: settings.json env.HOME 已更新'); } catch (e) { console.error(` [WARN] settings.json 后处理失败: ${e.message}`); } } // ─── 汇总报告 ──────────────────────────────────────────── console.log(''); console.log('═══════════════════════════════════════════════'); console.log(' 迁移完成!'); console.log(` 复制文件: ${copied}`); console.log(` 路径重写: ${rewritten}`); console.log(` 跳过文件: ${skipped}`); console.log(` 错误: ${errors}`); console.log('═══════════════════════════════════════════════'); console.log(''); if (!DRY_RUN && errors === 0) { console.log('后续步骤:'); console.log(''); console.log(` 1. 切换到 ${TARGET_USER} 账户`); console.log(` 2. 打开终端,进入 C:\\Users\\${TARGET_USER}`); console.log(' 3. 运行 claude 启动 Claude Code'); console.log(' 4. 如果 hooks/__tests__ 需要测试,在该目录运行 pnpm install'); console.log(' 5. 如果 mcp-servers/browserbase 需要使用,在该目录运行 npm install'); console.log(''); console.log('环境变量 (可选,paths.config.js 已自动适配):'); console.log(` set CLAUDE_HOME=C:\\Users\\${TARGET_USER}\\.claude`); console.log(''); } if (DRY_RUN) { console.log('这是预览模式。移除 --dry-run 参数执行实际迁移。'); } process.exit(errors > 0 ? 1 : 0);