UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

320 lines (280 loc) 9.48 kB
import fs from "fs-extra"; import path from "path"; import chalk from "chalk"; export interface CLIValidationResult { valid: boolean; errors: string[]; warnings: string[]; } /** * Comprehensive CLI input validation utilities * Ensures robustness and security of CLI operations */ export class CLIValidator { /** * Validate file paths for security and existence */ static async validateFilePaths( files: string[], ): Promise<CLIValidationResult> { const errors: string[] = []; const warnings: string[] = []; if (!Array.isArray(files)) { errors.push("Files parameter must be an array"); return { valid: false, errors, warnings }; } for (const file of files) { // Check basic string validation if (!file || typeof file !== "string" || file.trim().length === 0) { errors.push("All file paths must be valid non-empty strings"); continue; } // Security checks for path traversal const normalizedPath = path.normalize(file); if ( normalizedPath.includes("..") || normalizedPath.startsWith("/etc") || normalizedPath.startsWith("/proc") ) { errors.push(`Potentially unsafe file path: ${file}`); continue; } // Check if path is too long (OS limits) if (file.length > 4096) { errors.push(`File path too long: ${file.substring(0, 50)}...`); continue; } // Check for suspicious characters if (/[\x00-\x1f\x7f-\x9f]/.test(file)) { errors.push(`File path contains invalid characters: ${file}`); continue; } // Warning for very deep paths const depth = file.split(path.sep).length; if (depth > 20) { warnings.push(`Very deep file path (${depth} levels): ${file}`); } // Check if file exists and is accessible try { if (await fs.pathExists(file)) { const stats = await fs.stat(file); if (!stats.isFile()) { warnings.push(`Path is not a file: ${file}`); } } else { warnings.push(`File does not exist: ${file}`); } } catch (error) { warnings.push(`Cannot access file: ${file}`); } } return { valid: errors.length === 0, errors, warnings }; } /** * Validate layer numbers and dependencies */ static validateLayers(layers: string | number[]): CLIValidationResult { const errors: string[] = []; const warnings: string[] = []; let layerArray: number[]; try { if (typeof layers === "string") { if (layers.trim().length === 0) { errors.push("Layer string cannot be empty"); return { valid: false, errors, warnings }; } layerArray = layers.split(",").map((l) => { const num = parseInt(l.trim(), 10); if (isNaN(num)) { throw new Error(`Invalid layer number: ${l.trim()}`); } return num; }); } else if (Array.isArray(layers)) { layerArray = layers; } else { errors.push("Layers must be a string or array of numbers"); return { valid: false, errors, warnings }; } // Check for valid layer numbers const invalidLayers = layerArray.filter( (layer) => !Number.isInteger(layer) || layer < 1 || layer > 6, ); if (invalidLayers.length > 0) { errors.push( `Invalid layer numbers: ${invalidLayers.join(", ")}. Must be integers between 1-6.`, ); } // Check for duplicates const uniqueLayers = [...new Set(layerArray)]; if (uniqueLayers.length !== layerArray.length) { errors.push("Duplicate layer numbers are not allowed"); } // Check layer dependencies const dependencies = { 1: [], 2: [1], 3: [1, 2], 4: [1, 2, 3], 5: [1, 2, 3, 4], 6: [1, 2, 3, 4, 5], }; for (const layer of uniqueLayers) { const required = dependencies[layer as keyof typeof dependencies] || []; const missing = required.filter((dep) => !uniqueLayers.includes(dep)); if (missing.length > 0) { warnings.push( `Layer ${layer} requires layers ${missing.join(", ")}. Consider adding them.`, ); } } return { valid: errors.length === 0, errors, warnings }; } catch (error) { errors.push( `Layer validation error: ${error instanceof Error ? error.message : "Unknown error"}`, ); return { valid: false, errors, warnings }; } } /** * Validate CLI options object */ static validateOptions(options: any): CLIValidationResult { const errors: string[] = []; const warnings: string[] = []; if (options === null) { errors.push("Options cannot be null"); return { valid: false, errors, warnings }; } if (options !== undefined && typeof options !== "object") { errors.push("Options must be an object"); return { valid: false, errors, warnings }; } if (options) { // Validate output format if ( options.output && !["table", "json", "summary"].includes(options.output) ) { errors.push( `Invalid output format: ${options.output}. Must be table, json, or summary.`, ); } // Validate boolean options const booleanOptions = ["recursive", "dryRun", "backup", "verbose"]; for (const opt of booleanOptions) { if (options[opt] !== undefined && typeof options[opt] !== "boolean") { errors.push(`Option ${opt} must be a boolean`); } } // Validate config path if provided if (options.config) { if (typeof options.config !== "string") { errors.push("Config path must be a string"); } else if (options.config.length > 4096) { errors.push("Config path too long"); } } } return { valid: errors.length === 0, errors, warnings }; } /** * Validate API configuration */ static validateApiConfig(config: any): CLIValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!config) { errors.push("Configuration is required"); return { valid: false, errors, warnings }; } // Validate API URL if (!config.api?.url) { errors.push("API URL is required"); } else { try { const url = new URL(config.api.url); if (!["http:", "https:"].includes(url.protocol)) { errors.push("API URL must use http or https protocol"); } if (url.hostname === "localhost" || url.hostname === "127.0.0.1") { warnings.push("Using localhost API - ensure the server is running"); } } catch { errors.push("Invalid API URL format"); } } // Validate API key if (!config.apiKey) { warnings.push( "No API key configured - authentication required for most operations", ); } else if (typeof config.apiKey !== "string") { errors.push("API key must be a string"); } else if (config.apiKey.length < 10) { errors.push("API key appears to be too short"); } // Validate timeout if (config.api?.timeout) { if (typeof config.api.timeout !== "number" || config.api.timeout <= 0) { errors.push("API timeout must be a positive number"); } else if (config.api.timeout < 5000) { warnings.push("API timeout is very low (< 5 seconds)"); } else if (config.api.timeout > 300000) { warnings.push("API timeout is very high (> 5 minutes)"); } } return { valid: errors.length === 0, errors, warnings }; } /** * Comprehensive validation runner */ static async runFullValidation( files: string[], options: any, config: any, ): Promise<CLIValidationResult> { const allErrors: string[] = []; const allWarnings: string[] = []; // Validate files const fileValidation = await this.validateFilePaths(files); allErrors.push(...fileValidation.errors); allWarnings.push(...fileValidation.warnings); // Validate options const optionsValidation = this.validateOptions(options); allErrors.push(...optionsValidation.errors); allWarnings.push(...optionsValidation.warnings); // Validate layers if specified if (options?.layers) { const layersValidation = this.validateLayers(options.layers); allErrors.push(...layersValidation.errors); allWarnings.push(...layersValidation.warnings); } // Validate config const configValidation = this.validateApiConfig(config); allErrors.push(...configValidation.errors); allWarnings.push(...configValidation.warnings); return { valid: allErrors.length === 0, errors: allErrors, warnings: allWarnings, }; } /** * Display validation results to user */ static displayValidationResults(validation: CLIValidationResult): void { if (validation.errors.length > 0) { console.log(chalk.red("\nValidation Errors:")); validation.errors.forEach((error) => { console.log(chalk.red(` ERROR: ${error}`)); }); } if (validation.warnings.length > 0) { console.log(chalk.yellow("\nValidation Warnings:")); validation.warnings.forEach((warning) => { console.log(chalk.yellow(` WARNING: ${warning}`)); }); } } }