#!/usr/bin/env npx ts-node /** * Service Configuration Validator * * Validates all per-feature YAML configurations for: * - No port conflicts across features * - All dependencies reference valid services * - Health check endpoints are defined * - Required fields present * * Usage: * pnpm services:validate # Validate all configs * pnpm services:validate --fix # Auto-fix issues where possible * pnpm services:validate --verbose # Show detailed output */ import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'yaml'; import { PATHS } from '../../configs/paths'; interface Service { id: string; name: string; type: string; port?: number; sharedPort?: boolean; // Intentional port sharing (dev servers) dependencies?: string[]; healthCheck?: { type: string; path?: string; url?: string; command?: string; name?: string; }; gpu?: boolean; critical?: boolean; entrypoint?: string; } // Known intentional port sharing (dev servers that never run simultaneously) const INTENTIONAL_SHARED_PORTS = new Set([5173]); // analytics + conversation-assistant dev interface FeatureConfig { feature: { id: string; name: string; description: string; owner?: string; }; ports: Record; services: Service[]; deployments?: Record; } interface ValidationError { feature: string; service?: string; type: 'error' | 'warning'; message: string; } const FEATURES_DIR = PATHS.servicesDir; function loadAllConfigs(): Map { const configs = new Map(); const files = fs.readdirSync(FEATURES_DIR).filter((f) => f.endsWith('.yaml') && !f.startsWith('_')); for (const file of files) { const content = fs.readFileSync(path.join(FEATURES_DIR, file), 'utf-8'); try { const config = yaml.parse(content) as FeatureConfig; if (config.feature && config.feature.id) { configs.set(config.feature.id, config); } } catch (e) { console.error(`Failed to parse ${file}: ${e}`); } } return configs; } function validatePortConflicts(configs: Map): ValidationError[] { const errors: ValidationError[] = []; const portUsage = new Map(); for (const [featureId, config] of configs) { // Check ports section for (const [portName, port] of Object.entries(config.ports || {})) { if (typeof port === 'number') { const key = `${featureId}.${portName}`; if (!portUsage.has(port)) { portUsage.set(port, []); } portUsage.get(port)!.push(key); } } // Check service ports for (const service of config.services || []) { if (service.port) { const key = `${featureId}.${service.id}`; if (!portUsage.has(service.port)) { portUsage.set(service.port, []); } portUsage.get(service.port)!.push(key); } } } // Find conflicts for (const [port, users] of portUsage) { // Skip intentional shared ports if (INTENTIONAL_SHARED_PORTS.has(port)) { continue; } // Filter out duplicates within the same feature (ports section + service) const uniqueFeatures = [...new Set(users.map((u) => u.split('.')[0]))]; if (uniqueFeatures.length > 1) { errors.push({ feature: 'global', type: 'error', message: `Port ${port} conflict: used by ${users.join(', ')}`, }); } } return errors; } function validateDependencies(configs: Map): ValidationError[] { const errors: ValidationError[] = []; // Build service registry const allServices = new Set(); for (const [featureId, config] of configs) { for (const service of config.services || []) { allServices.add(`${featureId}.${service.id}`); } } // Check all dependencies for (const [featureId, config] of configs) { for (const service of config.services || []) { for (const dep of service.dependencies || []) { if (!allServices.has(dep)) { errors.push({ feature: featureId, service: service.id, type: 'warning', message: `Dependency "${dep}" not found in any feature config`, }); } } } } return errors; } function validateRequiredFields(configs: Map): ValidationError[] { const errors: ValidationError[] = []; for (const [featureId, config] of configs) { // Check feature metadata if (!config.feature) { errors.push({ feature: featureId, type: 'error', message: 'Missing "feature" section', }); continue; } if (!config.feature.id) { errors.push({ feature: featureId, type: 'error', message: 'Missing "feature.id"', }); } if (!config.feature.name) { errors.push({ feature: featureId, type: 'error', message: 'Missing "feature.name"', }); } if (!config.feature.description) { errors.push({ feature: featureId, type: 'warning', message: 'Missing "feature.description"', }); } // Check services for (const service of config.services || []) { if (!service.id) { errors.push({ feature: featureId, type: 'error', message: 'Service missing "id"', }); } if (!service.name) { errors.push({ feature: featureId, service: service.id, type: 'error', message: 'Missing "name"', }); } if (!service.type) { errors.push({ feature: featureId, service: service.id, type: 'error', message: 'Missing "type"', }); } // Health check validation for exposed services // Skip for: databases, redis, dev servers (ephemeral), websockets const skipHealthCheck = service.type === 'postgresql' || service.type === 'redis' || service.id.includes('frontend-dev') || service.id.includes('websocket'); if (service.port && !service.healthCheck && !skipHealthCheck) { errors.push({ feature: featureId, service: service.id, type: 'warning', message: 'Exposed service missing "healthCheck"', }); } } } return errors; } function validatePortsMatchServices(configs: Map): ValidationError[] { const errors: ValidationError[] = []; for (const [featureId, config] of configs) { const declaredPorts = new Set(); const usedPorts = new Set(); // Collect declared ports for (const port of Object.values(config.ports || {})) { if (typeof port === 'number') { declaredPorts.add(port); } } // Collect used ports for (const service of config.services || []) { if (service.port) { usedPorts.add(service.port); } } // Check for unused declared ports for (const port of declaredPorts) { if (!usedPorts.has(port)) { errors.push({ feature: featureId, type: 'warning', message: `Declared port ${port} not used by any service`, }); } } // Check for undeclared used ports for (const port of usedPorts) { if (!declaredPorts.has(port)) { errors.push({ feature: featureId, type: 'warning', message: `Service uses port ${port} not declared in ports section`, }); } } } return errors; } function main(): void { const args = process.argv.slice(2); const verbose = args.includes('--verbose') || args.includes('-v'); console.log('šŸ” Validating service configurations...\n'); const configs = loadAllConfigs(); console.log(`Found ${configs.size} feature configurations\n`); const allErrors: ValidationError[] = []; // Run validations if (verbose) console.log('Checking port conflicts...'); allErrors.push(...validatePortConflicts(configs)); if (verbose) console.log('Checking dependencies...'); allErrors.push(...validateDependencies(configs)); if (verbose) console.log('Checking required fields...'); allErrors.push(...validateRequiredFields(configs)); if (verbose) console.log('Checking ports match services...'); allErrors.push(...validatePortsMatchServices(configs)); // Report results const errors = allErrors.filter((e) => e.type === 'error'); const warnings = allErrors.filter((e) => e.type === 'warning'); if (errors.length > 0) { console.log('āŒ Errors:'); for (const error of errors) { const location = error.service ? `${error.feature}.${error.service}` : error.feature; console.log(` [${location}] ${error.message}`); } console.log(''); } if (warnings.length > 0) { console.log('āš ļø Warnings:'); for (const warning of warnings) { const location = warning.service ? `${warning.feature}.${warning.service}` : warning.feature; console.log(` [${location}] ${warning.message}`); } console.log(''); } // Summary console.log('šŸ“Š Summary:'); console.log(` Features: ${configs.size}`); console.log(` Errors: ${errors.length}`); console.log(` Warnings: ${warnings.length}`); if (errors.length === 0) { console.log('\nāœ… All validations passed!'); } else { console.log('\nāŒ Validation failed - please fix errors above'); process.exit(1); } } main();