Files
everything-claude-code/scripts/ci/catalog.js
2026-03-31 22:57:48 -07:00

647 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Verify repo catalog counts against tracked documentation files.
*
* Usage:
* node scripts/ci/catalog.js
* node scripts/ci/catalog.js --json
* node scripts/ci/catalog.js --md
* node scripts/ci/catalog.js --text
* node scripts/ci/catalog.js --write --text
*/
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '../..');
const README_PATH = path.join(ROOT, 'README.md');
const AGENTS_PATH = path.join(ROOT, 'AGENTS.md');
const README_ZH_CN_PATH = path.join(ROOT, 'README.zh-CN.md');
const DOCS_ZH_CN_README_PATH = path.join(ROOT, 'docs', 'zh-CN', 'README.md');
const DOCS_ZH_CN_AGENTS_PATH = path.join(ROOT, 'docs', 'zh-CN', 'AGENTS.md');
const WRITE_MODE = process.argv.includes('--write');
const OUTPUT_MODE = process.argv.includes('--md')
? 'md'
: process.argv.includes('--text')
? 'text'
: 'json';
function normalizePathSegments(relativePath) {
return relativePath.split(path.sep).join('/');
}
function listMatchingFiles(relativeDir, matcher) {
const directory = path.join(ROOT, relativeDir);
if (!fs.existsSync(directory)) {
return [];
}
return fs.readdirSync(directory, { withFileTypes: true })
.filter(entry => matcher(entry))
.map(entry => normalizePathSegments(path.join(relativeDir, entry.name)))
.sort();
}
function buildCatalog() {
const agents = listMatchingFiles('agents', entry => entry.isFile() && entry.name.endsWith('.md'));
const commands = listMatchingFiles('commands', entry => entry.isFile() && entry.name.endsWith('.md'));
const skills = listMatchingFiles('skills', entry => (
entry.isDirectory() && fs.existsSync(path.join(ROOT, 'skills', entry.name, 'SKILL.md'))
)).map(skillDir => `${skillDir}/SKILL.md`);
return {
agents: { count: agents.length, files: agents, glob: 'agents/*.md' },
commands: { count: commands.length, files: commands, glob: 'commands/*.md' },
skills: { count: skills.length, files: skills, glob: 'skills/*/SKILL.md' }
};
}
function readFileOrThrow(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch (error) {
throw new Error(`Failed to read ${path.basename(filePath)}: ${error.message}`);
}
}
function writeFileOrThrow(filePath, content) {
try {
fs.writeFileSync(filePath, content, 'utf8');
} catch (error) {
throw new Error(`Failed to write ${path.basename(filePath)}: ${error.message}`);
}
}
function replaceOrThrow(content, regex, replacer, source) {
if (!regex.test(content)) {
throw new Error(`${source} is missing the expected catalog marker`);
}
return content.replace(regex, replacer);
}
function parseReadmeExpectations(readmeContent) {
const expectations = [];
const quickStartMatch = readmeContent.match(/access to\s+(\d+)\s+agents,\s+(\d+)\s+skills,\s+and\s+(\d+)\s+commands/i);
if (!quickStartMatch) {
throw new Error('README.md is missing the quick-start catalog summary');
}
expectations.push(
{ category: 'agents', mode: 'exact', expected: Number(quickStartMatch[1]), source: 'README.md quick-start summary' },
{ category: 'skills', mode: 'exact', expected: Number(quickStartMatch[2]), source: 'README.md quick-start summary' },
{ category: 'commands', mode: 'exact', expected: Number(quickStartMatch[3]), source: 'README.md quick-start summary' }
);
const tablePatterns = [
{ category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' },
{ category: 'commands', regex: /\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+commands\s*\|/i, source: 'README.md comparison table' },
{ category: 'skills', regex: /\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+skills\s*\|/i, source: 'README.md comparison table' }
];
for (const pattern of tablePatterns) {
const match = readmeContent.match(pattern.regex);
if (!match) {
throw new Error(`${pattern.source} is missing the ${pattern.category} row`);
}
expectations.push({
category: pattern.category,
mode: 'exact',
expected: Number(match[1]),
source: `${pattern.source} (${pattern.category})`
});
}
const parityPatterns = [
{
category: 'agents',
regex: /^\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*12\s*\|$/im,
source: 'README.md parity table'
},
{
category: 'commands',
regex: /^\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\|\s*Instruction-based\s*\|\s*31\s*\|$/im,
source: 'README.md parity table'
},
{
category: 'skills',
regex: /^\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\|\s*10\s*\(native format\)\s*\|\s*37\s*\|$/im,
source: 'README.md parity table'
}
];
for (const pattern of parityPatterns) {
const match = readmeContent.match(pattern.regex);
if (!match) {
throw new Error(`${pattern.source} is missing the ${pattern.category} row`);
}
expectations.push({
category: pattern.category,
mode: 'exact',
expected: Number(match[1]),
source: `${pattern.source} (${pattern.category})`
});
}
return expectations;
}
function parseZhRootReadmeExpectations(readmeContent) {
const match = readmeContent.match(/你现在可以使用\s+(\d+)\s+个代理、\s*(\d+)\s*个技能和\s*(\d+)\s*个命令/i);
if (!match) {
throw new Error('README.zh-CN.md is missing the quick-start catalog summary');
}
return [
{ category: 'agents', mode: 'exact', expected: Number(match[1]), source: 'README.zh-CN.md quick-start summary' },
{ category: 'skills', mode: 'exact', expected: Number(match[2]), source: 'README.zh-CN.md quick-start summary' },
{ category: 'commands', mode: 'exact', expected: Number(match[3]), source: 'README.zh-CN.md quick-start summary' }
];
}
function parseZhDocsReadmeExpectations(readmeContent) {
const expectations = [];
const quickStartMatch = readmeContent.match(/你现在可以使用\s+(\d+)\s+个智能体、\s*(\d+)\s*项技能和\s*(\d+)\s*个命令了/i);
if (!quickStartMatch) {
throw new Error('docs/zh-CN/README.md is missing the quick-start catalog summary');
}
expectations.push(
{ category: 'agents', mode: 'exact', expected: Number(quickStartMatch[1]), source: 'docs/zh-CN/README.md quick-start summary' },
{ category: 'skills', mode: 'exact', expected: Number(quickStartMatch[2]), source: 'docs/zh-CN/README.md quick-start summary' },
{ category: 'commands', mode: 'exact', expected: Number(quickStartMatch[3]), source: 'docs/zh-CN/README.md quick-start summary' }
);
const tablePatterns = [
{ category: 'agents', regex: /\|\s*智能体\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s*个\s*\|/i, source: 'docs/zh-CN/README.md comparison table' },
{ category: 'commands', regex: /\|\s*命令\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s*个\s*\|/i, source: 'docs/zh-CN/README.md comparison table' },
{ category: 'skills', regex: /\|\s*技能\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s*项\s*\|/i, source: 'docs/zh-CN/README.md comparison table' }
];
for (const pattern of tablePatterns) {
const match = readmeContent.match(pattern.regex);
if (!match) {
throw new Error(`${pattern.source} is missing the ${pattern.category} row`);
}
expectations.push({
category: pattern.category,
mode: 'exact',
expected: Number(match[1]),
source: `${pattern.source} (${pattern.category})`
});
}
const parityPatterns = [
{
category: 'agents',
regex: /^\|\s*(?:\*\*)?智能体(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*12\s*\|$/im,
source: 'docs/zh-CN/README.md parity table'
},
{
category: 'commands',
regex: /^\|\s*(?:\*\*)?命令(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*共享\s*\|\s*基于指令\s*\|\s*31\s*\|$/im,
source: 'docs/zh-CN/README.md parity table'
},
{
category: 'skills',
regex: /^\|\s*(?:\*\*)?技能(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*共享\s*\|\s*10\s*\(原生格式\)\s*\|\s*37\s*\|$/im,
source: 'docs/zh-CN/README.md parity table'
}
];
for (const pattern of parityPatterns) {
const match = readmeContent.match(pattern.regex);
if (!match) {
throw new Error(`${pattern.source} is missing the ${pattern.category} row`);
}
expectations.push({
category: pattern.category,
mode: 'exact',
expected: Number(match[1]),
source: `${pattern.source} (${pattern.category})`
});
}
return expectations;
}
function parseAgentsDocExpectations(agentsContent) {
const summaryMatch = agentsContent.match(/providing\s+(\d+)\s+specialized agents,\s+(\d+)(\+)?\s+skills,\s+(\d+)\s+commands/i);
if (!summaryMatch) {
throw new Error('AGENTS.md is missing the catalog summary line');
}
const expectations = [
{ category: 'agents', mode: 'exact', expected: Number(summaryMatch[1]), source: 'AGENTS.md summary' },
{
category: 'skills',
mode: summaryMatch[3] ? 'minimum' : 'exact',
expected: Number(summaryMatch[2]),
source: 'AGENTS.md summary'
},
{ category: 'commands', mode: 'exact', expected: Number(summaryMatch[4]), source: 'AGENTS.md summary' }
];
const structurePatterns = [
{
category: 'agents',
mode: 'exact',
regex: /^\s*agents\/\s*[—–-]\s*(\d+)\s+specialized subagents\s*$/im,
source: 'AGENTS.md project structure'
},
{
category: 'skills',
mode: 'minimum',
regex: /^\s*skills\/\s*[—–-]\s*(\d+)(\+)?\s+workflow skills and domain knowledge\s*$/im,
source: 'AGENTS.md project structure'
},
{
category: 'commands',
mode: 'exact',
regex: /^\s*commands\/\s*[—–-]\s*(\d+)\s+slash commands\s*$/im,
source: 'AGENTS.md project structure'
}
];
for (const pattern of structurePatterns) {
const match = agentsContent.match(pattern.regex);
if (!match) {
throw new Error(`${pattern.source} is missing the ${pattern.category} entry`);
}
expectations.push({
category: pattern.category,
mode: pattern.mode === 'minimum' && match[2] ? 'minimum' : pattern.mode,
expected: Number(match[1]),
source: `${pattern.source} (${pattern.category})`
});
}
return expectations;
}
function parseZhAgentsDocExpectations(agentsContent) {
const summaryMatch = agentsContent.match(/提供\s+(\d+)\s+个专业代理、\s*(\d+)(\+)?\s*项技能、\s*(\d+)\s+条命令/i);
if (!summaryMatch) {
throw new Error('docs/zh-CN/AGENTS.md is missing the catalog summary line');
}
const expectations = [
{ category: 'agents', mode: 'exact', expected: Number(summaryMatch[1]), source: 'docs/zh-CN/AGENTS.md summary' },
{
category: 'skills',
mode: summaryMatch[3] ? 'minimum' : 'exact',
expected: Number(summaryMatch[2]),
source: 'docs/zh-CN/AGENTS.md summary'
},
{ category: 'commands', mode: 'exact', expected: Number(summaryMatch[4]), source: 'docs/zh-CN/AGENTS.md summary' }
];
const structurePatterns = [
{
category: 'agents',
mode: 'exact',
regex: /^\s*agents\/\s*[—–-]\s*(\d+)\s+个专业子代理\s*$/im,
source: 'docs/zh-CN/AGENTS.md project structure'
},
{
category: 'skills',
mode: 'minimum',
regex: /^\s*skills\/\s*[—–-]\s*(\d+)(\+)?\s+个工作流技能和领域知识\s*$/im,
source: 'docs/zh-CN/AGENTS.md project structure'
},
{
category: 'commands',
mode: 'exact',
regex: /^\s*commands\/\s*[—–-]\s*(\d+)\s+个斜杠命令\s*$/im,
source: 'docs/zh-CN/AGENTS.md project structure'
}
];
for (const pattern of structurePatterns) {
const match = agentsContent.match(pattern.regex);
if (!match) {
throw new Error(`${pattern.source} is missing the ${pattern.category} entry`);
}
expectations.push({
category: pattern.category,
mode: pattern.mode === 'minimum' && match[2] ? 'minimum' : pattern.mode,
expected: Number(match[1]),
source: `${pattern.source} (${pattern.category})`
});
}
return expectations;
}
function evaluateExpectations(catalog, expectations) {
return expectations.map(expectation => {
const actual = catalog[expectation.category].count;
const ok = expectation.mode === 'minimum'
? actual >= expectation.expected
: actual === expectation.expected;
return {
...expectation,
actual,
ok
};
});
}
function formatExpectation(expectation) {
const comparator = expectation.mode === 'minimum' ? '>=' : '=';
return `${expectation.source}: ${expectation.category} documented ${comparator} ${expectation.expected}, actual ${expectation.actual}`;
}
function syncEnglishReadme(content, catalog) {
let nextContent = content;
nextContent = replaceOrThrow(
nextContent,
/(access to\s+)(\d+)(\s+agents,\s+)(\d+)(\s+skills,\s+and\s+)(\d+)(\s+commands)/i,
(_, prefix, __, agentsSuffix, ___, skillsSuffix, ____, commandsSuffix) =>
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`,
'README.md quick-start summary'
);
nextContent = replaceOrThrow(
nextContent,
/(\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+agents\s*\|)/i,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'README.md comparison table (agents)'
);
nextContent = replaceOrThrow(
nextContent,
/(\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+commands\s*\|)/i,
(_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`,
'README.md comparison table (commands)'
);
nextContent = replaceOrThrow(
nextContent,
/(\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s+skills\s*\|)/i,
(_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`,
'README.md comparison table (skills)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*12\s*\|)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'README.md parity table (agents)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\|\s*Instruction-based\s*\|\s*31\s*\|)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`,
'README.md parity table (commands)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\|\s*10\s*\(native format\)\s*\|\s*37\s*\|)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`,
'README.md parity table (skills)'
);
return nextContent;
}
function syncEnglishAgents(content, catalog) {
let nextContent = content;
nextContent = replaceOrThrow(
nextContent,
/(providing\s+)(\d+)(\s+specialized agents,\s+)(\d+)(\+?)(\s+skills,\s+)(\d+)(\s+commands)/i,
(_, prefix, __, agentsSuffix, ___, skillsPlus, skillsSuffix, ____, commandsSuffix) =>
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsPlus}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`,
'AGENTS.md summary'
);
nextContent = replaceOrThrow(
nextContent,
/^(\s*agents\/\s*[—–-]\s*)(\d+)(\s+specialized subagents\s*)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'AGENTS.md project structure (agents)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\s*skills\/\s*[—–-]\s*)(\d+)(\+?)(\s+workflow skills and domain knowledge\s*)$/im,
(_, prefix, __, plus, suffix) => `${prefix}${catalog.skills.count}${plus}${suffix}`,
'AGENTS.md project structure (skills)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\s*commands\/\s*[—–-]\s*)(\d+)(\s+slash commands\s*)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`,
'AGENTS.md project structure (commands)'
);
return nextContent;
}
function syncZhRootReadme(content, catalog) {
return replaceOrThrow(
content,
/(你现在可以使用\s+)(\d+)(\s+个代理、\s*)(\d+)(\s*个技能和\s*)(\d+)(\s*个命令[。.!]?)/i,
(_, prefix, __, agentsSuffix, ___, skillsSuffix, ____, commandsSuffix) =>
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`,
'README.zh-CN.md quick-start summary'
);
}
function syncZhDocsReadme(content, catalog) {
let nextContent = content;
nextContent = replaceOrThrow(
nextContent,
/(你现在可以使用\s+)(\d+)(\s+个智能体、\s*)(\d+)(\s*项技能和\s*)(\d+)(\s*个命令了[。.!]?)/i,
(_, prefix, __, agentsSuffix, ___, skillsSuffix, ____, commandsSuffix) =>
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`,
'docs/zh-CN/README.md quick-start summary'
);
nextContent = replaceOrThrow(
nextContent,
/(\|\s*智能体\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s*个\s*\|)/i,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'docs/zh-CN/README.md comparison table (agents)'
);
nextContent = replaceOrThrow(
nextContent,
/(\|\s*命令\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s*个\s*\|)/i,
(_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`,
'docs/zh-CN/README.md comparison table (commands)'
);
nextContent = replaceOrThrow(
nextContent,
/(\|\s*技能\s*\|\s*(?:(?:PASS:|\u2705)\s*)?)(\d+)(\s*项\s*\|)/i,
(_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`,
'docs/zh-CN/README.md comparison table (skills)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\|\s*(?:\*\*)?智能体(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*共享\s*\(AGENTS\.md\)\s*\|\s*12\s*\|)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'docs/zh-CN/README.md parity table (agents)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\|\s*(?:\*\*)?命令(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*共享\s*\|\s*基于指令\s*\|\s*31\s*\|)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`,
'docs/zh-CN/README.md parity table (commands)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\|\s*(?:\*\*)?技能(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*共享\s*\|\s*10\s*\(原生格式\)\s*\|\s*37\s*\|)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`,
'docs/zh-CN/README.md parity table (skills)'
);
return nextContent;
}
function syncZhAgents(content, catalog) {
let nextContent = content;
nextContent = replaceOrThrow(
nextContent,
/(提供\s+)(\d+)(\s+个专业代理、\s*)(\d+)(\+?)(\s*项技能、\s*)(\d+)(\s+条命令)/i,
(_, prefix, __, agentsSuffix, ___, skillsPlus, skillsSuffix, ____, commandsSuffix) =>
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsPlus}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`,
'docs/zh-CN/AGENTS.md summary'
);
nextContent = replaceOrThrow(
nextContent,
/^(\s*agents\/\s*[—–-]\s*)(\d+)(\s+个专业子代理\s*)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`,
'docs/zh-CN/AGENTS.md project structure (agents)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\s*skills\/\s*[—–-]\s*)(\d+)(\+?)(\s+个工作流技能和领域知识\s*)$/im,
(_, prefix, __, plus, suffix) => `${prefix}${catalog.skills.count}${plus}${suffix}`,
'docs/zh-CN/AGENTS.md project structure (skills)'
);
nextContent = replaceOrThrow(
nextContent,
/^(\s*commands\/\s*[—–-]\s*)(\d+)(\s+个斜杠命令\s*)$/im,
(_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`,
'docs/zh-CN/AGENTS.md project structure (commands)'
);
return nextContent;
}
const DOCUMENT_SPECS = [
{
filePath: README_PATH,
parseExpectations: parseReadmeExpectations,
syncContent: syncEnglishReadme,
},
{
filePath: AGENTS_PATH,
parseExpectations: parseAgentsDocExpectations,
syncContent: syncEnglishAgents,
},
{
filePath: README_ZH_CN_PATH,
parseExpectations: parseZhRootReadmeExpectations,
syncContent: syncZhRootReadme,
},
{
filePath: DOCS_ZH_CN_README_PATH,
parseExpectations: parseZhDocsReadmeExpectations,
syncContent: syncZhDocsReadme,
},
{
filePath: DOCS_ZH_CN_AGENTS_PATH,
parseExpectations: parseZhAgentsDocExpectations,
syncContent: syncZhAgents,
},
];
function renderText(result) {
console.log('Catalog counts:');
console.log(`- agents: ${result.catalog.agents.count}`);
console.log(`- commands: ${result.catalog.commands.count}`);
console.log(`- skills: ${result.catalog.skills.count}`);
console.log('');
const mismatches = result.checks.filter(check => !check.ok);
if (mismatches.length === 0) {
console.log('Documentation counts match the repository catalog.');
return;
}
console.error('Documentation count mismatches found:');
for (const mismatch of mismatches) {
console.error(`- ${formatExpectation(mismatch)}`);
}
}
function renderMarkdown(result) {
const mismatches = result.checks.filter(check => !check.ok);
console.log('# ECC Catalog Verification\n');
console.log('| Category | Count | Pattern |');
console.log('| --- | ---: | --- |');
console.log(`| Agents | ${result.catalog.agents.count} | \`${result.catalog.agents.glob}\` |`);
console.log(`| Commands | ${result.catalog.commands.count} | \`${result.catalog.commands.glob}\` |`);
console.log(`| Skills | ${result.catalog.skills.count} | \`${result.catalog.skills.glob}\` |`);
console.log('');
if (mismatches.length === 0) {
console.log('Documentation counts match the repository catalog.');
return;
}
console.log('## Mismatches\n');
for (const mismatch of mismatches) {
console.log(`- ${formatExpectation(mismatch)}`);
}
}
function main() {
const catalog = buildCatalog();
if (WRITE_MODE) {
for (const spec of DOCUMENT_SPECS) {
const currentContent = readFileOrThrow(spec.filePath);
const nextContent = spec.syncContent(currentContent, catalog);
if (nextContent !== currentContent) {
writeFileOrThrow(spec.filePath, nextContent);
}
}
}
const expectations = DOCUMENT_SPECS.flatMap(spec => (
spec.parseExpectations(readFileOrThrow(spec.filePath))
));
const checks = evaluateExpectations(catalog, expectations);
const result = { catalog, checks };
if (OUTPUT_MODE === 'json') {
console.log(JSON.stringify(result, null, 2));
} else if (OUTPUT_MODE === 'md') {
renderMarkdown(result);
} else {
renderText(result);
}
if (checks.some(check => !check.ok)) {
process.exit(1);
}
}
try {
main();
} catch (error) {
console.error(`ERROR: ${error.message}`);
process.exit(1);
}