install.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { spawnSync } = require('child_process');
  5. const ROOT = __dirname;
  6. const MIN_NODE_MAJOR = 18;
  7. const PACKAGE_NAMES = new Set([
  8. 'claude-code-voc-intelligence',
  9. '@gangvy/claude-code-voc-intelligence'
  10. ]);
  11. function parseArgs(argv) {
  12. const opts = {
  13. check: false,
  14. smoke: false,
  15. skipInstall: false,
  16. help: false,
  17. noNext: false
  18. };
  19. for (const token of argv) {
  20. if (token === '--check') opts.check = true;
  21. else if (token === '--smoke') opts.smoke = true;
  22. else if (token === '--skip-install') opts.skipInstall = true;
  23. else if (token === '--no-next') opts.noNext = true;
  24. else if (token === '--help' || token === '-h') opts.help = true;
  25. }
  26. return opts;
  27. }
  28. function usage() {
  29. return [
  30. 'Claude Code VOC Intelligence installer',
  31. '',
  32. 'Usage:',
  33. ' node install.js',
  34. ' node install.js --smoke',
  35. ' node install.js --check',
  36. '',
  37. 'Options:',
  38. ' --check Check files and environment only',
  39. ' --smoke Run sample/preference/MCP smoke checks',
  40. ' --skip-install Skip npm install',
  41. ' --no-next Do not print next-step instructions',
  42. ' --help, -h Show help'
  43. ].join('\n');
  44. }
  45. function run(command, args, options = {}) {
  46. const useCmd = process.platform === 'win32' && command === 'npm';
  47. const executable = useCmd ? 'cmd.exe' : command;
  48. const finalArgs = useCmd ? ['/d', '/s', '/c', 'npm', ...args] : args;
  49. const child = spawnSync(executable, finalArgs, {
  50. cwd: options.cwd || ROOT,
  51. encoding: 'utf8',
  52. stdio: options.stdio || 'inherit',
  53. maxBuffer: 1024 * 1024 * 100
  54. });
  55. if (child.status !== 0) {
  56. const detail = child.error ? `: ${child.error.message}` : '';
  57. throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
  58. }
  59. return child;
  60. }
  61. function readJson(relativePath) {
  62. return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8'));
  63. }
  64. function writeMcpConfig(rootDir = ROOT) {
  65. const mcpConfigPath = path.join(rootDir, '.mcp.json');
  66. const serverPath = path.join(rootDir, 'mcp', 'src', 'server.js');
  67. const mcpConfig = {
  68. mcpServers: {
  69. voc: {
  70. command: 'node',
  71. args: [serverPath],
  72. cwd: rootDir
  73. }
  74. }
  75. };
  76. fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
  77. }
  78. function assertFile(relativePath) {
  79. const absolute = path.join(ROOT, relativePath);
  80. if (!fs.existsSync(absolute)) {
  81. throw new Error(`Missing required file: ${relativePath}`);
  82. }
  83. }
  84. function checkNodeVersion() {
  85. const major = Number(process.versions.node.split('.')[0]);
  86. if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
  87. throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`);
  88. }
  89. }
  90. function checkFiles() {
  91. [
  92. 'package.json',
  93. '.claude-plugin/plugin.json',
  94. '.mcp.json',
  95. 'skill-package-manifest.json',
  96. 'skills/xiaohongshu-trend-intelligence/SKILL.md',
  97. 'skills/douyin-trend-intelligence/SKILL.md',
  98. 'skills/voc-issue-pool/SKILL.md',
  99. 'skills/voc-problem-deep-dive/SKILL.md',
  100. 'skills/voc-content-plan/SKILL.md',
  101. 'skills/voc-speaking-script/SKILL.md',
  102. 'skills/voc-competitor-map/SKILL.md',
  103. 'skills/voc-business-workflow/SKILL.md',
  104. 'mcp/src/server.js'
  105. ].forEach(assertFile);
  106. const packageJson = readJson('package.json');
  107. const pluginJson = readJson('.claude-plugin/plugin.json');
  108. const mcpJson = readJson('.mcp.json');
  109. if (!PACKAGE_NAMES.has(packageJson.name)) {
  110. throw new Error('package.json name is invalid');
  111. }
  112. if (pluginJson.name !== 'voc-intelligence') {
  113. throw new Error('.claude-plugin/plugin.json name is invalid');
  114. }
  115. if (!mcpJson.mcpServers || !(mcpJson.mcpServers.voc || mcpJson.mcpServers['voc-intelligence'])) {
  116. throw new Error('.mcp.json does not configure the VOC MCP server');
  117. }
  118. }
  119. function checkClaudeCommand() {
  120. const command = process.platform === 'win32' ? 'where' : 'which';
  121. const child = spawnSync(command, ['claude'], {
  122. cwd: ROOT,
  123. encoding: 'utf8',
  124. stdio: 'pipe'
  125. });
  126. return child.status === 0;
  127. }
  128. function printNextSteps({ claudeAvailable }) {
  129. const loadCommand = `claude --plugin-dir "${ROOT}"`;
  130. console.log('');
  131. console.log('Install check complete.');
  132. console.log('');
  133. console.log('Next step:');
  134. console.log(` 1. Start Claude Code with: ${loadCommand}`);
  135. console.log(' 2. In Claude Code, say:');
  136. console.log(' 帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。');
  137. console.log(' 或者说:');
  138. console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。');
  139. console.log(' 先用演示样例跑通流程,不要真实采集。');
  140. console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
  141. console.log(' 或者说:');
  142. console.log(' 帮我做一份家装全屋定制行业的抖音趋势情报。');
  143. console.log(' 先用演示样例跑通流程,不要真实采集。');
  144. console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
  145. console.log('');
  146. if (!claudeAvailable) {
  147. console.log('Tip: the current terminal did not detect the claude command.');
  148. console.log('If Claude Code is installed elsewhere, run the command above in that terminal.');
  149. console.log('');
  150. }
  151. console.log('Live collection requires a Xiaohongshu or Douyin/VOC token. Sample mode works without a token.');
  152. }
  153. function main() {
  154. const opts = parseArgs(process.argv.slice(2));
  155. if (opts.help) {
  156. console.log(usage());
  157. return;
  158. }
  159. console.log('');
  160. console.log('Claude Code VOC Intelligence install check');
  161. console.log('==========================================');
  162. console.log('');
  163. checkNodeVersion();
  164. checkFiles();
  165. writeMcpConfig(ROOT);
  166. console.log(`Node.js: ${process.version}`);
  167. console.log('Required files: OK');
  168. if (!opts.check && !opts.skipInstall) {
  169. console.log('');
  170. console.log('Installing dependencies...');
  171. run('npm', ['install', '--omit=dev', '--ignore-scripts']);
  172. }
  173. if (opts.smoke) {
  174. console.log('');
  175. console.log('Running sample/preference/MCP smoke checks...');
  176. run(process.execPath, ['scripts/smoke-package.js']);
  177. }
  178. if (!opts.noNext) {
  179. printNextSteps({ claudeAvailable: checkClaudeCommand() });
  180. }
  181. }
  182. try {
  183. main();
  184. } catch (error) {
  185. console.error('');
  186. console.error(`Install failed: ${error.message}`);
  187. process.exit(1);
  188. }