| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- #!/usr/bin/env node
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { spawnSync } = require('child_process');
- const SOURCE_ROOT = path.resolve(__dirname, '..');
- const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
- const MIN_NODE_MAJOR = 18;
- const EXCLUDED_DIRS = new Set([
- 'node_modules',
- 'memory',
- 'outputs',
- '.tmp',
- '.git'
- ]);
- const EXCLUDED_FILES = new Set([
- '.env',
- '.npmrc'
- ]);
- function usage() {
- return [
- 'Claude VOC 技能包安装工具',
- '',
- '用法:',
- ' claude-voc install',
- ' claude-voc install --smoke',
- ' claude-voc check',
- ' claude-voc smoke',
- ' claude-voc path',
- '',
- '也可以免全局安装:',
- ' npx @gangvy/claude-code-voc-intelligence install',
- '',
- '参数:',
- ' --target <dir> 指定安装目录,默认 ~/.claude/plugins/voc-intelligence',
- ' --skip-install 复制后跳过 npm install',
- ' --smoke 安装后跑 sample / 偏好记忆 / MCP smoke',
- ' --help, -h 显示帮助'
- ].join('\n');
- }
- function parseArgs(argv) {
- const opts = {
- command: argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install',
- target: DEFAULT_TARGET,
- skipInstall: false,
- smoke: false,
- help: false
- };
- for (let i = opts.command === argv[0] ? 1 : 0; i < argv.length; i++) {
- const token = argv[i];
- if (token === '--target') {
- opts.target = argv[++i];
- } else if (token.startsWith('--target=')) {
- opts.target = token.slice('--target='.length);
- } else if (token === '--skip-install') {
- opts.skipInstall = true;
- } else if (token === '--smoke') {
- opts.smoke = true;
- } else if (token === '--help' || token === '-h') {
- opts.help = true;
- }
- }
- opts.target = path.resolve(opts.target.replace(/^~(?=$|[\\/])/, os.homedir()));
- return opts;
- }
- function run(command, args, cwd) {
- 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,
- encoding: 'utf8',
- 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}`);
- }
- }
- function checkNodeVersion() {
- const major = Number(process.versions.node.split('.')[0]);
- if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
- throw new Error(`Node.js 版本过低:当前 ${process.version},需要 Node.js ${MIN_NODE_MAJOR}+`);
- }
- }
- function ensureDir(dirPath) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
- function isSafeDefaultTarget(targetDir) {
- return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
- }
- function shouldCopyEntry(entry) {
- if (entry.isDirectory()) return !EXCLUDED_DIRS.has(entry.name);
- if (entry.isFile()) {
- if (EXCLUDED_FILES.has(entry.name)) return false;
- if (entry.name.endsWith('.tgz')) return false;
- return true;
- }
- return false;
- }
- function copyDirRecursive(srcDir, destDir) {
- ensureDir(destDir);
- let count = 0;
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
- if (!shouldCopyEntry(entry)) continue;
- const src = path.join(srcDir, entry.name);
- const dest = path.join(destDir, entry.name);
- if (entry.isDirectory()) {
- count += copyDirRecursive(src, dest);
- } else if (entry.isFile()) {
- ensureDir(path.dirname(dest));
- fs.copyFileSync(src, dest);
- count++;
- }
- }
- return count;
- }
- function assertInstalled(targetDir) {
- const required = [
- 'package.json',
- 'install.js',
- '.claude-plugin/plugin.json',
- '.mcp.json',
- 'skills/xiaohongshu-trend-intelligence/SKILL.md',
- 'mcp/src/server.js'
- ];
- const missing = required.filter(rel => !fs.existsSync(path.join(targetDir, rel)));
- if (missing.length) {
- throw new Error(`安装目录缺少必要文件:${missing.join(', ')}`);
- }
- }
- function printNextSteps(targetDir) {
- console.log('');
- console.log('安装完成。');
- console.log('');
- console.log('下一步:');
- console.log(` 1. 启动 Claude Code:claude --plugin-dir "${targetDir}"`);
- console.log(' 2. 在 Claude Code 里说:');
- console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。');
- console.log(' 重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。');
- console.log(' 先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。');
- console.log('');
- console.log(`安装目录:${targetDir}`);
- }
- function install(opts) {
- checkNodeVersion();
- if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
- if (fs.existsSync(opts.target)) {
- if (!isSafeDefaultTarget(opts.target)) {
- throw new Error(`自定义安装目录已存在,为避免覆盖,请先手动清理:${opts.target}`);
- }
- fs.rmSync(opts.target, { recursive: true, force: true });
- }
- ensureDir(opts.target);
- const copied = copyDirRecursive(SOURCE_ROOT, opts.target);
- console.log(`已复制技能包文件:${copied} 个`);
- }
- assertInstalled(opts.target);
- if (!opts.skipInstall) {
- console.log('正在安装运行依赖...');
- run('npm', ['install', '--omit=dev', '--ignore-scripts'], opts.target);
- }
- if (opts.smoke) {
- console.log('正在运行安装自检...');
- run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
- } else {
- run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
- }
- printNextSteps(opts.target);
- }
- function check(opts) {
- assertInstalled(opts.target);
- run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
- }
- function smoke(opts) {
- assertInstalled(opts.target);
- run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
- }
- function printPath(opts) {
- console.log(opts.target);
- console.log(`claude --plugin-dir "${opts.target}"`);
- }
- function main() {
- const opts = parseArgs(process.argv.slice(2));
- if (opts.help || opts.command === 'help') {
- console.log(usage());
- return;
- }
- if (opts.command === 'install') return install(opts);
- if (opts.command === 'check') return check(opts);
- if (opts.command === 'smoke') return smoke(opts);
- if (opts.command === 'path') return printPath(opts);
- throw new Error(`未知命令:${opts.command}\n\n${usage()}`);
- }
- try {
- main();
- } catch (error) {
- console.error('');
- console.error(`claude-voc 执行失败:${error.message}`);
- process.exit(1);
- }
|