installer.mjs 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. #!/usr/bin/env node
  2. /**
  3. * fmode-skillhub installer.mjs
  4. * 检测宿主 Agent,把仓库内 skills/ 下所有子技能分发到宿主技能目录。
  5. * Copyright (c) 未来飞马 MPL-2.0
  6. *
  7. * 用法:
  8. * node installer.mjs --all 分发全部子技能到检测到的宿主目录
  9. * node installer.mjs --check 检查现状(只读,不写文件)
  10. * node installer.mjs --host wb 指定宿主 wb|cc|hermes(跳过自动检测)
  11. */
  12. import { existsSync, readdirSync, mkdirSync, cpSync, writeFileSync } from 'node:fs';
  13. import { join, dirname } from 'node:path';
  14. import { homedir } from 'node:os';
  15. import { fileURLToPath } from 'node:url';
  16. const __dirname = dirname(fileURLToPath(import.meta.url));
  17. const ROOT = join(__dirname, '..');
  18. const SKILLS_DIR = join(ROOT, 'skills');
  19. // 宿主 -> 技能目录 映射(ESM 技能放在各宿主全局技能位置)
  20. const HOSTS = {
  21. wb: { name: 'WorkBuddy', dir: () => join(homedir(), '.workbuddy', 'skills') },
  22. cc: { name: 'Claude Code', dir: () => join(homedir(), '.claude', 'skills') },
  23. hermes: { name: 'Hermes Agent', dir: () => join(homedir(), '.hermes', 'skills') },
  24. };
  25. function detectHost() {
  26. // 探测哪个宿主目录存在
  27. if (existsSync(join(homedir(), '.workbuddy', 'skills')) ||
  28. existsSync(join(homedir(), '.workbuddy'))) return 'wb';
  29. if (existsSync(join(homedir(), '.claude', 'skills')) ||
  30. existsSync(join(homedir(), '.claude')) ||
  31. process.env.CLAUDE_CODE_ENTRYPOINT) return 'cc';
  32. if (existsSync(join(homedir(), '.hermes', 'skills')) ||
  33. process.env.HERMES_HOME) return 'hermes';
  34. return 'cc'; // 默认 Claude-compatible
  35. }
  36. function listSkills() {
  37. if (!existsSync(SKILLS_DIR)) return [];
  38. return readdirSync(SKILLS_DIR, { withFileTypes: true })
  39. .filter(e => e.isDirectory())
  40. .map(e => e.name)
  41. .filter(n => n.startsWith('fmode-'));
  42. }
  43. function main() {
  44. const args = process.argv.slice(2);
  45. const check = args.includes('--check');
  46. const hostArg = args.find(a => a.startsWith('--host'));
  47. const hostKey = (hostArg ? hostArg.split('=')[1] || process.argv[process.argv.indexOf(hostArg)+1] : detectHost()) || detectHost();
  48. const host = HOSTS[hostKey];
  49. if (!host) { console.error(`未知宿主: ${hostKey}`); process.exit(1); }
  50. const skills = listSkills();
  51. console.log(`[fmode-skillhub] 检测到宿主: ${host.name}`);
  52. console.log(`[fmode-skillhub] 发现子技能: ${skills.length} 个 -> ${skills.join(', ')}`);
  53. if (skills.length === 0) { console.error('未找到子技能(打开 /skills 目录检查)'); process.exit(1); }
  54. if (check) {
  55. const target = host.dir();
  56. console.log(`[check] 目标目录: ${target}`);
  57. console.log(`[check] 已存在: ${Object.keys(HOSTS).map(k => `${k}:${existsSync(join(homedir(), k === 'wb' ? '.workbuddy' : k === 'cc' ? '.claude' : '.hermes'))}`).join(' ')}`);
  58. process.exit(0);
  59. }
  60. const target = host.dir();
  61. console.log(`[fmode-skillhub] 安装到: ${target}`);
  62. const installed = [];
  63. for (const skill of skills) {
  64. const src = join(SKILLS_DIR, skill);
  65. const dest = join(target, skill);
  66. if (!existsSync(src)) continue;
  67. mkdirSync(target, { recursive: true });
  68. cpSync(src, dest, { recursive: true, force: true });
  69. installed.push(skill);
  70. }
  71. writeFileSync(join(target, 'fmode-skillhub.installed.json'), JSON.stringify({
  72. installedAt: new Date().toISOString(),
  73. host: hostKey,
  74. count: installed.length,
  75. skills: installed,
  76. }, null, 2));
  77. console.log(`✅ 安装完成: ${installed.length} 个子技能已落盘到 ${target}`);
  78. console.log(`清单: fmode-skillhub.installed.json`);
  79. }
  80. main();