audit-index.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const fs = require('fs');
  4. const path = require('path');
  5. function readJson(filePath) {
  6. return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
  7. }
  8. function sorted(values) {
  9. return [...values].sort((a, b) => String(a).localeCompare(String(b)));
  10. }
  11. function diff(expected, actual) {
  12. const expectedSet = new Set(expected);
  13. const actualSet = new Set(actual);
  14. return {
  15. missing: sorted(expected.filter(value => !actualSet.has(value))),
  16. extra: sorted(actual.filter(value => !expectedSet.has(value)))
  17. };
  18. }
  19. function duplicateValues(values) {
  20. const seen = new Set();
  21. const duplicates = new Set();
  22. for (const value of values) {
  23. if (seen.has(value)) duplicates.add(value);
  24. seen.add(value);
  25. }
  26. return sorted([...duplicates]);
  27. }
  28. function listSkillNames(root) {
  29. const skillsRoot = path.join(root, 'skills');
  30. if (!fs.existsSync(skillsRoot)) return [];
  31. return sorted(fs.readdirSync(skillsRoot, { withFileTypes: true })
  32. .filter(entry => entry.isDirectory() && fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md')))
  33. .map(entry => entry.name));
  34. }
  35. function listCatalogBackups(root) {
  36. const catalogRoot = path.join(root, 'mcp', 'catalog');
  37. if (!fs.existsSync(catalogRoot)) return [];
  38. return fs.readdirSync(catalogRoot, { withFileTypes: true })
  39. .filter(entry => entry.isFile() && /\.bak-[^/\\]+$/i.test(entry.name))
  40. .map(entry => path.relative(root, path.join(catalogRoot, entry.name)).replace(/\\/g, '/'))
  41. .sort();
  42. }
  43. function listRegisteredTools(root) {
  44. const serverPath = path.join(root, 'mcp', 'src', 'server.js');
  45. if (!fs.existsSync(serverPath)) return [];
  46. const source = fs.readFileSync(serverPath, 'utf8');
  47. return [...source.matchAll(/registerTool\(\s*['"]([^'"]+)['"]/g)].map(match => match[1]);
  48. }
  49. function auditIndex(root = path.resolve(__dirname, '..')) {
  50. const issues = [];
  51. const warnings = [];
  52. const manifestPath = path.join(root, 'skill-package-manifest.json');
  53. const packagePath = path.join(root, 'package.json');
  54. const lockPath = path.join(root, 'package-lock.json');
  55. if (!fs.existsSync(manifestPath)) issues.push('missing skill-package-manifest.json');
  56. if (!fs.existsSync(packagePath)) issues.push('missing package.json');
  57. if (issues.length) return { status: 'error', root, issues, warnings };
  58. const manifest = readJson(manifestPath);
  59. const packageJson = readJson(packagePath);
  60. const actualSkills = listSkillNames(root);
  61. const actualTools = listRegisteredTools(root);
  62. const manifestSkills = Array.isArray(manifest.skills) ? manifest.skills : [];
  63. const manifestTools = Array.isArray(manifest.mcpTools) ? manifest.mcpTools : [];
  64. const skillDiff = diff(manifestSkills, actualSkills);
  65. const toolDiff = diff(manifestTools, actualTools);
  66. if (skillDiff.missing.length || skillDiff.extra.length) {
  67. issues.push(`skill index mismatch: missing=[${skillDiff.missing.join(', ')}], extra=[${skillDiff.extra.join(', ')}]`);
  68. }
  69. if (toolDiff.missing.length || toolDiff.extra.length) {
  70. issues.push(`MCP tool index mismatch: missing=[${toolDiff.missing.join(', ')}], extra=[${toolDiff.extra.join(', ')}]`);
  71. }
  72. if (duplicateValues(manifestSkills).length) {
  73. issues.push(`duplicate skills in manifest: ${duplicateValues(manifestSkills).join(', ')}`);
  74. }
  75. if (duplicateValues(manifestTools).length) {
  76. issues.push(`duplicate MCP tools in manifest: ${duplicateValues(manifestTools).join(', ')}`);
  77. }
  78. if (manifest.entrySkill && !manifestSkills.includes(manifest.entrySkill)) {
  79. issues.push(`entrySkill is not listed in skills: ${manifest.entrySkill}`);
  80. }
  81. const referenceFields = [
  82. 'apiCatalog',
  83. 'capabilityMap',
  84. 'taobaoV7Doc',
  85. 'bossPromptTemplates',
  86. 'liveAcceptanceChecklist',
  87. 'troubleshootingPlaybook',
  88. 'paymentLinksDoc',
  89. 'contextBudgetDoc',
  90. 'costMeteringSpec',
  91. 'apiErrorCodesDoc'
  92. ];
  93. for (const field of referenceFields) {
  94. if (!manifest[field]) continue;
  95. const target = path.join(root, manifest[field]);
  96. if (!fs.existsSync(target)) issues.push(`manifest.${field} points to missing file: ${manifest[field]}`);
  97. }
  98. if (packageJson.version !== manifest.version) {
  99. issues.push(`version mismatch: package.json=${packageJson.version}, manifest=${manifest.version}`);
  100. }
  101. if (fs.existsSync(lockPath)) {
  102. const lock = readJson(lockPath);
  103. if (lock.version && lock.version !== packageJson.version) {
  104. issues.push(`version mismatch: package-lock.json=${lock.version}, package.json=${packageJson.version}`);
  105. }
  106. if (lock.packages && lock.packages[''] && lock.packages[''].version !== packageJson.version) {
  107. issues.push(`version mismatch: package-lock root=${lock.packages[''].version}, package.json=${packageJson.version}`);
  108. }
  109. }
  110. const backups = listCatalogBackups(root);
  111. if (backups.length) {
  112. warnings.push(`local catalog backups detected; release builders must exclude: ${backups.join(', ')}`);
  113. }
  114. return {
  115. status: issues.length ? 'error' : 'ok',
  116. root,
  117. version: packageJson.version,
  118. skills: actualSkills,
  119. mcpTools: actualTools,
  120. warnings,
  121. issues
  122. };
  123. }
  124. function main() {
  125. const result = auditIndex();
  126. console.log(JSON.stringify(result, null, 2));
  127. process.exitCode = result.status === 'ok' ? 0 : 1;
  128. }
  129. if (require.main === module) main();
  130. module.exports = {
  131. auditIndex,
  132. listRegisteredTools,
  133. listSkillNames
  134. };