| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const os = require('os');
- const { spawnSync } = require('child_process');
- const qiniu = require('qiniu');
- const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
- const SUITE_DIR = path.join(PROJECT_ROOT, 'claude-code', 'claude-code-voc-intelligence');
- const MANIFEST_PATH = path.join(SUITE_DIR, 'skill-package-manifest.json');
- const DIST_DIR = path.join(PROJECT_ROOT, 'dist');
- const PACKAGE_ID = 'claude-code-voc-intelligence';
- const NPM_PACKAGE_NAME = '@vocmarket/voc-skill';
- const PACKAGE_ZIP = path.join(DIST_DIR, `${PACKAGE_ID}.zip`);
- const DIST_MANIFEST = path.join(DIST_DIR, `${PACKAGE_ID}-suite-manifest.json`);
- const TEMP_ROOT = path.join(os.tmpdir(), 'claude-code-voc-pkg', PACKAGE_ID);
- const TEST_ROOT = path.join(os.tmpdir(), 'claude-code-voc-pkg-test', PACKAGE_ID);
- const CDN_PREFIX = 'x/claude-code-skills/packages';
- const CDN_KEY = `${CDN_PREFIX}/${PACKAGE_ID}.zip`;
- const CDN_DOMAIN = 'https://repos.fmode.cn';
- const BUCKET = 'nova-repos';
- const REQUIRED_FILES = [
- '.claude-plugin/plugin.json',
- '.mcp.json',
- 'bin/claude-voc.js',
- 'install.js',
- 'README.md',
- 'docs/customer-quickstart.md',
- 'docs/demo-runbook.md',
- 'docs/npm-packaging-notes.md',
- 'package.json',
- 'package-lock.json',
- 'skill-package-manifest.json',
- 'memory-templates/xiaohongshu-trend-profile.json',
- 'mcp/src/server.js',
- 'mcp/src/core/credentials.js',
- 'mcp/src/core/memory-store.js',
- 'mcp/src/core/result-envelope.js',
- 'mcp/src/features/xiaohongshu-trend/live-collector.js',
- 'mcp/src/features/xiaohongshu-trend/preference-memory.js',
- 'mcp/src/features/xiaohongshu-trend/report.js',
- 'mcp/src/features/xiaohongshu-trend/sample-data.js',
- 'mcp/src/features/douyin-trend/live-collector.js',
- 'mcp/src/features/douyin-trend/preference-memory.js',
- 'mcp/src/features/douyin-trend/report.js',
- 'mcp/src/features/douyin-trend/sample-data.js',
- 'mcp/src/features/voc-problem-deep-dive/deep-dive.js',
- 'mcp/src/providers/xiaohongshu-api.js',
- 'mcp/src/providers/douyin-api.js',
- 'mcp/src/tools/xiaohongshu-preference-update.js',
- 'mcp/src/tools/xiaohongshu-trend-run.js',
- 'mcp/src/tools/douyin-preference-update.js',
- 'mcp/src/tools/douyin-trend-run.js',
- 'mcp/src/tools/voc-problem-deep-dive-run.js',
- 'scripts/smoke-mcp.js',
- 'scripts/smoke-package.js',
- 'skills/xiaohongshu-trend-intelligence/SKILL.md',
- 'skills/douyin-trend-intelligence/SKILL.md',
- 'skills/voc-problem-deep-dive/SKILL.md',
- 'skills/xiaohongshu-trend-intelligence/references/user-workflow.md',
- 'skills/xiaohongshu-trend-intelligence/references/output-format.md',
- 'skills/xiaohongshu-trend-intelligence/references/live-mode.md'
- ];
- const EXCLUDED_DIRS = new Set([
- 'node_modules',
- 'memory',
- 'outputs',
- '.npm-cache-acceptance',
- '.tmp',
- '.git',
- '.claude'
- ]);
- const EXCLUDED_FILES = new Set([
- '.env',
- '.env.local',
- '.npmrc'
- ]);
- function parseArgs(argv) {
- const opts = {
- validate: false,
- build: false,
- testPackage: false,
- upload: false,
- help: false
- };
- for (const token of argv) {
- if (token === '--validate') opts.validate = true;
- else if (token === '--build') opts.build = true;
- else if (token === '--test-package') opts.testPackage = true;
- else if (token === '--upload') opts.upload = true;
- else if (token === '--all') {
- opts.validate = true;
- opts.build = true;
- opts.testPackage = true;
- } else if (token === '--help' || token === '-h') {
- opts.help = true;
- }
- }
- if (!opts.validate && !opts.build && !opts.testPackage && !opts.upload && !opts.help) {
- opts.validate = true;
- opts.build = true;
- opts.testPackage = true;
- }
- return opts;
- }
- function usage() {
- return [
- 'Usage:',
- ' node scripts/deploy/claude-code-voc-intelligence-suite.js',
- ' node scripts/deploy/claude-code-voc-intelligence-suite.js --validate',
- ' node scripts/deploy/claude-code-voc-intelligence-suite.js --build --test-package',
- ' node scripts/deploy/claude-code-voc-intelligence-suite.js --build --upload',
- '',
- 'Default mode is --validate --build --test-package without upload.',
- 'Upload requires QINIU_AK and QINIU_SK environment variables.'
- ].join('\n');
- }
- function ensureDir(dirPath) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
- function readJson(filePath) {
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- }
- function copyDirRecursive(srcDir, destDir) {
- ensureDir(destDir);
- let count = 0;
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
- if (entry.isDirectory() && EXCLUDED_DIRS.has(entry.name)) continue;
- if (entry.isFile() && EXCLUDED_FILES.has(entry.name)) 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 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',
- maxBuffer: 1024 * 1024 * 100
- });
- if (child.stdout) process.stdout.write(child.stdout);
- if (child.stderr) process.stderr.write(child.stderr);
- if (child.status !== 0) {
- const detail = child.error ? `: ${child.error.message}` : '';
- throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
- }
- }
- function validateSuite() {
- console.log('[validate] claude-code-voc-intelligence suite');
- const missing = [];
- for (const rel of REQUIRED_FILES) {
- if (!fs.existsSync(path.join(SUITE_DIR, rel))) missing.push(rel);
- }
- const packageJson = readJson(path.join(SUITE_DIR, 'package.json'));
- const pluginJson = readJson(path.join(SUITE_DIR, '.claude-plugin', 'plugin.json'));
- const manifest = readJson(MANIFEST_PATH);
- const mcpJson = readJson(path.join(SUITE_DIR, '.mcp.json'));
- if (packageJson.name !== NPM_PACKAGE_NAME) missing.push('package.json:name mismatch');
- if (pluginJson.name !== 'voc-intelligence') missing.push('.claude-plugin/plugin.json:name mismatch');
- if (!manifest.skills.includes('xiaohongshu-trend-intelligence')) missing.push('manifest missing xiaohongshu-trend-intelligence');
- if (!mcpJson.mcpServers || !(mcpJson.mcpServers.voc || mcpJson.mcpServers['voc-intelligence'])) {
- missing.push('.mcp.json missing VOC MCP server');
- }
- if (missing.length) {
- throw new Error(`Suite validation failed:\n${missing.map(item => ` - ${item}`).join('\n')}`);
- }
- console.log(` ok: ${manifest.skills.length} skill, ${manifest.mcpTools.length} MCP tools`);
- }
- function buildSuite() {
- console.log('[build] dist/claude-code-voc-intelligence.zip');
- ensureDir(DIST_DIR);
- if (fs.existsSync(TEMP_ROOT)) fs.rmSync(TEMP_ROOT, { recursive: true, force: true });
- ensureDir(TEMP_ROOT);
- const copied = copyDirRecursive(SUITE_DIR, TEMP_ROOT);
- if (fs.existsSync(PACKAGE_ZIP)) fs.unlinkSync(PACKAGE_ZIP);
- run('powershell', [
- '-NoProfile',
- '-Command',
- `Compress-Archive -Path '${TEMP_ROOT}\\*' -DestinationPath '${PACKAGE_ZIP}' -Force`
- ], PROJECT_ROOT);
- writeDistManifest({ uploaded: false, copiedFiles: copied });
- const size = fs.statSync(PACKAGE_ZIP).size;
- console.log(` ok: ${PACKAGE_ZIP} (${(size / 1024).toFixed(0)} KB, ${copied} files)`);
- }
- function testPackage() {
- console.log('[test-package] unzip + install.js smoke');
- if (!fs.existsSync(PACKAGE_ZIP)) throw new Error(`Package zip not found: ${PACKAGE_ZIP}`);
- if (fs.existsSync(TEST_ROOT)) fs.rmSync(TEST_ROOT, { recursive: true, force: true });
- ensureDir(TEST_ROOT);
- run('powershell', [
- '-NoProfile',
- '-Command',
- `Expand-Archive -Path '${PACKAGE_ZIP}' -DestinationPath '${TEST_ROOT}' -Force`
- ], PROJECT_ROOT);
- run(process.execPath, ['install.js', '--smoke'], TEST_ROOT);
- console.log(' ok: extracted package can run install.js and sample/preference/MCP smoke');
- }
- function uploadFile(localFile, cdnKey) {
- return new Promise((resolve, reject) => {
- const accessKey = process.env.QINIU_AK;
- const secretKey = process.env.QINIU_SK;
- if (!accessKey || !secretKey) {
- return reject(new Error('Missing QINIU_AK or QINIU_SK environment variable'));
- }
- const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
- const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${cdnKey}`, expires: 3600 });
- const token = putPolicy.uploadToken(mac);
- const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 });
- const formUploader = new qiniu.form_up.FormUploader(config);
- const putExtra = new qiniu.form_up.PutExtra();
- formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => {
- if (err) return reject(err);
- if (info.statusCode === 200) return resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` });
- reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`));
- });
- });
- }
- async function uploadSuite() {
- console.log('[upload] qiniu nova-repos');
- if (!fs.existsSync(PACKAGE_ZIP)) throw new Error(`Package zip not found: ${PACKAGE_ZIP}`);
- const result = await uploadFile(PACKAGE_ZIP, CDN_KEY);
- writeDistManifest({ uploaded: true, downloadUrl: result.url });
- const urlsPath = path.join(DIST_DIR, 'download-urls.json');
- const urls = fs.existsSync(urlsPath) ? readJson(urlsPath) : {};
- urls[PACKAGE_ID] = result.url;
- fs.writeFileSync(urlsPath, `${JSON.stringify(urls, null, 2)}\n`, 'utf8');
- console.log(` ok: ${result.url}`);
- }
- function writeDistManifest(extra = {}) {
- const manifest = readJson(MANIFEST_PATH);
- const stat = fs.existsSync(PACKAGE_ZIP) ? fs.statSync(PACKAGE_ZIP) : undefined;
- const output = {
- name: PACKAGE_ID,
- version: manifest.version,
- generatedAt: new Date().toISOString(),
- packageZip: fs.existsSync(PACKAGE_ZIP) ? path.relative(PROJECT_ROOT, PACKAGE_ZIP).replace(/\\/g, '/') : '',
- packageZipBytes: stat ? stat.size : 0,
- plugin: manifest.plugin,
- skills: manifest.skills,
- mcpTools: manifest.mcpTools,
- installHint: manifest.installHint,
- installCommand: manifest.installCommand,
- npmInstallCommand: manifest.npmInstallCommand,
- npxInstallCommand: manifest.npxInstallCommand,
- workspaceInstallCommand: manifest.workspaceInstallCommand,
- workspaceInstallTarget: manifest.workspaceInstallTarget,
- buildCommand: 'node scripts/deploy/claude-code-voc-intelligence-suite.js --validate --build --test-package',
- uploadCommand: 'node scripts/deploy/claude-code-voc-intelligence-suite.js --validate --build --test-package --upload',
- ...extra
- };
- fs.writeFileSync(DIST_MANIFEST, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
- console.log(`[manifest] ${path.relative(PROJECT_ROOT, DIST_MANIFEST)}`);
- }
- async function main() {
- const opts = parseArgs(process.argv.slice(2));
- if (opts.help) {
- console.log(usage());
- return;
- }
- if (opts.validate) validateSuite();
- if (opts.build) buildSuite();
- if (opts.testPackage) testPackage();
- if (opts.upload) await uploadSuite();
- if (!opts.build) writeDistManifest({ uploaded: false });
- console.log('[done] claude-code-voc-intelligence suite is ready');
- }
- main().catch(error => {
- console.error(`[error] ${error.message}`);
- process.exit(1);
- });
|