install.js 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. #!/usr/bin/env node
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import { spawnSync } from 'node:child_process';
  5. import { fileURLToPath } from 'node:url';
  6. const packageRoot = path.dirname(fileURLToPath(import.meta.url));
  7. const required = [
  8. '.claude-plugin/plugin.json', '.mcp.json', 'README.md', 'package.json', 'package-lock.json',
  9. 'skill-package-manifest.json', 'mcp/src/server.mjs', 'scripts/smoke-package.mjs',
  10. 'scripts/smoke-mcp.mjs', 'docs/capability-map.md', 'docs/customer-quickstart.md',
  11. 'docs/live-manual-acceptance-checklist.md'
  12. ];
  13. function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); }
  14. export function checkPackage(root = packageRoot) {
  15. const errors = [];
  16. const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
  17. if (nodeMajor < 20 || (nodeMajor === 20 && nodeMinor < 11)) errors.push('node_version_requires_20.11');
  18. for (const item of required) if (!fs.existsSync(path.join(root, item))) errors.push(`missing:${item}`);
  19. if (errors.length) return { ok: false, errors };
  20. const pkg = readJson(path.join(root, 'package.json'));
  21. const plugin = readJson(path.join(root, '.claude-plugin', 'plugin.json'));
  22. const manifest = readJson(path.join(root, 'skill-package-manifest.json'));
  23. if (pkg.version !== plugin.version || pkg.version !== manifest.version) errors.push('version_mismatch');
  24. const skills = fs.readdirSync(path.join(root, 'skills'), { withFileTypes: true }).filter(item => item.isDirectory()).map(item => item.name).sort();
  25. if (JSON.stringify(skills) !== JSON.stringify([...manifest.skills].sort())) errors.push('skill_manifest_mismatch');
  26. for (const skill of manifest.skills) if (!fs.existsSync(path.join(root, 'skills', skill, 'SKILL.md'))) errors.push(`missing_skill:${skill}`);
  27. const serverText = fs.readFileSync(path.join(root, 'mcp', 'src', 'server.mjs'), 'utf8');
  28. for (const tool of manifest.mcpTools) if (!serverText.includes(`'${tool}'`)) errors.push(`missing_tool:${tool}`);
  29. return { ok: errors.length === 0, errors, version: pkg.version, skills: manifest.skills.length, tools: manifest.mcpTools.length };
  30. }
  31. function run(command, args, cwd = packageRoot) {
  32. const result = spawnSync(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
  33. if (result.status !== 0) process.exit(result.status || 1);
  34. }
  35. function help() {
  36. console.log('Usage: node install.js [--check] [--smoke] [--skip-install]');
  37. }
  38. async function main() {
  39. const args = new Set(process.argv.slice(2));
  40. if (args.has('--help')) return help();
  41. const before = checkPackage();
  42. if (!before.ok && !before.errors.every(item => item === 'missing:package-lock.json')) {
  43. console.error(JSON.stringify(before, null, 2));
  44. process.exit(1);
  45. }
  46. if (!args.has('--skip-install') && !fs.existsSync(path.join(packageRoot, 'node_modules'))) run('npm', ['ci']);
  47. const result = checkPackage();
  48. console.log(JSON.stringify(result, null, 2));
  49. if (!result.ok) process.exit(1);
  50. if (args.has('--smoke')) {
  51. run(process.execPath, ['scripts/smoke-package.mjs']);
  52. run(process.execPath, ['scripts/smoke-mcp.mjs']);
  53. }
  54. }
  55. if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();