368 lines
12 KiB
JavaScript
368 lines
12 KiB
JavaScript
#!/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/<target>/.claude
|
||
// C:\\Users\\janson9527us\\.claude → C:\\Users\\<target>\\.claude
|
||
// C:\Users\janson9527us\.claude → C:\Users\<target>\.claude
|
||
// /c/Users/janson9527us/.claude → /c/Users/<target>/.claude (WSL/Git Bash)
|
||
// /mnt/c/Users/janson9527us/.claude → /mnt/c/Users/<target>/.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);
|