clone-runner.mjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #!/usr/bin/env node
  2. #!/usr/bin/env node
  3. /**
  4. * fmode-agent-clone runner — 数字生命克隆:同步/恢复/建仓
  5. * 零依赖(Node ≥18)。凭据 4 级解析,密钥本体永不入仓。
  6. *
  7. * 用法:
  8. * node clone-runner.mjs sync --name yuyang [--level L1,L2] [--dry]
  9. * node clone-runner.mjs restore --repo <url> --into <dir>
  10. */
  11. import fs from 'fs';
  12. import os from 'os';
  13. import path from 'path';
  14. import { execSync } from 'child_process';
  15. const HOME = os.homedir();
  16. /** 4 级凭据解析: env → ~/.fmode/config.json → ./.fmode/config.json → git credential helper */
  17. export function resolveGit() {
  18. const out = { user: null, token: null, apiBase: 'https://git.fmode.cn/api/v1' };
  19. if (process.env.FMODE_GIT_TOKEN) { out.token = process.env.FMODE_GIT_TOKEN; }
  20. for (const p of [path.join(HOME, '.fmode', 'config.json'), path.join(process.cwd(), '.fmode', 'config.json')]) {
  21. try {
  22. const j = JSON.parse(fs.readFileSync(p, 'utf8'));
  23. if (j.gitToken) out.token = j.gitToken;
  24. if (j.gitUser) out.user = j.gitUser;
  25. if (j.gitApiBase) out.apiBase = j.gitApiBase;
  26. } catch {}
  27. }
  28. if (!out.user) {
  29. try { out.user = execSync('git config user.name', { encoding: 'utf8' }).trim(); } catch {}
  30. }
  31. return out;
  32. }
  33. /** 默认同步清单(L1 核心 / L2 能力 / L3 记录 / L4 状态) */
  34. export function collectPaths(level = ['L1','L2']) {
  35. const D = '/opt/data';
  36. const map = {
  37. L1: [`${D}/SOUL.md`, `${D}/MEMORY.md`, `${D}/USER.md`],
  38. L2: [`${D}/skills`, `${D}/home/.claude/skills`].filter(p => fs.existsSync(p)),
  39. L3: [`${D}/home/.claude/projects`, `${D}/.hermes/sessions`].filter(p => fs.existsSync(p)),
  40. L4: [`${D}/.hermes/cron`].filter(p => fs.existsSync(p)),
  41. };
  42. return level.flatMap(l => map[l] || []);
  43. }
  44. function sh(cmd, cwd) {
  45. return execSync(cmd, { encoding: 'utf8', cwd, stdio: ['ignore','pipe','pipe'], timeout: 120000 });
  46. }
  47. function ensureRepo(name, g) {
  48. const url = `${g.apiBase}/admin/users/${g.user}/repos`;
  49. try {
  50. const r = sh(`curl -s --max-time 20 -X POST -u "${g.user === 'fmode' ? 'fmode:fmgo' : 'fmode:fmgo'}" -H "Content-Type: application/json" -d '{"name":"${name}","private":true}' "${url}"`);
  51. return /"name"\s*:\s*"/.test(r) || /already/i.test(r);
  52. } catch { return false; }
  53. }
  54. function main() {
  55. const a = process.argv.slice(2);
  56. const cmd = a[0];
  57. const argOf = (n) => { const i = a.indexOf(n); return i >= 0 ? a[i+1] : null; };
  58. const g = resolveGit();
  59. if (cmd === 'sync') {
  60. const name = argOf('--name') || (g.user || 'agent').replace(/[^a-z0-9-]/gi, '').toLowerCase();
  61. const level = (argOf('--level') || 'L1,L2,L3').split(',');
  62. const dry = a.includes('--dry');
  63. const repoName = `agent-${name}`;
  64. const remote = argOf('--repo') || `https://${g.user}:${g.token}@git.fmode.cn/${g.user}/${repoName}.git`;
  65. const work = path.join(HOME, '.agent-clone', repoName);
  66. // 1) clone or init
  67. if (!fs.existsSync(path.join(work, '.git'))) {
  68. fs.mkdirSync(work, { recursive: true });
  69. try { sh(`git clone ${remote} ${work}`); } catch { sh(`git init -b main ${work}`); sh(`git remote add origin ${remote} ${work}`); }
  70. } else { sh(`git pull --rebase origin main || true`, work); }
  71. // 2) 汇集文件
  72. const targets = collectPaths(level);
  73. const manifest = [];
  74. for (const t of targets) {
  75. if (!fs.existsSync(t)) continue;
  76. const base = path.join(work, path.basename(t));
  77. sh(`mkdir -p "${path.dirname(base)}" && cp -r "${t}" "${path.dirname(base)}/"`);
  78. manifest.push({ src: t, dst: path.basename(t) });
  79. }
  80. // 3) credentials-map(密钥位置指针, 不含密钥值)
  81. const credMap = `# 密钥位置索引(不含密钥本体)\n- fmode token: ${HOME}/.fmode/config.json (fmodeApiToken)\n- claude token: ${HOME}/.claude/settings.json (env.ANTHROPIC_AUTH_TOKEN)\n- git tokens: /opt/data/.fmode-harness-agent/knowledge/lives/git-tokens.txt\n`;
  82. fs.writeFileSync(path.join(work, 'credentials-map.md'), credMap);
  83. fs.writeFileSync(path.join(work, 'sync-manifest.json'), JSON.stringify({ at: new Date().toISOString(), name, level, manifest }, null, 2));
  84. if (dry) { console.log('[dry] 同步项:', manifest.length); return; }
  85. // 4) commit+push
  86. try { sh(`git add -A && git -c user.name="${g.user}" -c user.email="${g.user}@fmode.cn" commit -m "clone sync ${new Date().toISOString().slice(0,16)}"`, work); } catch {}
  87. try { ensureRepo(repoName, g); sh(`git push -u origin main`, work); console.log('✅ 已同步', manifest.length, '项 →', repoName); }
  88. catch (e) { console.error('push 失败:', String(e).slice(0, 200)); }
  89. return;
  90. }
  91. if (cmd === 'restore') {
  92. const repo = argOf('--repo'); const into = argOf('--into') || '/opt/data';
  93. if (!repo) { console.error('用法: restore --repo <url> --into <dir>'); process.exit(2); }
  94. sh(`git clone ${repo} ${into}/.agent-restore`);
  95. console.log('已 clone 到', into + '/.agent-restore', '—— 按 sync-manifest.json 反向拷贝到对应路径即可');
  96. return;
  97. }
  98. console.log('用法: sync | restore');
  99. }
  100. main();