claude-voc.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const SOURCE_ROOT = path.resolve(__dirname, '..');
  7. const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
  8. const MIN_NODE_MAJOR = 18;
  9. const EXCLUDED_DIRS = new Set([
  10. 'node_modules',
  11. 'memory',
  12. 'outputs',
  13. '.tmp',
  14. '.git'
  15. ]);
  16. const EXCLUDED_FILES = new Set([
  17. '.env',
  18. '.npmrc'
  19. ]);
  20. function usage() {
  21. return [
  22. 'Claude VOC 技能包安装工具',
  23. '',
  24. '用法:',
  25. ' claude-voc install',
  26. ' claude-voc install --smoke',
  27. ' claude-voc check',
  28. ' claude-voc smoke',
  29. ' claude-voc path',
  30. '',
  31. '也可以免全局安装:',
  32. ' npx @gangvy/claude-code-voc-intelligence install',
  33. '',
  34. '参数:',
  35. ' --target <dir> 指定安装目录,默认 ~/.claude/plugins/voc-intelligence',
  36. ' --skip-install 复制后跳过 npm install',
  37. ' --smoke 安装后跑 sample / 偏好记忆 / MCP smoke',
  38. ' --help, -h 显示帮助'
  39. ].join('\n');
  40. }
  41. function parseArgs(argv) {
  42. const opts = {
  43. command: argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install',
  44. target: DEFAULT_TARGET,
  45. skipInstall: false,
  46. smoke: false,
  47. help: false
  48. };
  49. for (let i = opts.command === argv[0] ? 1 : 0; i < argv.length; i++) {
  50. const token = argv[i];
  51. if (token === '--target') {
  52. opts.target = argv[++i];
  53. } else if (token.startsWith('--target=')) {
  54. opts.target = token.slice('--target='.length);
  55. } else if (token === '--skip-install') {
  56. opts.skipInstall = true;
  57. } else if (token === '--smoke') {
  58. opts.smoke = true;
  59. } else if (token === '--help' || token === '-h') {
  60. opts.help = true;
  61. }
  62. }
  63. opts.target = path.resolve(opts.target.replace(/^~(?=$|[\\/])/, os.homedir()));
  64. return opts;
  65. }
  66. function run(command, args, cwd) {
  67. const useCmd = process.platform === 'win32' && command === 'npm';
  68. const executable = useCmd ? 'cmd.exe' : command;
  69. const finalArgs = useCmd ? ['/d', '/s', '/c', 'npm', ...args] : args;
  70. const child = spawnSync(executable, finalArgs, {
  71. cwd,
  72. encoding: 'utf8',
  73. stdio: 'inherit',
  74. maxBuffer: 1024 * 1024 * 100
  75. });
  76. if (child.status !== 0) {
  77. const detail = child.error ? `: ${child.error.message}` : '';
  78. throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
  79. }
  80. }
  81. function checkNodeVersion() {
  82. const major = Number(process.versions.node.split('.')[0]);
  83. if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
  84. throw new Error(`Node.js 版本过低:当前 ${process.version},需要 Node.js ${MIN_NODE_MAJOR}+`);
  85. }
  86. }
  87. function ensureDir(dirPath) {
  88. fs.mkdirSync(dirPath, { recursive: true });
  89. }
  90. function isSafeDefaultTarget(targetDir) {
  91. return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
  92. }
  93. function shouldCopyEntry(entry) {
  94. if (entry.isDirectory()) return !EXCLUDED_DIRS.has(entry.name);
  95. if (entry.isFile()) {
  96. if (EXCLUDED_FILES.has(entry.name)) return false;
  97. if (entry.name.endsWith('.tgz')) return false;
  98. return true;
  99. }
  100. return false;
  101. }
  102. function copyDirRecursive(srcDir, destDir) {
  103. ensureDir(destDir);
  104. let count = 0;
  105. for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
  106. if (!shouldCopyEntry(entry)) continue;
  107. const src = path.join(srcDir, entry.name);
  108. const dest = path.join(destDir, entry.name);
  109. if (entry.isDirectory()) {
  110. count += copyDirRecursive(src, dest);
  111. } else if (entry.isFile()) {
  112. ensureDir(path.dirname(dest));
  113. fs.copyFileSync(src, dest);
  114. count++;
  115. }
  116. }
  117. return count;
  118. }
  119. function assertInstalled(targetDir) {
  120. const required = [
  121. 'package.json',
  122. 'install.js',
  123. '.claude-plugin/plugin.json',
  124. '.mcp.json',
  125. 'skills/xiaohongshu-trend-intelligence/SKILL.md',
  126. 'mcp/src/server.js'
  127. ];
  128. const missing = required.filter(rel => !fs.existsSync(path.join(targetDir, rel)));
  129. if (missing.length) {
  130. throw new Error(`安装目录缺少必要文件:${missing.join(', ')}`);
  131. }
  132. }
  133. function printNextSteps(targetDir) {
  134. console.log('');
  135. console.log('安装完成。');
  136. console.log('');
  137. console.log('下一步:');
  138. console.log(` 1. 启动 Claude Code:claude --plugin-dir "${targetDir}"`);
  139. console.log(' 2. 在 Claude Code 里说:');
  140. console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。');
  141. console.log(' 重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。');
  142. console.log(' 先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。');
  143. console.log('');
  144. console.log(`安装目录:${targetDir}`);
  145. }
  146. function install(opts) {
  147. checkNodeVersion();
  148. if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
  149. if (fs.existsSync(opts.target)) {
  150. if (!isSafeDefaultTarget(opts.target)) {
  151. throw new Error(`自定义安装目录已存在,为避免覆盖,请先手动清理:${opts.target}`);
  152. }
  153. fs.rmSync(opts.target, { recursive: true, force: true });
  154. }
  155. ensureDir(opts.target);
  156. const copied = copyDirRecursive(SOURCE_ROOT, opts.target);
  157. console.log(`已复制技能包文件:${copied} 个`);
  158. }
  159. assertInstalled(opts.target);
  160. if (!opts.skipInstall) {
  161. console.log('正在安装运行依赖...');
  162. run('npm', ['install', '--omit=dev', '--ignore-scripts'], opts.target);
  163. }
  164. if (opts.smoke) {
  165. console.log('正在运行安装自检...');
  166. run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
  167. } else {
  168. run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
  169. }
  170. printNextSteps(opts.target);
  171. }
  172. function check(opts) {
  173. assertInstalled(opts.target);
  174. run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
  175. }
  176. function smoke(opts) {
  177. assertInstalled(opts.target);
  178. run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
  179. }
  180. function printPath(opts) {
  181. console.log(opts.target);
  182. console.log(`claude --plugin-dir "${opts.target}"`);
  183. }
  184. function main() {
  185. const opts = parseArgs(process.argv.slice(2));
  186. if (opts.help || opts.command === 'help') {
  187. console.log(usage());
  188. return;
  189. }
  190. if (opts.command === 'install') return install(opts);
  191. if (opts.command === 'check') return check(opts);
  192. if (opts.command === 'smoke') return smoke(opts);
  193. if (opts.command === 'path') return printPath(opts);
  194. throw new Error(`未知命令:${opts.command}\n\n${usage()}`);
  195. }
  196. try {
  197. main();
  198. } catch (error) {
  199. console.error('');
  200. console.error(`claude-voc 执行失败:${error.message}`);
  201. process.exit(1);
  202. }