mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-09 19:03:28 +08:00
189 lines
6.0 KiB
JavaScript
189 lines
6.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Validate command markdown files are non-empty, readable,
|
|
* and have valid cross-references to other commands, agents, and skills.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT_DIR = path.join(__dirname, '../..');
|
|
const COMMANDS_DIR = path.join(ROOT_DIR, 'commands');
|
|
const AGENTS_DIR = path.join(ROOT_DIR, 'agents');
|
|
const SKILLS_DIR = path.join(ROOT_DIR, 'skills');
|
|
|
|
function validateFrontmatter(file, content) {
|
|
if (!content.startsWith('---\n')) {
|
|
return [];
|
|
}
|
|
|
|
const endIndex = content.indexOf('\n---\n', 4);
|
|
if (endIndex === -1) {
|
|
return [`${file} - frontmatter block is missing a closing --- delimiter`];
|
|
}
|
|
|
|
const block = content.slice(4, endIndex);
|
|
const errors = [];
|
|
|
|
for (const rawLine of block.split('\n')) {
|
|
const line = rawLine.trim();
|
|
if (!line || line.startsWith('#')) {
|
|
continue;
|
|
}
|
|
|
|
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
if (!match) {
|
|
errors.push(`${file} - invalid frontmatter line: ${rawLine}`);
|
|
continue;
|
|
}
|
|
|
|
const value = match[2].trim();
|
|
const isQuoted = (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
);
|
|
|
|
if (!isQuoted && value.startsWith('[') && !value.endsWith(']')) {
|
|
errors.push(
|
|
`${file} - frontmatter value for "${match[1]}" starts with "[" but is not a closed YAML sequence; wrap it in quotes`,
|
|
);
|
|
}
|
|
|
|
if (!isQuoted && value.startsWith('{') && !value.endsWith('}')) {
|
|
errors.push(
|
|
`${file} - frontmatter value for "${match[1]}" starts with "{" but is not a closed YAML mapping; wrap it in quotes`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
function validateCommands() {
|
|
if (!fs.existsSync(COMMANDS_DIR)) {
|
|
console.log('No commands directory found, skipping validation');
|
|
process.exit(0);
|
|
}
|
|
|
|
const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
|
|
let hasErrors = false;
|
|
let warnCount = 0;
|
|
|
|
// Build set of valid command names (without .md extension)
|
|
const validCommands = new Set(files.map(f => f.replace(/\.md$/, '')));
|
|
|
|
// Build set of valid agent names (without .md extension)
|
|
const validAgents = new Set();
|
|
if (fs.existsSync(AGENTS_DIR)) {
|
|
for (const f of fs.readdirSync(AGENTS_DIR)) {
|
|
if (f.endsWith('.md')) {
|
|
validAgents.add(f.replace(/\.md$/, ''));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build set of valid skill directory names
|
|
const validSkills = new Set();
|
|
if (fs.existsSync(SKILLS_DIR)) {
|
|
for (const f of fs.readdirSync(SKILLS_DIR)) {
|
|
const skillPath = path.join(SKILLS_DIR, f);
|
|
try {
|
|
if (fs.statSync(skillPath).isDirectory()) {
|
|
validSkills.add(f);
|
|
}
|
|
} catch {
|
|
// skip unreadable entries
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const file of files) {
|
|
const filePath = path.join(COMMANDS_DIR, file);
|
|
let content;
|
|
try {
|
|
content = fs.readFileSync(filePath, 'utf-8');
|
|
} catch (err) {
|
|
console.error(`ERROR: ${file} - ${err.message}`);
|
|
hasErrors = true;
|
|
continue;
|
|
}
|
|
|
|
// Validate the file is non-empty readable markdown
|
|
if (content.trim().length === 0) {
|
|
console.error(`ERROR: ${file} - Empty command file`);
|
|
hasErrors = true;
|
|
continue;
|
|
}
|
|
|
|
for (const error of validateFrontmatter(file, content)) {
|
|
console.error(`ERROR: ${error}`);
|
|
hasErrors = true;
|
|
}
|
|
|
|
// Strip fenced code blocks before checking cross-references.
|
|
// Examples/templates inside ``` blocks are not real references.
|
|
const contentNoCodeBlocks = content.replace(/```[\s\S]*?```/g, '');
|
|
|
|
// Check cross-references to other commands (e.g., `/build-fix`)
|
|
// Skip lines that describe hypothetical output (e.g., "→ Creates: `/new-table`")
|
|
// Process line-by-line so ALL command refs per line are captured
|
|
// (previous anchored regex /^.*`\/...`.*$/gm only matched the last ref per line)
|
|
for (const line of contentNoCodeBlocks.split('\n')) {
|
|
if (/creates:|would create:/i.test(line)) continue;
|
|
const lineRefs = line.matchAll(/`\/([a-z][-a-z0-9]*)`/g);
|
|
for (const match of lineRefs) {
|
|
const refName = match[1];
|
|
if (!validCommands.has(refName)) {
|
|
console.error(`ERROR: ${file} - references non-existent command /${refName}`);
|
|
hasErrors = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check agent references (e.g., "agents/planner.md" or "`planner` agent")
|
|
const agentPathRefs = contentNoCodeBlocks.matchAll(/agents\/([a-z][-a-z0-9]*)\.md/g);
|
|
for (const match of agentPathRefs) {
|
|
const refName = match[1];
|
|
if (!validAgents.has(refName)) {
|
|
console.error(`ERROR: ${file} - references non-existent agent agents/${refName}.md`);
|
|
hasErrors = true;
|
|
}
|
|
}
|
|
|
|
// Check skill directory references (e.g., "skills/tdd-workflow/")
|
|
// learned and imported are reserved roots (~/.claude/skills/); no local dir expected
|
|
const reservedSkillRoots = new Set(['learned', 'imported']);
|
|
const skillRefs = contentNoCodeBlocks.matchAll(/skills\/([a-z][-a-z0-9]*)\//g);
|
|
for (const match of skillRefs) {
|
|
const refName = match[1];
|
|
if (reservedSkillRoots.has(refName) || validSkills.has(refName)) continue;
|
|
console.warn(`WARN: ${file} - references skill directory skills/${refName}/ (not found locally)`);
|
|
warnCount++;
|
|
}
|
|
|
|
// Check agent name references in workflow diagrams (e.g., "planner -> tdd-guide")
|
|
const workflowLines = contentNoCodeBlocks.matchAll(/^([a-z][-a-z0-9]*(?:\s*->\s*[a-z][-a-z0-9]*)+)$/gm);
|
|
for (const match of workflowLines) {
|
|
const agents = match[1].split(/\s*->\s*/);
|
|
for (const agent of agents) {
|
|
if (!validAgents.has(agent)) {
|
|
console.error(`ERROR: ${file} - workflow references non-existent agent "${agent}"`);
|
|
hasErrors = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasErrors) {
|
|
process.exit(1);
|
|
}
|
|
|
|
let msg = `Validated ${files.length} command files`;
|
|
if (warnCount > 0) {
|
|
msg += ` (${warnCount} warnings)`;
|
|
}
|
|
console.log(msg);
|
|
}
|
|
|
|
validateCommands();
|