| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const os = require('os');
- const { spawnSync } = require('child_process');
- const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
- const SUITE_DIR = path.join(PROJECT_ROOT, 'claude-code', 'claude-code-voc-intelligence');
- const RELEASE_NPM_DIR = path.join(PROJECT_ROOT, 'release', 'npm', 'claude-code-voc-intelligence');
- const DIST_DIR = path.join(PROJECT_ROOT, 'dist', 'npm');
- const DIST_MANIFEST = path.join(DIST_DIR, 'claude-code-voc-npm-package-manifest.json');
- const PACKAGE_NAME = '@vocmarket/voc-skill';
- const BIN_NAME = 'claude-voc';
- const TEST_ROOT = path.join(os.tmpdir(), 'claude-code-voc-npm-test', String(process.pid));
- function parseArgs(argv) {
- const opts = {
- pack: false,
- test: false,
- publish: false,
- dryRun: false,
- help: false
- };
- for (const token of argv) {
- if (token === '--pack') opts.pack = true;
- else if (token === '--test') opts.test = true;
- else if (token === '--publish') opts.publish = true;
- else if (token === '--dry-run') opts.dryRun = true;
- else if (token === '--help' || token === '-h') opts.help = true;
- }
- if (!opts.pack && !opts.test && !opts.publish && !opts.help) {
- opts.pack = true;
- opts.test = true;
- }
- if (opts.publish) {
- opts.pack = true;
- opts.test = true;
- }
- return opts;
- }
- function usage() {
- return [
- 'Usage:',
- ' node scripts/deploy/claude-code-voc-npm-package.js --pack --test',
- ' node scripts/deploy/claude-code-voc-npm-package.js --pack --test --publish',
- ' node scripts/deploy/claude-code-voc-npm-package.js --pack --dry-run',
- '',
- 'Publish requires npm login or a configured npm token.'
- ].join('\n');
- }
- function ensureDir(dirPath) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
- function parseJsonText(text, source = 'JSON') {
- return JSON.parse(String(text || '').replace(/^\uFEFF/, ''));
- }
- function readJson(filePath) {
- return parseJsonText(fs.readFileSync(filePath, 'utf8'), filePath);
- }
- function readEnvFileMaybe(filePath) {
- try {
- if (!filePath || !fs.existsSync(filePath)) return {};
- const env = {};
- const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
- for (const rawLine of content.split(/\r?\n/)) {
- const line = rawLine.trim();
- if (!line || line.startsWith('#')) continue;
- const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
- if (!match) continue;
- let value = match[2].trim();
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
- value = value.slice(1, -1);
- }
- env[match[1]] = value;
- }
- return env;
- } catch {
- return {};
- }
- }
- function run(command, args, cwd, 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,
- encoding: 'utf8',
- stdio: options.capture ? 'pipe' : 'inherit',
- env: options.env || process.env,
- maxBuffer: 1024 * 1024 * 100
- });
- if (child.status !== 0) {
- if (options.capture && child.stdout) process.stdout.write(child.stdout);
- if (options.capture && child.stderr) process.stderr.write(child.stderr);
- const detail = child.error ? `: ${child.error.message}` : '';
- throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
- }
- return child;
- }
- function validatePackage() {
- console.log('[validate-npm] package metadata');
- const packageJson = readJson(path.join(SUITE_DIR, 'package.json'));
- if (packageJson.name !== PACKAGE_NAME) {
- throw new Error(`package.json name should be ${PACKAGE_NAME}`);
- }
- if (!packageJson.bin || packageJson.bin[BIN_NAME] !== 'bin/claude-voc.js') {
- throw new Error(`package.json missing bin.${BIN_NAME}`);
- }
- const requiredFiles = [
- 'bin/claude-voc.js',
- 'install.js',
- '.claude-plugin/plugin.json',
- '.mcp.json',
- 'skills/xiaohongshu-trend-intelligence/SKILL.md',
- 'mcp/src/server.js',
- 'docs/customer-quickstart.md',
- 'docs/demo-runbook.md'
- ];
- const missing = requiredFiles.filter(rel => !fs.existsSync(path.join(SUITE_DIR, rel)));
- if (missing.length) {
- throw new Error(`missing files:\n${missing.map(item => ` - ${item}`).join('\n')}`);
- }
- console.log(` ok: ${packageJson.name}@${packageJson.version}`);
- return packageJson;
- }
- function packPackage({ dryRun = false } = {}) {
- console.log(dryRun ? '[npm-pack] dry run' : '[npm-pack] create tgz');
- ensureDir(DIST_DIR);
- const args = ['pack', '--json', '--pack-destination', DIST_DIR];
- if (dryRun) args.push('--dry-run');
- const result = run('npm', args, SUITE_DIR, { capture: true });
- const packed = parseJsonText(result.stdout || '[]', 'npm pack output')[0];
- if (!packed) throw new Error('npm pack did not return package metadata');
- const tarball = dryRun ? '' : path.join(DIST_DIR, packed.filename);
- if (!dryRun && !fs.existsSync(tarball)) {
- throw new Error(`tarball not found: ${tarball}`);
- }
- console.log(` ok: ${packed.filename || packed.name}`);
- return {
- filename: packed.filename,
- tarball,
- packageSize: packed.size || 0,
- unpackedSize: packed.unpackedSize || 0,
- entryCount: Array.isArray(packed.files) ? packed.files.length : 0
- };
- }
- function testTarball(tarball) {
- console.log('[npm-test] install tarball + run claude-voc install --smoke');
- if (!tarball || !fs.existsSync(tarball)) throw new Error(`missing tarball: ${tarball}`);
- if (fs.existsSync(TEST_ROOT)) fs.rmSync(TEST_ROOT, { recursive: true, force: true });
- ensureDir(TEST_ROOT);
- const appRoot = path.join(TEST_ROOT, 'app');
- const pluginTarget = path.join(TEST_ROOT, 'claude-plugins', 'voc-intelligence');
- ensureDir(appRoot);
- run('npm', ['install', tarball, '--ignore-scripts'], appRoot);
- run('npm', [
- 'exec',
- '--prefix',
- appRoot,
- '--',
- BIN_NAME,
- 'install',
- '--target',
- pluginTarget,
- '--smoke'
- ], PROJECT_ROOT);
- console.log(' ok: npm-installed CLI can install and smoke-test the plugin');
- return { appRoot, pluginTarget };
- }
- function testNpxLikeTarball(tarball) {
- console.log('[npx-test] npm exec --package tarball + claude-voc install --smoke');
- if (!tarball || !fs.existsSync(tarball)) throw new Error(`missing tarball: ${tarball}`);
- const pluginTarget = path.join(TEST_ROOT, 'npx-plugins', 'voc-intelligence');
- if (fs.existsSync(pluginTarget)) fs.rmSync(pluginTarget, { recursive: true, force: true });
- run('npm', [
- 'exec',
- '--yes',
- '--package',
- tarball,
- '--',
- BIN_NAME,
- 'install',
- '--target',
- pluginTarget,
- '--smoke'
- ], PROJECT_ROOT);
- console.log(' ok: npx-style CLI can install and smoke-test the plugin');
- return { pluginTarget };
- }
- function testWorkspaceTarball(tarball) {
- console.log('[workspace-test] npm exec --package tarball + claude-voc workspace --smoke');
- if (!tarball || !fs.existsSync(tarball)) throw new Error(`missing tarball: ${tarball}`);
- const workspaceRoot = path.join(TEST_ROOT, 'workspace-app');
- if (fs.existsSync(workspaceRoot)) fs.rmSync(workspaceRoot, { recursive: true, force: true });
- ensureDir(workspaceRoot);
- run('npm', [
- 'exec',
- '--yes',
- '--package',
- tarball,
- '--',
- BIN_NAME,
- 'workspace',
- '--smoke'
- ], workspaceRoot);
- const pluginTarget = path.join(workspaceRoot, '.claude', 'plugins', 'voc-intelligence');
- const pluginMcpConfig = path.join(pluginTarget, '.mcp.json');
- const workspaceMcpConfig = path.join(workspaceRoot, '.mcp.json');
- const workspaceSkill = path.join(workspaceRoot, '.claude', 'skills', 'xiaohongshu-trend-intelligence', 'SKILL.md');
- if (!fs.existsSync(pluginMcpConfig)) {
- throw new Error(`workspace install missing plugin .mcp.json: ${pluginMcpConfig}`);
- }
- if (!fs.existsSync(workspaceMcpConfig)) {
- throw new Error(`workspace install missing project .mcp.json: ${workspaceMcpConfig}`);
- }
- if (!fs.existsSync(workspaceSkill)) {
- throw new Error(`workspace install missing project skill entry: ${workspaceSkill}`);
- }
- console.log(' ok: workspace CLI can install and smoke-test the plugin');
- return { workspaceRoot, pluginTarget };
- }
- function publishTarball(tarball) {
- console.log('[npm-publish] npm publish');
- if (!tarball || !fs.existsSync(tarball)) throw new Error(`missing tarball: ${tarball}`);
- const env = { ...process.env };
- const rootLocalEnv = readEnvFileMaybe(path.join(PROJECT_ROOT, '.env.local'));
- const suiteLocalEnv = readEnvFileMaybe(path.join(SUITE_DIR, '.env.local'));
- const releaseLocalEnv = readEnvFileMaybe(path.join(RELEASE_NPM_DIR, '.env.local'));
- const token = process.env.NPM_TOKEN ||
- process.env.NODE_AUTH_TOKEN ||
- rootLocalEnv.NPM_TOKEN ||
- rootLocalEnv.NODE_AUTH_TOKEN ||
- suiteLocalEnv.NPM_TOKEN ||
- suiteLocalEnv.NODE_AUTH_TOKEN ||
- releaseLocalEnv.NPM_TOKEN ||
- releaseLocalEnv.NODE_AUTH_TOKEN ||
- '';
- if (token) {
- ensureDir(TEST_ROOT);
- const npmrcPath = path.join(TEST_ROOT, '.npmrc');
- fs.writeFileSync(npmrcPath, `//registry.npmjs.org/:_authToken=${token}\n`, 'utf8');
- env.NPM_CONFIG_USERCONFIG = npmrcPath;
- env.NODE_AUTH_TOKEN = token;
- }
- run('npm', ['publish', tarball, '--access', 'public'], PROJECT_ROOT, { env });
- ensurePublicAccess(env);
- verifyPublishedPublic();
- console.log(' ok: published and visible on npm registry');
- }
- function ensurePublicAccess(env = process.env) {
- run('npm', ['access', 'set', 'status=public', PACKAGE_NAME, '--registry=https://registry.npmjs.org/'], PROJECT_ROOT, { env });
- }
- function verifyPublishedPublic() {
- const version = readJson(path.join(SUITE_DIR, 'package.json')).version;
- const args = ['view', `${PACKAGE_NAME}@${version}`, 'version', '--registry=https://registry.npmjs.org/'];
- const publicEnv = { ...process.env };
- delete publicEnv.NPM_TOKEN;
- delete publicEnv.NODE_AUTH_TOKEN;
- delete publicEnv.NPM_CONFIG_USERCONFIG;
- let lastError = '';
- for (let attempt = 1; attempt <= 12; attempt++) {
- const child = spawnSync(process.platform === 'win32' ? 'cmd.exe' : 'npm', process.platform === 'win32'
- ? ['/d', '/s', '/c', 'npm', ...args]
- : args, {
- cwd: PROJECT_ROOT,
- encoding: 'utf8',
- stdio: 'pipe',
- env: publicEnv,
- maxBuffer: 1024 * 1024 * 10
- });
- if (child.status === 0 && String(child.stdout || '').trim() === version) {
- return true;
- }
- lastError = String(child.stderr || child.stdout || '').trim();
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10000);
- }
- throw new Error(`npm publish returned success, but registry verification failed for ${PACKAGE_NAME}@${version}: ${lastError}`);
- }
- function writeManifest({ packageJson, packed, tested, npxTested, workspaceTested, published }) {
- ensureDir(DIST_DIR);
- const existing = fs.existsSync(DIST_MANIFEST) ? readJson(DIST_MANIFEST) : {};
- const preservePublished = existing.name === PACKAGE_NAME &&
- existing.version === packageJson.version &&
- existing.published === true;
- const manifest = {
- name: PACKAGE_NAME,
- version: packageJson.version,
- generatedAt: new Date().toISOString(),
- tarball: packed.tarball ? path.relative(PROJECT_ROOT, packed.tarball).replace(/\\/g, '/') : '',
- tarballBytes: packed.packageSize,
- unpackedBytes: packed.unpackedSize,
- entryCount: packed.entryCount,
- installCommands: [
- `npm install -g ${PACKAGE_NAME}`,
- `${BIN_NAME} install`,
- `npx ${PACKAGE_NAME} install`,
- `npx ${PACKAGE_NAME} workspace --smoke`
- ],
- tested: Boolean(tested),
- npxTested: Boolean(npxTested),
- workspaceTested: Boolean(workspaceTested),
- published: Boolean(published) || preservePublished
- };
- fs.writeFileSync(DIST_MANIFEST, `${JSON.stringify(manifest, 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;
- }
- const packageJson = validatePackage();
- let packed = { filename: '', tarball: '', packageSize: 0, unpackedSize: 0, entryCount: 0 };
- let tested;
- let npxTested;
- let workspaceTested;
- if (opts.pack) packed = packPackage({ dryRun: opts.dryRun });
- if (opts.test && !opts.dryRun) {
- tested = testTarball(packed.tarball);
- npxTested = testNpxLikeTarball(packed.tarball);
- workspaceTested = testWorkspaceTarball(packed.tarball);
- }
- if (opts.publish && !opts.dryRun) publishTarball(packed.tarball);
- writeManifest({ packageJson, packed, tested, npxTested, workspaceTested, published: opts.publish && !opts.dryRun });
- console.log('[done] npm package flow is ready');
- }
- main().catch(error => {
- console.error(`[error] ${error.message}`);
- process.exit(1);
- });
|