UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

939 lines (938 loc) 37.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TemplateValidator = exports.WarningType = exports.ErrorType = void 0; exports.createTemplateValidator = createTemplateValidator; exports.getGlobalTemplateValidator = getGlobalTemplateValidator; exports.setGlobalTemplateValidator = setGlobalTemplateValidator; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const semver = __importStar(require("semver")); const yaml = __importStar(require("js-yaml")); const template_engine_1 = require("./template-engine"); var ErrorType; (function (ErrorType) { ErrorType["MISSING_FIELD"] = "missing_field"; ErrorType["INVALID_TYPE"] = "invalid_type"; ErrorType["INVALID_VALUE"] = "invalid_value"; ErrorType["INVALID_REFERENCE"] = "invalid_reference"; ErrorType["CIRCULAR_DEPENDENCY"] = "circular_dependency"; ErrorType["VERSION_CONFLICT"] = "version_conflict"; ErrorType["FILE_NOT_FOUND"] = "file_not_found"; ErrorType["SYNTAX_ERROR"] = "syntax_error"; ErrorType["SECURITY_ISSUE"] = "security_issue"; ErrorType["COMPATIBILITY_ISSUE"] = "compatibility_issue"; })(ErrorType || (exports.ErrorType = ErrorType = {})); var WarningType; (function (WarningType) { WarningType["DEPRECATED_FEATURE"] = "deprecated_feature"; WarningType["MISSING_OPTIONAL"] = "missing_optional"; WarningType["PERFORMANCE_ISSUE"] = "performance_issue"; WarningType["BEST_PRACTICE"] = "best_practice"; WarningType["NAMING_CONVENTION"] = "naming_convention"; WarningType["DOCUMENTATION"] = "documentation"; WarningType["ACCESSIBILITY"] = "accessibility"; })(WarningType || (exports.WarningType = WarningType = {})); class TemplateValidator extends events_1.EventEmitter { constructor(config = {}) { super(); this.config = config; this.defaultConfig = { strict: true, checkSecurity: true, checkPerformance: true, checkBestPractices: true, checkAccessibility: false, maxComplexity: 100 }; this.builtinRules = []; this.securityPatterns = { dangerousCommands: [ /rm\s+-rf\s+\//, /eval\s*\(/, /exec\s*\(/, /require\s*\(\s*['"]\s*child_process/, /process\.env\./, /\$\{.*\}/ ], sensitiveFiles: [ /\.env/, /\.ssh/, /private.*key/, /password/, /secret/, /token/ ] }; this.config = { ...this.defaultConfig, ...config }; this.initializeBuiltinRules(); } initializeBuiltinRules() { // Structure validation rules this.builtinRules.push({ name: 'required_fields', description: 'Check for required template fields', check: (template) => { if (!template.id) { return { type: ErrorType.MISSING_FIELD, severity: 'critical', field: 'id', message: 'Template ID is required' }; } if (!template.name) { return { type: ErrorType.MISSING_FIELD, severity: 'critical', field: 'name', message: 'Template name is required' }; } if (!template.version) { return { type: ErrorType.MISSING_FIELD, severity: 'critical', field: 'version', message: 'Template version is required' }; } if (!template.category) { return { type: ErrorType.MISSING_FIELD, severity: 'critical', field: 'category', message: 'Template category is required' }; } return null; } }); // Version validation this.builtinRules.push({ name: 'valid_version', description: 'Check for valid semantic version', check: (template) => { if (!semver.valid(template.version)) { return { type: ErrorType.INVALID_VALUE, severity: 'error', field: 'version', message: `Invalid version format: ${template.version}. Must be valid semver (e.g., 1.0.0)` }; } return null; } }); // Category validation this.builtinRules.push({ name: 'valid_category', description: 'Check for valid template category', check: (template) => { const validCategories = Object.values(template_engine_1.TemplateCategory); if (!validCategories.includes(template.category)) { return { type: ErrorType.INVALID_VALUE, severity: 'error', field: 'category', message: `Invalid category: ${template.category}. Must be one of: ${validCategories.join(', ')}` }; } if (this.config.allowedCategories && !this.config.allowedCategories.includes(template.category)) { return { type: ErrorType.INVALID_VALUE, severity: 'error', field: 'category', message: `Category '${template.category}' is not allowed in this context` }; } return null; } }); // Naming convention this.builtinRules.push({ name: 'naming_convention', description: 'Check naming conventions', check: (template) => { if (!/^[a-z0-9-]+$/.test(template.id)) { return { type: WarningType.NAMING_CONVENTION, field: 'id', message: 'Template ID should use lowercase letters, numbers, and hyphens only', suggestion: template.id.toLowerCase().replace(/[^a-z0-9]+/g, '-') }; } return null; } }); // Documentation check this.builtinRules.push({ name: 'documentation', description: 'Check for proper documentation', check: (template) => { if (!template.description || template.description.length < 20) { return { type: WarningType.DOCUMENTATION, field: 'description', message: 'Template should have a detailed description (at least 20 characters)', suggestion: 'Add a comprehensive description explaining the template purpose and usage' }; } return null; } }); } async validate(template) { const errors = []; const warnings = []; const suggestions = []; this.emit('validation:start', template); try { // Run built-in rules for (const rule of this.builtinRules) { const result = rule.check(template); if (result) { if ('severity' in result) { errors.push(result); } else { warnings.push(result); } } } // Run custom rules if (this.config.customRules) { for (const rule of this.config.customRules) { const result = rule.check(template); if (result) { if ('severity' in result) { errors.push(result); } else { warnings.push(result); } } } } // Validate structure await this.validateStructure(template, errors, warnings); // Validate files await this.validateFiles(template, errors, warnings); // Validate variables this.validateVariables(template, errors, warnings); // Validate hooks this.validateHooks(template, errors, warnings); // Check security if (this.config.checkSecurity) { await this.validateSecurity(template, errors, warnings); } // Check performance if (this.config.checkPerformance) { this.validatePerformance(template, errors, warnings); } // Check best practices if (this.config.checkBestPractices) { this.validateBestPractices(template, errors, warnings, suggestions); } // Check compatibility const compatibility = await this.checkCompatibility(template); // Calculate score const score = this.calculateScore(errors, warnings, compatibility); const result = { valid: errors.length === 0 || errors.every(e => e.severity !== 'critical'), errors, warnings, compatibility, suggestions, score }; this.emit('validation:complete', { template, result }); return result; } catch (error) { this.emit('validation:error', { template, error }); throw error; } } async validateStructure(template, errors, warnings) { // Check for circular dependencies in extends if (template.extends) { const visited = new Set(); const checkCircular = (id, chain) => { if (visited.has(id)) { return true; } visited.add(id); // Note: This would need access to other templates to fully check return false; }; if (checkCircular(template.id, [])) { errors.push({ type: ErrorType.CIRCULAR_DEPENDENCY, severity: 'critical', field: 'extends', message: 'Circular dependency detected in template inheritance' }); } } // Check template complexity const complexity = this.calculateComplexity(template); if (this.config.maxComplexity && complexity > this.config.maxComplexity) { warnings.push({ type: WarningType.PERFORMANCE_ISSUE, field: 'template', message: `Template complexity (${complexity}) exceeds recommended maximum (${this.config.maxComplexity})`, suggestion: 'Consider breaking the template into smaller, more focused templates' }); } // Validate metadata if (!template.metadata?.created) { warnings.push({ type: WarningType.MISSING_OPTIONAL, field: 'metadata.created', message: 'Template should include creation date' }); } // Check for required tags if (!template.tags || template.tags.length === 0) { warnings.push({ type: WarningType.BEST_PRACTICE, field: 'tags', message: 'Template should include tags for better discoverability' }); } } async validateFiles(template, errors, warnings) { if (!template.files || template.files.length === 0) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'files', message: 'Template must include at least one file' }); return; } const destinations = new Set(); for (const file of template.files) { // Check required fields if (!file.source) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'file.source', message: 'File source is required' }); } if (!file.destination) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'file.destination', message: 'File destination is required' }); } // Check for duplicate destinations if (destinations.has(file.destination)) { errors.push({ type: ErrorType.INVALID_VALUE, severity: 'error', field: 'file.destination', message: `Duplicate destination: ${file.destination}` }); } destinations.add(file.destination); // Check if source file exists (if absolute path) if (file.source && path.isAbsolute(file.source)) { if (!await fs.pathExists(file.source)) { errors.push({ type: ErrorType.FILE_NOT_FOUND, severity: 'error', field: 'file.source', message: `Source file not found: ${file.source}` }); } } // Validate transform type if (file.transform && !['handlebars', 'ejs', 'none'].includes(file.transform)) { errors.push({ type: ErrorType.INVALID_VALUE, severity: 'error', field: 'file.transform', message: `Invalid transform type: ${file.transform}` }); } // Validate merge strategy if (file.mergeStrategy && !['override', 'append', 'prepend', 'deep', 'custom'].includes(file.mergeStrategy)) { errors.push({ type: ErrorType.INVALID_VALUE, severity: 'error', field: 'file.mergeStrategy', message: `Invalid merge strategy: ${file.mergeStrategy}` }); } // Check for security issues in paths if (this.config.checkSecurity) { if (file.destination.includes('..')) { errors.push({ type: ErrorType.SECURITY_ISSUE, severity: 'critical', field: 'file.destination', message: `Path traversal detected in destination: ${file.destination}` }); } } } } validateVariables(template, errors, warnings) { const variableNames = new Set(); for (const variable of template.variables || []) { // Check required fields if (!variable.name) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'variable.name', message: 'Variable name is required' }); continue; } if (!variable.type) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'variable.type', message: `Variable type is required for '${variable.name}'` }); } // Check for duplicates if (variableNames.has(variable.name)) { errors.push({ type: ErrorType.INVALID_VALUE, severity: 'error', field: 'variable.name', message: `Duplicate variable name: ${variable.name}` }); } variableNames.add(variable.name); // Validate type const validTypes = ['string', 'number', 'boolean', 'array', 'object', 'choice']; if (variable.type && !validTypes.includes(variable.type)) { errors.push({ type: ErrorType.INVALID_TYPE, severity: 'error', field: 'variable.type', message: `Invalid variable type '${variable.type}' for '${variable.name}'` }); } // Validate choices for choice type if (variable.type === 'choice' && (!variable.choices || variable.choices.length === 0)) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'variable.choices', message: `Choices are required for choice type variable '${variable.name}'` }); } // Validate pattern if (variable.pattern) { try { new RegExp(variable.pattern); } catch { errors.push({ type: ErrorType.INVALID_VALUE, severity: 'error', field: 'variable.pattern', message: `Invalid regex pattern for variable '${variable.name}'` }); } } // Check for documentation if (!variable.description) { warnings.push({ type: WarningType.DOCUMENTATION, field: 'variable.description', message: `Variable '${variable.name}' should have a description` }); } // Validate transform and validate functions if (variable.transform) { try { new Function('value', variable.transform); } catch (error) { errors.push({ type: ErrorType.SYNTAX_ERROR, severity: 'error', field: 'variable.transform', message: `Invalid transform function for '${variable.name}': ${error.message}` }); } } if (variable.validate) { try { new Function('value', variable.validate); } catch (error) { errors.push({ type: ErrorType.SYNTAX_ERROR, severity: 'error', field: 'variable.validate', message: `Invalid validate function for '${variable.name}': ${error.message}` }); } } } } validateHooks(template, errors, warnings) { for (const hook of template.hooks || []) { // Check required fields if (!hook.type) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'hook.type', message: 'Hook type is required' }); } if (!hook.name) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'hook.name', message: 'Hook name is required' }); } // Must have either command or script if (!hook.command && !hook.script) { errors.push({ type: ErrorType.MISSING_FIELD, severity: 'error', field: 'hook.command|script', message: `Hook '${hook.name}' must have either command or script` }); } // Validate script syntax if (hook.script) { try { new Function('context', 'require', hook.script); } catch (error) { errors.push({ type: ErrorType.SYNTAX_ERROR, severity: 'error', field: 'hook.script', message: `Invalid script for hook '${hook.name}': ${error.message}` }); } } // Check for security issues if (this.config.checkSecurity && hook.command) { for (const pattern of this.securityPatterns.dangerousCommands) { if (pattern.test(hook.command)) { errors.push({ type: ErrorType.SECURITY_ISSUE, severity: 'critical', field: 'hook.command', message: `Potentially dangerous command in hook '${hook.name}'`, details: { command: hook.command } }); } } } // Validate timeout if (hook.timeout && (hook.timeout < 0 || hook.timeout > 600000)) { warnings.push({ type: WarningType.BEST_PRACTICE, field: 'hook.timeout', message: `Hook timeout should be between 0 and 600000ms (10 minutes)` }); } } } async validateSecurity(template, errors, warnings) { // Check author trust if (template.author && this.config.trustedAuthors && !this.config.trustedAuthors.includes(template.author)) { warnings.push({ type: WarningType.BEST_PRACTICE, field: 'author', message: `Template author '${template.author}' is not in trusted authors list` }); } // Check for sensitive file patterns for (const file of template.files || []) { for (const pattern of this.securityPatterns.sensitiveFiles) { if (pattern.test(file.destination)) { warnings.push({ type: WarningType.BEST_PRACTICE, field: 'file.destination', message: `File destination may contain sensitive data: ${file.destination}` }); } } } // Check variable names for sensitive data for (const variable of template.variables || []) { if (/password|secret|token|key/i.test(variable.name)) { warnings.push({ type: WarningType.BEST_PRACTICE, field: 'variable.name', message: `Variable '${variable.name}' may contain sensitive data. Ensure proper handling.` }); } } } validatePerformance(template, errors, warnings) { // Check file count if (template.files.length > 100) { warnings.push({ type: WarningType.PERFORMANCE_ISSUE, field: 'files', message: `Template has ${template.files.length} files. Consider splitting into smaller templates.` }); } // Check hook count if (template.hooks.length > 20) { warnings.push({ type: WarningType.PERFORMANCE_ISSUE, field: 'hooks', message: `Template has ${template.hooks.length} hooks. This may impact processing time.` }); } // Check variable count if (template.variables.length > 50) { warnings.push({ type: WarningType.PERFORMANCE_ISSUE, field: 'variables', message: `Template has ${template.variables.length} variables. Consider grouping related variables.` }); } } validateBestPractices(template, errors, warnings, suggestions) { // Check for README const hasReadme = template.files.some(f => /readme\.(md|txt)/i.test(path.basename(f.destination))); if (!hasReadme) { suggestions.push('Consider adding a README file to document the generated project'); } // Check for .gitignore const hasGitignore = template.files.some(f => path.basename(f.destination) === '.gitignore'); if (!hasGitignore && template.category !== template_engine_1.TemplateCategory.CONFIGURATION) { suggestions.push('Consider adding a .gitignore file'); } // Check for license if (!template.license) { warnings.push({ type: WarningType.DOCUMENTATION, field: 'license', message: 'Template should specify a license' }); } // Check for repository if (!template.repository) { warnings.push({ type: WarningType.DOCUMENTATION, field: 'repository', message: 'Template should include repository URL for updates and issues' }); } // Check for semantic variable names for (const variable of template.variables || []) { if (variable.name.length < 3) { warnings.push({ type: WarningType.NAMING_CONVENTION, field: 'variable.name', message: `Variable name '${variable.name}' is too short. Use descriptive names.` }); } } } async checkCompatibility(template) { const nodeVersion = await this.checkNodeCompatibility(template); const cliVersion = await this.checkCliCompatibility(template); const dependencies = await this.checkDependencyCompatibility(template); const platforms = await this.checkPlatformCompatibility(template); const frameworks = await this.checkFrameworkCompatibility(template); const scores = [ nodeVersion.compatible ? 100 : 0, cliVersion.compatible ? 100 : 0, ...dependencies.map(d => d.compatible ? 100 : 50), ...platforms.map(p => p.compatible ? 100 : 0), ...frameworks.map(f => f.compatible ? 100 : 50) ]; const overallScore = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100; return { nodeVersion, cliVersion, dependencies, platforms, frameworks, overallScore }; } async checkNodeCompatibility(template) { const required = template.requires?.find(r => r.type === 'environment' && r.name === 'node')?.version || '>=14.0.0'; const current = process.version; return { name: 'Node.js', required, current, compatible: semver.satisfies(current, required), recommendation: semver.satisfies(current, required) ? undefined : `Requires Node.js ${required}, you have ${current}` }; } async checkCliCompatibility(template) { const required = template.requires?.find(r => r.type === 'environment' && r.name === 'cli')?.version || '*'; // Get CLI version let current = 'unknown'; try { const packagePath = path.join(__dirname, '..', '..', 'package.json'); const pkg = await fs.readJson(packagePath); current = pkg.version; } catch { // Ignore } return { name: 'Re-Shell CLI', required, current, compatible: required === '*' || semver.satisfies(current, required), recommendation: undefined }; } async checkDependencyCompatibility(template) { const checks = []; const packageDeps = template.requires?.filter(r => r.type === 'package') || []; for (const dep of packageDeps) { checks.push({ name: dep.name, required: dep.version || '*', compatible: true, // TODO: Actually check if package is available recommendation: undefined }); } return checks; } async checkPlatformCompatibility(template) { const platforms = [ { platform: 'windows', compatible: true, issues: [] }, { platform: 'darwin', compatible: true, issues: [] }, { platform: 'linux', compatible: true, issues: [] } ]; // Check for platform-specific issues for (const file of template.files || []) { // Check for Windows path issues if (file.destination.includes(':') || file.destination.includes('\\')) { platforms[0].issues.push(`Path may have issues on Windows: ${file.destination}`); } // Check for case sensitivity issues if (file.destination !== file.destination.toLowerCase()) { platforms[0].issues.push(`Case-sensitive path may cause issues on Windows: ${file.destination}`); } // Check for permissions (Windows doesn't support chmod) if (file.permissions) { platforms[0].issues.push(`File permissions not supported on Windows: ${file.destination}`); } } // Check hooks for platform-specific commands for (const hook of template.hooks || []) { if (hook.command) { if (hook.command.includes('chmod') || hook.command.includes('chown')) { platforms[0].issues.push(`Unix-specific command in hook: ${hook.name}`); } if (hook.command.includes('.sh')) { platforms[0].issues.push(`Shell script may not work on Windows: ${hook.name}`); } } } // Update compatibility based on issues for (const platform of platforms) { platform.compatible = platform.issues.length === 0; } return platforms; } async checkFrameworkCompatibility(template) { const frameworks = []; // Check based on template category and tags if (template.category === template_engine_1.TemplateCategory.MICROFRONTEND || template.tags.includes('react')) { frameworks.push({ framework: 'react', version: '>=16.8.0', compatible: true, issues: [] }); } if (template.tags.includes('vue')) { frameworks.push({ framework: 'vue', version: '>=3.0.0', compatible: true, issues: [] }); } if (template.tags.includes('angular')) { frameworks.push({ framework: 'angular', version: '>=12.0.0', compatible: true, issues: [] }); } return frameworks; } calculateComplexity(template) { let complexity = 0; // Base complexity complexity += template.files.length * 2; complexity += template.variables.length; complexity += template.hooks.length * 3; // Inheritance complexity complexity += (template.extends?.length || 0) * 5; complexity += (template.implements?.length || 0) * 3; // Conditional complexity for (const file of template.files) { if (file.condition) complexity += 2; if (file.merge) complexity += 3; } for (const variable of template.variables) { if (variable.when) complexity += 2; if (variable.validate) complexity += 2; if (variable.transform) complexity += 2; } for (const hook of template.hooks) { if (hook.condition) complexity += 2; if (hook.script) complexity += 5; // Scripts are more complex than commands } return complexity; } calculateScore(errors, warnings, compatibility) { let score = 100; // Deduct for errors for (const error of errors) { if (error.severity === 'critical') { score -= 20; } else { score -= 10; } } // Deduct for warnings score -= warnings.length * 2; // Factor in compatibility score = Math.round((score + compatibility.overallScore) / 2); return Math.max(0, Math.min(100, score)); } // Utility methods async validateTemplateFile(filePath) { try { const content = await fs.readFile(filePath, 'utf8'); const template = yaml.load(content); return await this.validate(template); } catch (error) { return { valid: false, errors: [{ type: ErrorType.SYNTAX_ERROR, severity: 'critical', field: 'file', message: `Failed to parse template file: ${error.message}` }], warnings: [], compatibility: { nodeVersion: { name: 'Node.js', required: '*', compatible: true }, cliVersion: { name: 'CLI', required: '*', compatible: true }, dependencies: [], platforms: [], frameworks: [], overallScore: 0 }, suggestions: [], score: 0 }; } } updateConfig(config) { this.config = { ...this.config, ...config }; } addCustomRule(rule) { if (!this.config.customRules) { this.config.customRules = []; } this.config.customRules.push(rule); } removeCustomRule(name) { if (!this.config.customRules) return false; const index = this.config.customRules.findIndex(r => r.name === name); if (index >= 0) { this.config.customRules.splice(index, 1); return true; } return false; } } exports.TemplateValidator = TemplateValidator; // Global template validator let globalTemplateValidator = null; function createTemplateValidator(config) { return new TemplateValidator(config); } function getGlobalTemplateValidator() { if (!globalTemplateValidator) { globalTemplateValidator = new TemplateValidator(); } return globalTemplateValidator; } function setGlobalTemplateValidator(validator) { globalTemplateValidator = validator; }