#!/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); });