UNPKG

@vibe-dev-kit/cli

Version:

Advanced Command-line toolkit that analyzes your codebase and deploys project-aware rules, memories, commands and agents to any AI coding assistant - VDK is the world's first Vibe Development Kit

207 lines (174 loc) • 5.68 kB
#!/usr/bin/env node /** * Simple Rule Validator Tool * * This script performs basic validation on MDC files: * - Checks for duplicate rule IDs (filenames) * - Validates that YAML frontmatter is parseable */ import chalk from 'chalk'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; // Get directory paths for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Rule repository paths const rulesRootDir = path.join(__dirname, '../..'); const ruleDirectories = [ '.ai/rules', '.ai/rules/assistants', '.ai/rules/languages', '.ai/rules/stacks', '.ai/rules/tasks', '.ai/rules/technologies', '.ai/rules/tools', ]; // Track all rule IDs to check for duplicates const ruleIds = new Map(); // Simple YAML frontmatter parser function parseYamlFrontmatter(content) { const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/; const match = content.match(frontmatterRegex); if (!match) { // Check if there's YAML at the start without --- delimiters const lines = content.split('\n'); let yamlContent = ''; let foundYaml = false; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line === '---') { break; } if (line.includes(':') && !line.startsWith('#')) { foundYaml = true; yamlContent += lines[i] + '\n'; } else if (foundYaml && line === '') { continue; } else if (foundYaml) { break; } } return yamlContent.trim() ? yamlContent : null; } return match[1]; } // Basic YAML validation (just check for basic structure) function isValidYaml(yamlContent) { if (!yamlContent) return false; try { const lines = yamlContent.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed === '' || trimmed.startsWith('#')) continue; // Check for basic YAML key-value structure if (!trimmed.includes(':') && !trimmed.startsWith('-')) { return false; } } return true; } catch (_error) { return false; } } // Get all MDC files recursively async function getAllMdcFiles(dirPath) { const files = []; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isFile() && (entry.name.endsWith('.mdc') || entry.name.endsWith('.md'))) { // Skip common non-rule files if (!['README.md', 'CONTRIBUTING.md', 'CHANGELOG.md'].includes(entry.name)) { files.push(fullPath); } } // Don't recursively scan subdirectories since we list them explicitly } } catch (_error) { // Directory doesn't exist, skip } return files; } // Main validation function async function validateRules() { let errors = 0; let warnings = 0; let validFiles = 0; console.log(chalk.blue.bold('šŸ”Ž Validating MDC rule files...\n')); // Get all MDC files from all directories const allFiles = []; for (const dir of ruleDirectories) { const dirPath = path.join(rulesRootDir, dir); const files = await getAllMdcFiles(dirPath); allFiles.push(...files); } if (allFiles.length === 0) { console.log(chalk.yellow('No MDC files found in any directory.')); process.exit(0); } console.log(chalk.cyan(`Found ${allFiles.length} MDC files to validate\n`)); // Validate each file for (const filePath of allFiles) { const relativePath = path.relative(rulesRootDir, filePath); try { // Read file content const content = await fs.readFile(filePath, 'utf-8'); // Check for YAML frontmatter const yamlContent = parseYamlFrontmatter(content); if (!yamlContent) { console.log( chalk.yellow(` ⚠ ${relativePath}:`), chalk.yellow('No YAML frontmatter found') ); warnings++; } else if (!isValidYaml(yamlContent)) { console.log( chalk.red(` ✘ ${relativePath}:`), chalk.red('Invalid YAML frontmatter structure') ); errors++; } else { console.log(chalk.green(` āœ“ ${relativePath}`)); validFiles++; } // Check for duplicate rule IDs (based on filename) const fileName = path.basename(filePath); const ruleId = path.basename(fileName, path.extname(fileName)).toLowerCase(); if (ruleIds.has(ruleId)) { console.log( chalk.red(` ✘ Duplicate rule ID: ${ruleId}`), chalk.red(`\n Current: ${relativePath}`), chalk.red(`\n Existing: ${ruleIds.get(ruleId)}`) ); errors++; } else { ruleIds.set(ruleId, relativePath); } } catch (err) { console.log(chalk.red(` ✘ ${relativePath}: Error reading file: ${err.message}`)); errors++; } } // Summary console.log(chalk.blue.bold('\nValidation Summary:')); console.log(chalk.green(` Valid files: ${validFiles}`)); console.log(chalk.yellow(` Warnings: ${warnings}`)); console.log(chalk.red(` Errors: ${errors}`)); if (errors > 0) { console.log(chalk.red.bold('\nāŒ Validation failed. Please fix the errors above.')); process.exit(1); } else if (warnings > 0) { console.log(chalk.yellow.bold('\nāš ļø Validation passed with warnings.')); process.exit(0); } else { console.log(chalk.green.bold('\nāœ… All rules are valid!')); process.exit(0); } } // Run validation validateRules().catch((err) => { console.error(chalk.red(`An error occurred: ${err.message}`)); process.exit(1); });