| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- #!/usr/bin/env node
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- function readJson(filePath) {
- return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
- }
- function sorted(values) {
- return [...values].sort((a, b) => String(a).localeCompare(String(b)));
- }
- function diff(expected, actual) {
- const expectedSet = new Set(expected);
- const actualSet = new Set(actual);
- return {
- missing: sorted(expected.filter(value => !actualSet.has(value))),
- extra: sorted(actual.filter(value => !expectedSet.has(value)))
- };
- }
- function duplicateValues(values) {
- const seen = new Set();
- const duplicates = new Set();
- for (const value of values) {
- if (seen.has(value)) duplicates.add(value);
- seen.add(value);
- }
- return sorted([...duplicates]);
- }
- function listSkillNames(root) {
- const skillsRoot = path.join(root, 'skills');
- if (!fs.existsSync(skillsRoot)) return [];
- return sorted(fs.readdirSync(skillsRoot, { withFileTypes: true })
- .filter(entry => entry.isDirectory() && fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md')))
- .map(entry => entry.name));
- }
- function listCatalogBackups(root) {
- const catalogRoot = path.join(root, 'mcp', 'catalog');
- if (!fs.existsSync(catalogRoot)) return [];
- return fs.readdirSync(catalogRoot, { withFileTypes: true })
- .filter(entry => entry.isFile() && /\.bak-[^/\\]+$/i.test(entry.name))
- .map(entry => path.relative(root, path.join(catalogRoot, entry.name)).replace(/\\/g, '/'))
- .sort();
- }
- function listRegisteredTools(root) {
- const serverPath = path.join(root, 'mcp', 'src', 'server.js');
- if (!fs.existsSync(serverPath)) return [];
- const source = fs.readFileSync(serverPath, 'utf8');
- return [...source.matchAll(/registerTool\(\s*['"]([^'"]+)['"]/g)].map(match => match[1]);
- }
- function auditIndex(root = path.resolve(__dirname, '..')) {
- const issues = [];
- const warnings = [];
- const manifestPath = path.join(root, 'skill-package-manifest.json');
- const packagePath = path.join(root, 'package.json');
- const lockPath = path.join(root, 'package-lock.json');
- if (!fs.existsSync(manifestPath)) issues.push('missing skill-package-manifest.json');
- if (!fs.existsSync(packagePath)) issues.push('missing package.json');
- if (issues.length) return { status: 'error', root, issues, warnings };
- const manifest = readJson(manifestPath);
- const packageJson = readJson(packagePath);
- const actualSkills = listSkillNames(root);
- const actualTools = listRegisteredTools(root);
- const manifestSkills = Array.isArray(manifest.skills) ? manifest.skills : [];
- const manifestTools = Array.isArray(manifest.mcpTools) ? manifest.mcpTools : [];
- const skillDiff = diff(manifestSkills, actualSkills);
- const toolDiff = diff(manifestTools, actualTools);
- if (skillDiff.missing.length || skillDiff.extra.length) {
- issues.push(`skill index mismatch: missing=[${skillDiff.missing.join(', ')}], extra=[${skillDiff.extra.join(', ')}]`);
- }
- if (toolDiff.missing.length || toolDiff.extra.length) {
- issues.push(`MCP tool index mismatch: missing=[${toolDiff.missing.join(', ')}], extra=[${toolDiff.extra.join(', ')}]`);
- }
- if (duplicateValues(manifestSkills).length) {
- issues.push(`duplicate skills in manifest: ${duplicateValues(manifestSkills).join(', ')}`);
- }
- if (duplicateValues(manifestTools).length) {
- issues.push(`duplicate MCP tools in manifest: ${duplicateValues(manifestTools).join(', ')}`);
- }
- if (manifest.entrySkill && !manifestSkills.includes(manifest.entrySkill)) {
- issues.push(`entrySkill is not listed in skills: ${manifest.entrySkill}`);
- }
- const referenceFields = [
- 'apiCatalog',
- 'capabilityMap',
- 'taobaoV7Doc',
- 'bossPromptTemplates',
- 'liveAcceptanceChecklist',
- 'troubleshootingPlaybook',
- 'paymentLinksDoc',
- 'contextBudgetDoc',
- 'costMeteringSpec',
- 'apiErrorCodesDoc'
- ];
- for (const field of referenceFields) {
- if (!manifest[field]) continue;
- const target = path.join(root, manifest[field]);
- if (!fs.existsSync(target)) issues.push(`manifest.${field} points to missing file: ${manifest[field]}`);
- }
- if (packageJson.version !== manifest.version) {
- issues.push(`version mismatch: package.json=${packageJson.version}, manifest=${manifest.version}`);
- }
- if (fs.existsSync(lockPath)) {
- const lock = readJson(lockPath);
- if (lock.version && lock.version !== packageJson.version) {
- issues.push(`version mismatch: package-lock.json=${lock.version}, package.json=${packageJson.version}`);
- }
- if (lock.packages && lock.packages[''] && lock.packages[''].version !== packageJson.version) {
- issues.push(`version mismatch: package-lock root=${lock.packages[''].version}, package.json=${packageJson.version}`);
- }
- }
- const backups = listCatalogBackups(root);
- if (backups.length) {
- warnings.push(`local catalog backups detected; release builders must exclude: ${backups.join(', ')}`);
- }
- return {
- status: issues.length ? 'error' : 'ok',
- root,
- version: packageJson.version,
- skills: actualSkills,
- mcpTools: actualTools,
- warnings,
- issues
- };
- }
- function main() {
- const result = auditIndex();
- console.log(JSON.stringify(result, null, 2));
- process.exitCode = result.status === 'ok' ? 0 : 1;
- }
- if (require.main === module) main();
- module.exports = {
- auditIndex,
- listRegisteredTools,
- listSkillNames
- };
|