UNPKG

backend-mcp

Version:

Generador automático de backends con Node.js, Express, Prisma y módulos configurables. Servidor MCP compatible con npx para agentes IA. Soporta PostgreSQL, MySQL, MongoDB y SQLite.

407 lines (328 loc) 12.4 kB
#!/usr/bin/env node /** * 🔍 Script de Validación de Módulos MCP Backend * * Este script valida la integridad y estructura de todos los módulos * del framework MCP Backend. */ const fs = require('fs-extra'); const path = require('path'); const yaml = require('js-yaml'); const chalk = require('chalk'); const glob = require('glob'); class ModuleValidator { constructor() { this.modulesPath = path.join(__dirname, '..', 'modules'); this.errors = []; this.warnings = []; this.validatedModules = []; } async validate() { console.log(chalk.blue.bold('\n🔍 Validador de Módulos MCP Backend\n')); try { const modules = await this.getModules(); if (modules.length === 0) { this.addError('No se encontraron módulos en la carpeta modules/'); return this.showResults(); } console.log(chalk.gray(`Validando ${modules.length} módulos...\n`)); for (const module of modules) { await this.validateModule(module); } this.showResults(); } catch (error) { console.error(chalk.red('❌ Error durante la validación:'), error.message); process.exit(1); } } async getModules() { const moduleDirs = await fs.readdir(this.modulesPath); const modules = []; for (const dir of moduleDirs) { const modulePath = path.join(this.modulesPath, dir); const stat = await fs.stat(modulePath); if (stat.isDirectory()) { modules.push({ name: dir, path: modulePath }); } } return modules; } async validateModule(module) { console.log(chalk.yellow(`🔍 Validando módulo: ${module.name}`)); const validationResults = { name: module.name, path: module.path, hasManifest: false, hasInit: false, hasReadme: false, hasTemplates: false, manifestValid: false, errors: [], warnings: [] }; // Validar estructura básica await this.validateModuleStructure(module, validationResults); // Validar manifest.yaml await this.validateManifest(module, validationResults); // Validar init.js await this.validateInitScript(module, validationResults); // Validar README.md await this.validateReadme(module, validationResults); // Validar templates await this.validateTemplates(module, validationResults); this.validatedModules.push(validationResults); // Mostrar resultado del módulo this.showModuleResult(validationResults); } async validateModuleStructure(module, results) { const requiredFiles = [ 'manifest.yaml', 'init.js', 'README.md' ]; const optionalDirs = [ 'templates', 'examples', 'src', 'docs' ]; // Verificar archivos requeridos for (const file of requiredFiles) { const filePath = path.join(module.path, file); const exists = await fs.pathExists(filePath); switch (file) { case 'manifest.yaml': results.hasManifest = exists; break; case 'init.js': results.hasInit = exists; break; case 'README.md': results.hasReadme = exists; break; } if (!exists) { results.errors.push(`Archivo requerido faltante: ${file}`); } } // Verificar directorios opcionales for (const dir of optionalDirs) { const dirPath = path.join(module.path, dir); const exists = await fs.pathExists(dirPath); if (dir === 'templates') { results.hasTemplates = exists; } if (!exists && dir === 'templates') { results.warnings.push(`Directorio recomendado faltante: ${dir}`); } } } async validateManifest(module, results) { if (!results.hasManifest) return; try { const manifestPath = path.join(module.path, 'manifest.yaml'); const manifestContent = await fs.readFile(manifestPath, 'utf8'); const manifest = yaml.load(manifestContent); // Validar estructura del manifest const requiredFields = [ 'module.name', 'module.version', 'module.description', 'module.category' ]; for (const field of requiredFields) { if (!this.getNestedProperty(manifest, field)) { results.errors.push(`Campo requerido faltante en manifest: ${field}`); } } // Validar que el nombre coincida con el directorio if (manifest.module && manifest.module.name !== module.name) { results.errors.push(`El nombre en manifest (${manifest.module.name}) no coincide con el directorio (${module.name})`); } // Validar versión semántica if (manifest.module && manifest.module.version) { const versionRegex = /^\d+\.\d+\.\d+$/; if (!versionRegex.test(manifest.module.version)) { results.warnings.push('La versión no sigue el formato semántico (x.y.z)'); } } // Validar dependencias if (manifest.dependencies) { if (manifest.dependencies.required && !Array.isArray(manifest.dependencies.required)) { results.errors.push('dependencies.required debe ser un array'); } if (manifest.dependencies.optional && !Array.isArray(manifest.dependencies.optional)) { results.errors.push('dependencies.optional debe ser un array'); } } // Validar triggers if (manifest.triggers && !Array.isArray(manifest.triggers)) { results.errors.push('triggers debe ser un array'); } results.manifestValid = results.errors.length === 0; } catch (error) { results.errors.push(`Error al parsear manifest.yaml: ${error.message}`); } } async validateInitScript(module, results) { if (!results.hasInit) return; try { const initPath = path.join(module.path, 'init.js'); const initContent = await fs.readFile(initPath, 'utf8'); // Verificar que sea un módulo válido de Node.js if (!initContent.includes('module.exports') && !initContent.includes('exports.')) { results.warnings.push('init.js no parece exportar una función'); } // Verificar que tenga una función async if (!initContent.includes('async') && !initContent.includes('Promise')) { results.warnings.push('init.js debería ser asíncrono para mejor compatibilidad'); } // Verificar manejo de errores básico if (!initContent.includes('try') && !initContent.includes('catch')) { results.warnings.push('init.js debería incluir manejo de errores'); } } catch (error) { results.errors.push(`Error al leer init.js: ${error.message}`); } } async validateReadme(module, results) { if (!results.hasReadme) return; try { const readmePath = path.join(module.path, 'README.md'); const readmeContent = await fs.readFile(readmePath, 'utf8'); // Verificar secciones básicas const requiredSections = [ '# ', // Título '## ', // Secciones 'Características', 'Configuración', 'Uso' ]; for (const section of requiredSections) { if (!readmeContent.includes(section)) { results.warnings.push(`Sección recomendada faltante en README: ${section}`); } } // Verificar longitud mínima if (readmeContent.length < 500) { results.warnings.push('README.md parece muy corto (< 500 caracteres)'); } // Verificar ejemplos de código if (!readmeContent.includes('```')) { results.warnings.push('README.md debería incluir ejemplos de código'); } } catch (error) { results.errors.push(`Error al leer README.md: ${error.message}`); } } async validateTemplates(module, results) { if (!results.hasTemplates) return; try { const templatesPath = path.join(module.path, 'templates'); const templateFiles = await glob('**/*.hbs', { cwd: templatesPath }); if (templateFiles.length === 0) { results.warnings.push('Directorio templates existe pero no contiene archivos .hbs'); } // Validar cada template for (const templateFile of templateFiles) { const templatePath = path.join(templatesPath, templateFile); const templateContent = await fs.readFile(templatePath, 'utf8'); // Verificar sintaxis básica de Handlebars const handlebarsRegex = /{{.*?}}/g; const matches = templateContent.match(handlebarsRegex); if (!matches || matches.length === 0) { results.warnings.push(`Template ${templateFile} no contiene variables de Handlebars`); } } } catch (error) { results.warnings.push(`Error al validar templates: ${error.message}`); } } getNestedProperty(obj, path) { return path.split('.').reduce((current, key) => current && current[key], obj); } showModuleResult(results) { const hasErrors = results.errors.length > 0; const hasWarnings = results.warnings.length > 0; if (hasErrors) { console.log(chalk.red(` ❌ ${results.name} - ${results.errors.length} errores`)); this.errors.push(...results.errors.map(err => `[${results.name}] ${err}`)); } else if (hasWarnings) { console.log(chalk.yellow(` ⚠️ ${results.name} - ${results.warnings.length} advertencias`)); } else { console.log(chalk.green(` ✅ ${results.name} - Válido`)); } this.warnings.push(...results.warnings.map(warn => `[${results.name}] ${warn}`)); } showResults() { console.log(chalk.blue.bold('\n📊 Resultados de Validación\n')); const totalModules = this.validatedModules.length; const validModules = this.validatedModules.filter(m => m.errors.length === 0).length; const modulesWithWarnings = this.validatedModules.filter(m => m.warnings.length > 0).length; console.log(chalk.gray(`Total de módulos: ${totalModules}`)); console.log(chalk.green(`Módulos válidos: ${validModules}`)); console.log(chalk.yellow(`Módulos con advertencias: ${modulesWithWarnings}`)); console.log(chalk.red(`Módulos con errores: ${totalModules - validModules}\n`)); // Mostrar errores if (this.errors.length > 0) { console.log(chalk.red.bold('❌ Errores encontrados:')); this.errors.forEach(error => { console.log(chalk.red(` • ${error}`)); }); console.log(''); } // Mostrar advertencias if (this.warnings.length > 0) { console.log(chalk.yellow.bold('⚠️ Advertencias:')); this.warnings.forEach(warning => { console.log(chalk.yellow(` • ${warning}`)); }); console.log(''); } // Resumen final if (this.errors.length === 0) { console.log(chalk.green.bold('🎉 Todos los módulos son válidos!')); if (this.warnings.length > 0) { console.log(chalk.yellow('💡 Considera revisar las advertencias para mejorar la calidad.')); } } else { console.log(chalk.red.bold('🚨 Se encontraron errores que deben ser corregidos.')); process.exit(1); } // Generar reporte detallado this.generateReport(); } async generateReport() { const report = { timestamp: new Date().toISOString(), summary: { totalModules: this.validatedModules.length, validModules: this.validatedModules.filter(m => m.errors.length === 0).length, modulesWithWarnings: this.validatedModules.filter(m => m.warnings.length > 0).length, totalErrors: this.errors.length, totalWarnings: this.warnings.length }, modules: this.validatedModules, errors: this.errors, warnings: this.warnings }; const reportPath = path.join(__dirname, '..', 'validation-report.json'); await fs.writeJson(reportPath, report, { spaces: 2 }); console.log(chalk.gray(`\n📄 Reporte detallado guardado en: ${reportPath}`)); } addError(message) { this.errors.push(message); } addWarning(message) { this.warnings.push(message); } } // Ejecutar si es llamado directamente if (require.main === module) { const validator = new ModuleValidator(); validator.validate().catch(console.error); } module.exports = ModuleValidator;