supernal-coding
Version:
Comprehensive development workflow CLI with kanban task management, project validation, git safety hooks, and cross-project distribution system
908 lines (759 loc) • 32.5 kB
JavaScript
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const yaml = require('js-yaml');
const { getConfig } = require('../../../scripts/config-loader');
/**
* Comprehensive Repository Health Check and Self-Validation System
* Validates equipment pack installation, system health, and provides actionable feedback
* Now includes requirements validation functionality
*/
const VALIDATION_CATEGORIES = {
EQUIPMENT_PACK: 'Equipment Pack Installation',
PROJECT_TYPE: 'Project Type Configuration',
CURSOR_RULES: 'Cursor Rules System',
MCP_CONFIG: 'MCP Configuration',
ENVIRONMENT: 'Environment Setup',
TESTING: 'Testing System',
AUTOMATION: 'Automation Capabilities',
HEALTH: 'System Health',
REQUIREMENTS: 'Requirements Validation' // NEW
};
const VALIDATION_LEVELS = {
CRITICAL: { level: 'critical', icon: '❌', color: 'red' },
WARNING: { level: 'warning', icon: '⚠️', color: 'yellow' },
INFO: { level: 'info', icon: 'ℹ️', color: 'blue' },
SUCCESS: { level: 'success', icon: '✅', color: 'green' }
};
function validateConfiguration(config) {
console.log(chalk.blue('🔍 Configuration Validation'));
console.log(chalk.blue('=' .repeat(30)));
const validation = config.validateConfiguration();
if (!validation.valid) {
console.log(chalk.red('❌ Configuration validation failed:'));
validation.errors.forEach(error => {
console.log(chalk.red(` ✗ ${error}`));
});
return false;
}
console.log(chalk.green('✅ Configuration is valid'));
// Show warnings if any
if (validation.warnings.length > 0) {
console.log(chalk.yellow('\n⚠️ Warnings:'));
validation.warnings.forEach(warning => {
console.log(chalk.yellow(` ⚠ ${warning}`));
});
}
// Show configured directories
console.log(chalk.blue('\n📁 Configured Directories:'));
console.log(` Requirements: ${config.getRequirementsDirectory()}`);
console.log(` Kanban: ${config.getKanbanBaseDirectory()}`);
return true;
}
async function validateCommand(options) {
try {
// NEW: Check if this is a requirements validation request
if (options.requirements || options.req) {
return await validateRequirements(options);
}
console.log(chalk.blue('🔍 Supernal Coding Repository Health Check'));
console.log(chalk.blue('=' .repeat(60)));
// Load configuration - only when actually running validation, not during init
let config;
try {
config = getConfig();
config.load();
} catch (error) {
console.log(chalk.yellow('⚠️ Configuration not found or invalid. This may be expected for new projects.'));
console.log(chalk.yellow(' Run "sc init" to set up the project configuration.'));
console.log('');
// For now, continue with basic validation that doesn't require config
config = null;
}
// First validate configuration (only if config is available)
if (config && !validateConfiguration(config)) {
console.log(chalk.red('\n❌ Configuration validation failed. Please fix configuration issues first.'));
process.exit(1);
}
console.log('');
const targetDir = options.directory || process.cwd();
const results = {
passed: 0,
failed: 0,
warnings: 0,
info: 0,
checks: []
};
// Equipment Pack Installation Validation
console.log(chalk.yellow(`\n🏗️ ${VALIDATION_CATEGORIES.EQUIPMENT_PACK}`));
await validateEquipmentPack(targetDir, config, results);
// Project Type Configuration Validation
console.log(chalk.yellow(`\n🎯 ${VALIDATION_CATEGORIES.PROJECT_TYPE}`));
await validateProjectType(targetDir, config, results);
// Cursor Rules System Validation
console.log(chalk.yellow(`\n📋 ${VALIDATION_CATEGORIES.CURSOR_RULES}`));
await validateCursorRules(targetDir, config, results);
// MCP Configuration Validation
console.log(chalk.yellow(`\n🔌 ${VALIDATION_CATEGORIES.MCP_CONFIG}`));
await validateMCPConfig(targetDir, config, results);
// Environment Setup Validation
console.log(chalk.yellow(`\n🌍 ${VALIDATION_CATEGORIES.ENVIRONMENT}`));
await validateEnvironment(targetDir, config, results);
// Testing System Validation
console.log(chalk.yellow(`\n🧪 ${VALIDATION_CATEGORIES.TESTING}`));
await validateTesting(targetDir, config, results);
// Automation Capabilities Validation
console.log(chalk.yellow(`\n🤖 ${VALIDATION_CATEGORIES.AUTOMATION}`));
await validateAutomation(targetDir, config, results);
// System Health Validation
console.log(chalk.yellow(`\n🏥 ${VALIDATION_CATEGORIES.HEALTH}`));
await validateHealth(targetDir, config, results);
// Display Summary
displayValidationSummary(results, options);
// Generate actionable recommendations
await generateRecommendations(targetDir, config, results);
// Exit with appropriate code
if (results.failed > 0) {
process.exit(1);
}
} catch (error) {
console.error(chalk.red('❌ Validation failed:'), error.message);
if (options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
async function loadConfiguration(targetDir) {
try {
const configPath = path.join(targetDir, '.supernal-config.json');
if (await fs.pathExists(configPath)) {
return await fs.readJSON(configPath);
}
} catch (error) {
// Configuration not found or invalid
}
return null;
}
async function validateEquipmentPack(targetDir, config, results) {
// Check if equipment pack is installed
await checkFile(targetDir, '.supernal-config.json', 'Equipment Pack configuration', results, VALIDATION_LEVELS.CRITICAL);
if (config) {
// Validate equipment pack structure
const requiredFields = ['version', 'equipmentPack'];
for (const field of requiredFields) {
if (!config[field]) {
addResult(results, VALIDATION_LEVELS.CRITICAL, `Configuration missing required field: ${field}`);
} else {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Configuration has ${field}`);
}
}
// Validate equipment pack components
if (config.equipmentPack) {
const ep = config.equipmentPack;
// Check installation timestamp
if (ep.installedAt) {
const installDate = new Date(ep.installedAt);
const daysSinceInstall = Math.floor((Date.now() - installDate.getTime()) / (1000 * 60 * 60 * 24));
if (daysSinceInstall > 30) {
addResult(results, VALIDATION_LEVELS.WARNING, `Equipment pack installed ${daysSinceInstall} days ago - consider updating`);
} else {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Equipment pack recently installed (${daysSinceInstall} days ago)`);
}
}
// Check project type detection
if (ep.projectType) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Project type detected: ${ep.projectType.name}`);
if (ep.projectType.confidence < 0.5) {
addResult(results, VALIDATION_LEVELS.WARNING, `Low project type detection confidence: ${Math.round(ep.projectType.confidence * 100)}%`);
}
}
// Check features
if (ep.features) {
const features = ep.features;
addResult(results, VALIDATION_LEVELS.SUCCESS, `Core System: ${features.coreSystem ? 'enabled' : 'disabled'}`);
addResult(results, VALIDATION_LEVELS.INFO, `Git Management: ${features.gitManagement ? 'enabled' : 'disabled'}`);
}
}
} else {
addResult(results, VALIDATION_LEVELS.CRITICAL, 'Equipment pack not installed - run: supernal-coding init');
}
}
async function validateProjectType(targetDir, config, results) {
if (!config?.equipmentPack?.projectType) {
addResult(results, VALIDATION_LEVELS.WARNING, 'Project type not detected');
return;
}
const projectType = config.equipmentPack.projectType;
// Validate project type against actual project structure
const actualIndicators = await detectProjectIndicators(targetDir);
if (actualIndicators.length > 0) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Project indicators found: ${actualIndicators.join(', ')}`);
// Check if detected type matches actual indicators
const typeMatch = actualIndicators.some(indicator =>
indicator.toLowerCase().includes(projectType.type.toLowerCase()) ||
projectType.type.toLowerCase().includes(indicator.toLowerCase())
);
if (typeMatch) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Project type matches detected indicators');
} else {
addResult(results, VALIDATION_LEVELS.WARNING, 'Project type may not match actual project structure');
}
}
// Validate project-specific packages
if (await fs.pathExists(path.join(targetDir, 'package.json'))) {
const packageJson = await fs.readJSON(path.join(targetDir, 'package.json'));
const deps = {...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {})};
// Check for project-type specific dependencies
const typeSpecificDeps = getExpectedDependencies(projectType.type);
const foundDeps = typeSpecificDeps.filter(dep => deps[dep]);
if (foundDeps.length > 0) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Project-specific dependencies found: ${foundDeps.join(', ')}`);
}
}
}
async function validateCursorRules(targetDir, config, results) {
const cursorRulesDir = path.join(targetDir, '.cursor/rules');
// Check if cursor rules directory exists
if (await fs.pathExists(cursorRulesDir)) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Cursor rules directory exists');
// Check for core rules
const coreRules = ['agent-on-board', 'dev_workflow', 'agent-hand-off', 'avoid-anti-patterns'];
const existingRules = await fs.readdir(cursorRulesDir);
for (const rule of coreRules) {
const ruleFile = `${rule}.mdc`;
if (existingRules.includes(ruleFile)) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Core rule installed: ${rule}`);
} else {
addResult(results, VALIDATION_LEVELS.WARNING, `Core rule missing: ${rule}`);
}
}
// Check for project-specific rules
if (config?.equipmentPack?.projectType) {
const projectType = config.equipmentPack.projectType.type;
const expectedAdditionalRules = getExpectedAdditionalRules(projectType);
for (const rule of expectedAdditionalRules) {
const ruleFile = `${rule}.mdc`;
if (existingRules.includes(ruleFile)) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Project-specific rule installed: ${rule}`);
} else {
addResult(results, VALIDATION_LEVELS.INFO, `Project-specific rule could be added: ${rule}`);
}
}
}
// Check README
if (existingRules.includes('README.md')) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Cursor rules README exists');
} else {
addResult(results, VALIDATION_LEVELS.INFO, 'Cursor rules README missing');
}
} else {
addResult(results, VALIDATION_LEVELS.CRITICAL, 'Cursor rules directory missing');
}
}
async function validateMCPConfig(targetDir, config, results) {
const mcpConfigPath = path.join(targetDir, '.cursor/mcp.json');
// Check if MCP configuration exists
if (await fs.pathExists(mcpConfigPath)) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'MCP configuration exists');
try {
const mcpConfig = await fs.readJSON(mcpConfigPath);
// Validate MCP configuration structure
if (mcpConfig.mcpServers) {
const serverCount = Object.keys(mcpConfig.mcpServers).length;
addResult(results, VALIDATION_LEVELS.SUCCESS, `MCP servers configured: ${serverCount}`);
// Check for core MCP server
if (mcpConfig.mcpServers['supernal-coding']) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Core MCP server configured');
} else {
addResult(results, VALIDATION_LEVELS.WARNING, 'Core MCP server missing');
}
// Check for project-specific MCP servers
if (config?.equipmentPack?.projectType) {
const projectType = config.equipmentPack.projectType.type;
const expectedServers = getExpectedMCPServers(projectType);
for (const server of expectedServers) {
if (mcpConfig.mcpServers[server]) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Project-specific MCP server configured: ${server}`);
} else {
addResult(results, VALIDATION_LEVELS.INFO, `Project-specific MCP server could be configured: ${server}`);
}
}
}
}
// Check environment variables
if (mcpConfig.env) {
const envVarCount = Object.keys(mcpConfig.env).length;
addResult(results, VALIDATION_LEVELS.SUCCESS, `MCP environment variables configured: ${envVarCount}`);
}
} catch (error) {
addResult(results, VALIDATION_LEVELS.CRITICAL, 'MCP configuration is invalid JSON');
}
} else {
addResult(results, VALIDATION_LEVELS.CRITICAL, 'MCP configuration missing');
}
}
async function validateEnvironment(targetDir, config, results) {
// Check for environment template
await checkFile(targetDir, '.env.template', 'Environment template', results, VALIDATION_LEVELS.SUCCESS);
// Check for environment-specific files
const envFiles = ['.env.development', '.env.production', '.env.test'];
for (const envFile of envFiles) {
await checkFile(targetDir, envFile, `Environment file: ${envFile}`, results, VALIDATION_LEVELS.SUCCESS);
}
// Check for .env file (should not exist in repo)
if (await fs.pathExists(path.join(targetDir, '.env'))) {
addResult(results, VALIDATION_LEVELS.WARNING, '.env file exists - ensure it\'s in .gitignore');
} else {
addResult(results, VALIDATION_LEVELS.SUCCESS, '.env file properly excluded from repository');
}
// Check .gitignore
const gitignorePath = path.join(targetDir, '.gitignore');
if (await fs.pathExists(gitignorePath)) {
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
if (gitignoreContent.includes('.env')) {
addResult(results, VALIDATION_LEVELS.SUCCESS, '.env properly ignored in .gitignore');
} else {
addResult(results, VALIDATION_LEVELS.WARNING, '.env not found in .gitignore');
}
}
}
async function validateTesting(targetDir, config, results) {
// Check testing directories
const testDirs = ['tests', 'tests/e2e', 'tests/requirements'];
for (const testDir of testDirs) {
await checkDir(targetDir, testDir, `Testing directory: ${testDir}`, results, VALIDATION_LEVELS.SUCCESS);
}
// Check for test runner
await checkFile(targetDir, 'tests/e2e/lib/test-runner.js', 'E2E test runner', results, VALIDATION_LEVELS.SUCCESS);
// Check for test scenarios
const scenariosDir = path.join(targetDir, 'tests/e2e/scenarios');
if (await fs.pathExists(scenariosDir)) {
const scenarios = await fs.readdir(scenariosDir);
const yamlScenarios = scenarios.filter(f => f.endsWith('.yaml'));
if (yamlScenarios.length > 0) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `E2E test scenarios found: ${yamlScenarios.length}`);
} else {
addResult(results, VALIDATION_LEVELS.INFO, 'No E2E test scenarios found');
}
}
// Check for package.json test scripts
const packageJsonPath = path.join(targetDir, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath);
if (packageJson.scripts) {
const testScripts = Object.keys(packageJson.scripts).filter(s => s.includes('test'));
if (testScripts.length > 0) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `NPM test scripts found: ${testScripts.join(', ')}`);
} else {
addResult(results, VALIDATION_LEVELS.INFO, 'No NPM test scripts found');
}
}
}
}
async function validateAutomation(targetDir, config, results) {
// Check for automation scripts
const automationScripts = [
'scripts/agent-onboard.sh',
'scripts/validate-dependencies.sh',
'scripts/setup-pre-commit-hooks.sh'
];
for (const script of automationScripts) {
await checkFile(targetDir, script, `Automation script: ${path.basename(script)}`, results, VALIDATION_LEVELS.SUCCESS);
}
// Check for CLI system
await checkDir(targetDir, 'cli', 'CLI system', results, VALIDATION_LEVELS.SUCCESS);
await checkFile(targetDir, 'cli/index.js', 'CLI entry point', results, VALIDATION_LEVELS.SUCCESS);
// Check for workflow system
await checkFile(targetDir, 'wf.sh', 'Workflow system', results, VALIDATION_LEVELS.SUCCESS);
// Check Git setup
if (await fs.pathExists(path.join(targetDir, '.git'))) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Git repository initialized');
// Check for pre-commit hooks
const preCommitHook = path.join(targetDir, '.git/hooks/pre-commit');
if (await fs.pathExists(preCommitHook)) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Pre-commit hooks installed');
} else {
addResult(results, VALIDATION_LEVELS.INFO, 'Pre-commit hooks not installed');
}
} else {
addResult(results, VALIDATION_LEVELS.WARNING, 'Git repository not initialized');
}
}
async function validateHealth(targetDir, config, results) {
// Check Node.js version
try {
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
const majorVersion = parseInt(nodeVersion.substring(1).split('.')[0]);
if (majorVersion >= 18) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `Node.js version: ${nodeVersion}`);
} else {
addResult(results, VALIDATION_LEVELS.WARNING, `Node.js version ${nodeVersion} - recommend v18+`);
}
} catch (error) {
addResult(results, VALIDATION_LEVELS.CRITICAL, 'Node.js not available');
}
// Check npm/package manager
try {
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
addResult(results, VALIDATION_LEVELS.SUCCESS, `NPM version: ${npmVersion}`);
} catch (error) {
addResult(results, VALIDATION_LEVELS.WARNING, 'NPM not available');
}
// Check dependencies
const packageJsonPath = path.join(targetDir, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
const nodeModulesPath = path.join(targetDir, 'node_modules');
if (await fs.pathExists(nodeModulesPath)) {
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Dependencies installed');
} else {
addResult(results, VALIDATION_LEVELS.WARNING, 'Dependencies not installed - run: npm install');
}
}
// Check disk space
try {
const stats = await fs.stat(targetDir);
addResult(results, VALIDATION_LEVELS.SUCCESS, 'Repository directory accessible');
} catch (error) {
addResult(results, VALIDATION_LEVELS.CRITICAL, 'Repository directory not accessible');
}
}
// Helper functions
async function checkFile(targetDir, filePath, description, results, level) {
const fullPath = path.join(targetDir, filePath);
const exists = await fs.pathExists(fullPath);
if (exists) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `${description} exists`);
} else {
addResult(results, level, `${description} missing`);
}
return exists;
}
async function checkDir(targetDir, dirPath, description, results, level) {
const fullPath = path.join(targetDir, dirPath);
const exists = await fs.pathExists(fullPath);
if (exists) {
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
addResult(results, VALIDATION_LEVELS.SUCCESS, `${description} exists`);
} else {
addResult(results, VALIDATION_LEVELS.CRITICAL, `${description} is not a directory`);
}
} else {
addResult(results, level, `${description} missing`);
}
return exists;
}
function addResult(results, level, message) {
const check = {
level: level.level,
icon: level.icon,
message,
color: level.color
};
results.checks.push(check);
// Update counters
switch (level.level) {
case 'success':
results.passed++;
break;
case 'critical':
results.failed++;
break;
case 'warning':
results.warnings++;
break;
case 'info':
results.info++;
break;
}
// Display immediately
const colorFunc = chalk[level.color] || chalk.white;
console.log(colorFunc(` ${level.icon} ${message}`));
}
function displayValidationSummary(results, options) {
console.log(chalk.blue('\n📊 VALIDATION SUMMARY'));
console.log(chalk.blue('=' .repeat(60)));
console.log(chalk.green(`✅ Passed: ${results.passed}`));
console.log(chalk.red(`❌ Failed: ${results.failed}`));
console.log(chalk.yellow(`⚠️ Warnings: ${results.warnings}`));
console.log(chalk.blue(`ℹ️ Info: ${results.info}`));
const total = results.passed + results.failed + results.warnings + results.info;
console.log(chalk.gray(`📊 Total: ${total}`));
if (results.failed > 0) {
console.log(chalk.red('\n❌ Repository validation FAILED!'));
console.log(chalk.red(`Found ${results.failed} critical issues that need attention.`));
} else if (results.warnings > 0) {
console.log(chalk.yellow('\n⚠️ Repository validation passed with warnings.'));
console.log(chalk.yellow(`Found ${results.warnings} issues that should be addressed.`));
} else {
console.log(chalk.green('\n✅ Repository validation PASSED!'));
console.log(chalk.green('All systems are properly configured and healthy.'));
}
}
async function generateRecommendations(targetDir, config, results) {
const criticalIssues = results.checks.filter(c => c.level === 'critical');
const warnings = results.checks.filter(c => c.level === 'warning');
if (criticalIssues.length > 0 || warnings.length > 0) {
console.log(chalk.blue('\n💡 RECOMMENDED ACTIONS'));
console.log(chalk.blue('=' .repeat(60)));
if (criticalIssues.length > 0) {
console.log(chalk.red('🚨 Critical Issues:'));
// Equipment pack not installed
if (criticalIssues.some(i => i.message.includes('Equipment pack not installed'))) {
console.log(chalk.red(' 1. Run: supernal-coding init'));
}
// MCP configuration issues
if (criticalIssues.some(i => i.message.includes('MCP configuration'))) {
console.log(chalk.red(' 2. Check .cursor/mcp.json configuration'));
}
// Node.js issues
if (criticalIssues.some(i => i.message.includes('Node.js'))) {
console.log(chalk.red(' 3. Install Node.js v18+ from https://nodejs.org'));
}
}
if (warnings.length > 0) {
console.log(chalk.yellow('⚠️ Warnings:'));
// Dependencies not installed
if (warnings.some(i => i.message.includes('Dependencies not installed'))) {
console.log(chalk.yellow(' 1. Run: npm install'));
}
// Git not initialized
if (warnings.some(i => i.message.includes('Git repository not initialized'))) {
console.log(chalk.yellow(' 2. Run: git init'));
}
// Environment file issues
if (warnings.some(i => i.message.includes('.env'))) {
console.log(chalk.yellow(' 3. Create .env file from .env.template'));
}
}
console.log(chalk.gray('\n📞 Need Help?'));
console.log(chalk.gray(' - Run: supernal-coding validate --verbose (for detailed output)'));
console.log(chalk.gray(' - Run: supernal-coding workflow test (to test the system)'));
console.log(chalk.gray(' - Check documentation in docs/ directory'));
}
}
// Project-specific helper functions
async function detectProjectIndicators(targetDir) {
const indicators = [];
// Check package.json
const packageJsonPath = path.join(targetDir, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath);
const deps = {...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {})};
if (deps.react) indicators.push('React');
if (deps.express) indicators.push('Express');
if (deps.vue) indicators.push('Vue');
if (deps.angular) indicators.push('Angular');
if (deps.electron) indicators.push('Electron');
if (deps.commander) indicators.push('CLI');
}
// Check for Python files
const pythonFiles = ['requirements.txt', 'setup.py', 'pyproject.toml'];
for (const file of pythonFiles) {
if (await fs.pathExists(path.join(targetDir, file))) {
indicators.push('Python');
break;
}
}
// Check directory structure
const dirs = await fs.readdir(targetDir).catch(() => []);
if (dirs.includes('src') && dirs.includes('public')) indicators.push('Frontend');
if (dirs.includes('server') && dirs.includes('client')) indicators.push('Fullstack');
if (dirs.includes('ios') && dirs.includes('android')) indicators.push('Mobile');
return indicators;
}
function getExpectedDependencies(projectType) {
const deps = {
'web-frontend': ['react', 'vue', '@angular/core', 'webpack', 'vite'],
'node-backend': ['express', 'fastify', 'koa', 'helmet', 'cors'],
'python': ['flask', 'django', 'fastapi', 'requests'],
'mobile': ['react-native', 'expo', '@react-native-community'],
'desktop': ['electron', 'electron-builder', 'tauri'],
'devtools': ['commander', 'inquirer', 'chalk', 'ora']
};
return deps[projectType] || [];
}
function getExpectedAdditionalRules(projectType) {
const rules = {
'web-frontend': ['react-patterns', 'component-architecture', 'frontend-performance'],
'node-backend': ['api-design', 'security-patterns', 'database-patterns'],
'python': ['python-patterns', 'django-patterns', 'flask-patterns'],
'mobile': ['mobile-patterns', 'performance-optimization', 'platform-specific'],
'desktop': ['desktop-patterns', 'native-integration', 'cross-platform'],
'devtools': ['cli-patterns', 'tool-architecture', 'config-management']
};
return rules[projectType] || [];
}
function getExpectedMCPServers(projectType) {
const servers = {
'web-frontend': ['react-devtools'],
'node-backend': ['api-tools', 'database-tools'],
'python': ['python-tools'],
'mobile': ['mobile-tools'],
'desktop': ['desktop-tools']
};
return servers[projectType] || [];
}
/**
* NEW: Requirements Validation Function
* Extracted from scripts/validate-requirements.js for CLI integration
*/
async function validateRequirements(options = {}) {
console.log(chalk.blue('🔍 Requirements Validation System'));
console.log(chalk.blue('=' .repeat(50)));
// Default configuration
const defaultConfig = {
directory: config.getRequirementsDirectory(),
ignoreFolders: ['to-fix', 'archive', 'deprecated']
};
// Try to load config from file or use defaults
let config = defaultConfig;
try {
const configPath = path.join(process.cwd(), 'scripts/config.json');
if (fs.existsSync(configPath)) {
config = { ...defaultConfig, ...require(configPath) };
}
} catch (error) {
// Use defaults if config loading fails
}
const requirementsDir = config.directory;
if (!fs.existsSync(requirementsDir)) {
console.error(chalk.red(`❌ Requirements directory not found: ${requirementsDir}`));
return false;
}
console.log(`📋 Found requirements directory: ${requirementsDir}`);
if (options.ignoreToFix) {
console.log(chalk.yellow(`⚠️ Ignoring validation errors in: ${config.ignoreFolders.join(', ')}`));
}
const results = {
totalFiles: 0,
validFiles: 0,
invalidFiles: 0,
skippedFiles: 0,
errors: 0,
warnings: 0,
traceabilityScore: 0
};
const requirementFiles = findRequirementFiles(requirementsDir, config.ignoreFolders);
results.totalFiles = requirementFiles.length;
console.log(`📊 Found ${results.totalFiles} requirement files`);
// Validate each file
for (const filePath of requirementFiles) {
try {
const validation = validateRequirementFile(filePath);
if (validation.isValid) {
results.validFiles++;
} else {
results.invalidFiles++;
results.errors += validation.errors.length;
results.warnings += validation.warnings.length;
// Display errors and warnings
if (validation.errors.length > 0) {
console.log(chalk.red(`\n❌ ${path.relative(process.cwd(), filePath)}:`));
validation.errors.forEach(error => {
console.log(chalk.red(` • ${error}`));
});
}
if (validation.warnings.length > 0) {
console.log(chalk.yellow(`\n⚠️ ${path.relative(process.cwd(), filePath)}:`));
validation.warnings.forEach(warning => {
console.log(chalk.yellow(` • ${warning}`));
});
}
}
} catch (error) {
results.invalidFiles++;
results.errors++;
console.log(chalk.red(`\n❌ Error validating ${filePath}: ${error.message}`));
}
}
// Display summary
console.log(chalk.blue('\n📊 VALIDATION SUMMARY:'));
console.log(` Total files: ${results.totalFiles}`);
console.log(` Valid files: ${chalk.green(results.validFiles)}`);
console.log(` Invalid files: ${chalk.red(results.invalidFiles)}`);
console.log(` Skipped files: ${results.skippedFiles}`);
console.log(` Errors: ${chalk.red(results.errors)}`);
console.log(` Warnings: ${chalk.yellow(results.warnings)}`);
console.log(` Traceability score: ${results.traceabilityScore}%`);
return results.invalidFiles === 0;
}
/**
* Find all requirement files in directory, excluding specified folders
*/
function findRequirementFiles(dir, ignoreFolders = []) {
const files = [];
function scanDirectory(currentDir) {
const items = fs.readdirSync(currentDir);
for (const item of items) {
const fullPath = path.join(currentDir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
// Skip ignored folders
const shouldSkip = ignoreFolders.some(folder =>
item === folder ||
fullPath.includes(`/${folder}/`) ||
fullPath.includes(`\\${folder}\\`)
);
if (!shouldSkip) {
scanDirectory(fullPath);
}
} else if (item.endsWith('.md') && (item.startsWith('req-') || item.startsWith('REQ-'))) {
files.push(fullPath);
}
}
}
scanDirectory(dir);
return files;
}
/**
* Validate individual requirement file
*/
function validateRequirementFile(filePath) {
const result = {
isValid: true,
errors: [],
warnings: []
};
try {
const content = fs.readFileSync(filePath, 'utf8');
// Check for YAML frontmatter
if (!content.startsWith('---')) {
result.errors.push('Missing YAML frontmatter');
result.isValid = false;
} else {
// Validate YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
try {
const frontmatter = yaml.load(frontmatterMatch[1]);
// Check required fields
const requiredFields = ['id', 'title', 'category', 'priority', 'status'];
for (const field of requiredFields) {
if (!frontmatter[field]) {
result.errors.push(`Missing required field: ${field}`);
result.isValid = false;
}
}
// Check for Gherkin scenarios
if (!content.includes('```gherkin') && !content.includes('Feature:')) {
result.warnings.push('No Gherkin scenarios found');
}
} catch (yamlError) {
result.errors.push(`Invalid YAML frontmatter: ${yamlError.message}`);
result.isValid = false;
}
} else {
result.errors.push('Invalid YAML frontmatter format');
result.isValid = false;
}
}
} catch (error) {
result.errors.push(`File read error: ${error.message}`);
result.isValid = false;
}
return result;
}
module.exports = validateCommand;