#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const ROOT = __dirname; const MIN_NODE_MAJOR = 18; const PACKAGE_NAMES = new Set([ 'claude-code-voc-intelligence', '@gangvy/claude-code-voc-intelligence' ]); function parseArgs(argv) { const opts = { check: false, smoke: false, skipInstall: false, help: false, noNext: false }; for (const token of argv) { if (token === '--check') opts.check = true; else if (token === '--smoke') opts.smoke = true; else if (token === '--skip-install') opts.skipInstall = true; else if (token === '--no-next') opts.noNext = true; else if (token === '--help' || token === '-h') opts.help = true; } return opts; } function usage() { return [ 'Claude Code VOC Intelligence installer', '', 'Usage:', ' node install.js', ' node install.js --smoke', ' node install.js --check', '', 'Options:', ' --check Check files and environment only', ' --smoke Run sample/preference/MCP smoke checks', ' --skip-install Skip npm install', ' --no-next Do not print next-step instructions', ' --help, -h Show help' ].join('\n'); } function run(command, args, options = {}) { const useCmd = process.platform === 'win32' && command === 'npm'; const executable = useCmd ? 'cmd.exe' : command; const finalArgs = useCmd ? ['/d', '/s', '/c', 'npm', ...args] : args; const child = spawnSync(executable, finalArgs, { cwd: options.cwd || ROOT, encoding: 'utf8', stdio: options.stdio || 'inherit', maxBuffer: 1024 * 1024 * 100 }); if (child.status !== 0) { const detail = child.error ? `: ${child.error.message}` : ''; throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`); } return child; } function readJson(relativePath) { return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8')); } function writeMcpConfig(rootDir = ROOT) { const mcpConfigPath = path.join(rootDir, '.mcp.json'); const serverPath = path.join(rootDir, 'mcp', 'src', 'server.js'); const mcpConfig = { mcpServers: { voc: { command: 'node', args: [serverPath], cwd: rootDir } } }; fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8'); } function assertFile(relativePath) { const absolute = path.join(ROOT, relativePath); if (!fs.existsSync(absolute)) { throw new Error(`Missing required file: ${relativePath}`); } } function checkNodeVersion() { const major = Number(process.versions.node.split('.')[0]); if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) { throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`); } } function checkFiles() { [ 'package.json', '.claude-plugin/plugin.json', '.mcp.json', 'skill-package-manifest.json', 'skills/xiaohongshu-trend-intelligence/SKILL.md', 'skills/douyin-trend-intelligence/SKILL.md', 'skills/voc-issue-pool/SKILL.md', 'skills/voc-problem-deep-dive/SKILL.md', 'skills/voc-content-plan/SKILL.md', 'skills/voc-speaking-script/SKILL.md', 'skills/voc-competitor-map/SKILL.md', 'skills/voc-business-workflow/SKILL.md', 'mcp/src/server.js' ].forEach(assertFile); const packageJson = readJson('package.json'); const pluginJson = readJson('.claude-plugin/plugin.json'); const mcpJson = readJson('.mcp.json'); if (!PACKAGE_NAMES.has(packageJson.name)) { throw new Error('package.json name is invalid'); } if (pluginJson.name !== 'voc-intelligence') { throw new Error('.claude-plugin/plugin.json name is invalid'); } if (!mcpJson.mcpServers || !(mcpJson.mcpServers.voc || mcpJson.mcpServers['voc-intelligence'])) { throw new Error('.mcp.json does not configure the VOC MCP server'); } } function checkClaudeCommand() { const command = process.platform === 'win32' ? 'where' : 'which'; const child = spawnSync(command, ['claude'], { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' }); return child.status === 0; } function printNextSteps({ claudeAvailable }) { const loadCommand = `claude --plugin-dir "${ROOT}"`; console.log(''); console.log('Install check complete.'); console.log(''); console.log('Next step:'); console.log(` 1. Start Claude Code with: ${loadCommand}`); console.log(' 2. In Claude Code, say:'); console.log(' 帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。'); console.log(' 或者说:'); console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。'); console.log(' 先用演示样例跑通流程,不要真实采集。'); console.log(' 在聊天里给我第一轮样本观察和待确认问题。'); console.log(' 或者说:'); console.log(' 帮我做一份家装全屋定制行业的抖音趋势情报。'); console.log(' 先用演示样例跑通流程,不要真实采集。'); console.log(' 在聊天里给我第一轮样本观察和待确认问题。'); console.log(''); if (!claudeAvailable) { console.log('Tip: the current terminal did not detect the claude command.'); console.log('If Claude Code is installed elsewhere, run the command above in that terminal.'); console.log(''); } console.log('Live collection requires a Xiaohongshu or Douyin/VOC token. Sample mode works without a token.'); } function main() { const opts = parseArgs(process.argv.slice(2)); if (opts.help) { console.log(usage()); return; } console.log(''); console.log('Claude Code VOC Intelligence install check'); console.log('=========================================='); console.log(''); checkNodeVersion(); checkFiles(); writeMcpConfig(ROOT); console.log(`Node.js: ${process.version}`); console.log('Required files: OK'); if (!opts.check && !opts.skipInstall) { console.log(''); console.log('Installing dependencies...'); run('npm', ['install', '--omit=dev', '--ignore-scripts']); } if (opts.smoke) { console.log(''); console.log('Running sample/preference/MCP smoke checks...'); run(process.execPath, ['scripts/smoke-package.js']); } if (!opts.noNext) { printNextSteps({ claudeAvailable: checkClaudeCommand() }); } } try { main(); } catch (error) { console.error(''); console.error(`Install failed: ${error.message}`); process.exit(1); }