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