install.js 6.7 KB

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