claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
234 lines (195 loc) ⢠6.61 kB
text/typescript
/**
* Skill Markdown Linting Utility
*
* Validates all SKILL.md files against standardized structure.
* Integrates with CI/CD pipeline for automated validation.
*
* Usage:
* tsx scripts/lint-skill-markdown.ts
* tsx scripts/lint-skill-markdown.ts --skill=cfn-coordination
* tsx scripts/lint-skill-markdown.ts --strict
*
* Exit codes:
* 0 - All validations passed
* 1 - Validation errors found
* 2 - Warnings found (strict mode only)
*
* @module scripts/lint-skill-markdown
* @version 1.0.0
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { validateSkillMarkdown, getValidationSummary } from '../src/lib/skill-markdown-validator';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface LintOptions {
skillName?: string;
strict?: boolean;
verbose?: boolean;
}
interface LintResult {
skillName: string;
valid: boolean;
errorCount: number;
warningCount: number;
errors: string[];
warnings: string[];
}
const SKILLS_DIR = path.resolve(__dirname, '../.claude/skills');
function parseArgs(): LintOptions {
const args = process.argv.slice(2);
const options: LintOptions = {
strict: false,
verbose: false,
};
args.forEach((arg) => {
if (arg.startsWith('--skill=')) {
options.skillName = arg.split('=')[1];
} else if (arg === '--strict') {
options.strict = true;
} else if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--help' || arg === '-h') {
printUsage();
process.exit(0);
}
});
return options;
}
function printUsage(): void {
console.log(`
Skill Markdown Linting Utility
Usage:
tsx scripts/lint-skill-markdown.ts [options]
Options:
--skill=NAME Lint specific skill by name
--strict Treat warnings as errors
--verbose, -v Enable verbose output
--help, -h Show this help message
Examples:
tsx scripts/lint-skill-markdown.ts
tsx scripts/lint-skill-markdown.ts --skill=cfn-coordination
tsx scripts/lint-skill-markdown.ts --strict --verbose
`);
}
function findSkillDirectories(): string[] {
if (!fs.existsSync(SKILLS_DIR)) {
return [];
}
return fs
.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)
.filter((name) => !name.startsWith('.') && !name.startsWith('_'));
}
function lintSkill(skillName: string, options: LintOptions): LintResult {
const result: LintResult = {
skillName,
valid: false,
errorCount: 0,
warningCount: 0,
errors: [],
warnings: [],
};
const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
if (!fs.existsSync(skillPath)) {
result.errors.push('SKILL.md file not found');
result.errorCount = 1;
return result;
}
try {
const content = fs.readFileSync(skillPath, 'utf-8');
const basePath = path.join(SKILLS_DIR, skillName);
const validation = validateSkillMarkdown(content, basePath);
result.valid = validation.valid;
result.errors = validation.errors;
result.warnings = validation.warnings;
result.errorCount = validation.errors.length;
result.warningCount = validation.warnings.length;
} catch (error) {
result.errors.push(
`Validation failed: ${error instanceof Error ? error.message : String(error)}`
);
result.errorCount = result.errors.length;
}
return result;
}
async function main(): Promise<void> {
const options = parseArgs();
const results: LintResult[] = [];
let totalErrors = 0;
let totalWarnings = 0;
console.log('========================================');
console.log('Skill Markdown Linting');
console.log('========================================\n');
if (options.strict) {
console.log('š STRICT MODE - Warnings treated as errors\n');
}
// Determine which skills to lint
let skillsToLint: string[] = [];
if (options.skillName) {
skillsToLint = [options.skillName];
} else {
skillsToLint = findSkillDirectories();
}
console.log(`Linting ${skillsToLint.length} skills...\n`);
// Lint each skill
for (const skillName of skillsToLint) {
const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
if (!fs.existsSync(skillPath)) {
console.log(`ā ${skillName} - No SKILL.md file`);
continue;
}
const result = lintSkill(skillName, options);
results.push(result);
totalErrors += result.errorCount;
totalWarnings += result.warningCount;
// Print result
if (result.errorCount === 0 && result.warningCount === 0) {
console.log(`ā ${skillName}`);
} else if (result.errorCount === 0) {
console.log(`ā ${skillName} (${result.warningCount} warnings)`);
} else {
console.log(`ā ${skillName} (${result.errorCount} errors, ${result.warningCount} warnings)`);
}
// Print details in verbose mode
if (options.verbose) {
if (result.errors.length > 0) {
console.log(' Errors:');
result.errors.forEach((error) => console.log(` - ${error}`));
}
if (result.warnings.length > 0) {
console.log(' Warnings:');
result.warnings.forEach((warning) => console.log(` - ${warning}`));
}
}
}
// Print summary
console.log('\n========================================');
console.log('Linting Summary');
console.log('========================================');
console.log(`Total skills: ${results.length}`);
console.log(`Errors: ${totalErrors}`);
console.log(`Warnings: ${totalWarnings}`);
const passedCount = results.filter((r) => r.errorCount === 0).length;
const failedCount = results.filter((r) => r.errorCount > 0).length;
console.log(`Passed: ${passedCount}`);
console.log(`Failed: ${failedCount}`);
// Exit with appropriate code
if (totalErrors > 0) {
console.log('\nā Linting failed with errors');
process.exit(1);
} else if (options.strict && totalWarnings > 0) {
console.log('\nā Linting failed with warnings (strict mode)');
process.exit(2);
} else {
console.log('\nā All linting checks passed');
process.exit(0);
}
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});