#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const packageRoot = path.dirname(fileURLToPath(import.meta.url)); const required = [ '.claude-plugin/plugin.json', '.mcp.json', 'README.md', 'package.json', 'package-lock.json', 'skill-package-manifest.json', 'mcp/src/server.mjs', 'scripts/smoke-package.mjs', 'scripts/smoke-mcp.mjs', 'docs/capability-map.md', 'docs/customer-quickstart.md', 'docs/live-manual-acceptance-checklist.md' ]; function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } export function checkPackage(root = packageRoot) { const errors = []; const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number); if (nodeMajor < 20 || (nodeMajor === 20 && nodeMinor < 11)) errors.push('node_version_requires_20.11'); for (const item of required) if (!fs.existsSync(path.join(root, item))) errors.push(`missing:${item}`); if (errors.length) return { ok: false, errors }; const pkg = readJson(path.join(root, 'package.json')); const plugin = readJson(path.join(root, '.claude-plugin', 'plugin.json')); const manifest = readJson(path.join(root, 'skill-package-manifest.json')); if (pkg.version !== plugin.version || pkg.version !== manifest.version) errors.push('version_mismatch'); const skills = fs.readdirSync(path.join(root, 'skills'), { withFileTypes: true }).filter(item => item.isDirectory()).map(item => item.name).sort(); if (JSON.stringify(skills) !== JSON.stringify([...manifest.skills].sort())) errors.push('skill_manifest_mismatch'); for (const skill of manifest.skills) if (!fs.existsSync(path.join(root, 'skills', skill, 'SKILL.md'))) errors.push(`missing_skill:${skill}`); const serverText = fs.readFileSync(path.join(root, 'mcp', 'src', 'server.mjs'), 'utf8'); for (const tool of manifest.mcpTools) if (!serverText.includes(`'${tool}'`)) errors.push(`missing_tool:${tool}`); return { ok: errors.length === 0, errors, version: pkg.version, skills: manifest.skills.length, tools: manifest.mcpTools.length }; } function run(command, args, cwd = packageRoot) { const result = spawnSync(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' }); if (result.status !== 0) process.exit(result.status || 1); } function help() { console.log('Usage: node install.js [--check] [--smoke] [--skip-install]'); } async function main() { const args = new Set(process.argv.slice(2)); if (args.has('--help')) return help(); const before = checkPackage(); if (!before.ok && !before.errors.every(item => item === 'missing:package-lock.json')) { console.error(JSON.stringify(before, null, 2)); process.exit(1); } if (!args.has('--skip-install') && !fs.existsSync(path.join(packageRoot, 'node_modules'))) run('npm', ['ci']); const result = checkPackage(); console.log(JSON.stringify(result, null, 2)); if (!result.ok) process.exit(1); if (args.has('--smoke')) { run(process.execPath, ['scripts/smoke-package.mjs']); run(process.execPath, ['scripts/smoke-mcp.mjs']); } } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();