@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
302 lines (258 loc) • 8.15 kB
text/typescript
import fs from "fs-extra";
import path from "path";
import { glob } from "glob";
export interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
export interface FileValidationOptions {
maxFileSize?: number; // in bytes
allowedExtensions?: string[];
minFiles?: number;
maxFiles?: number;
}
export interface Config {
apiKey?: string;
apiUrl?: string;
[key: string]: any;
}
// Add missing validateApiConnection function
export async function validateApiConnection(config: Config): Promise<boolean> {
if (!config.apiKey) {
throw new Error("API key not configured");
}
if (!config.apiUrl) {
throw new Error("API URL not configured");
}
try {
const response = await fetch(`${config.apiUrl}/api/health`, {
method: "GET",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(10000), // 10 second timeout
});
if (!response.ok) {
throw new Error(
`API connection failed: ${response.status} ${response.statusText}`,
);
}
return true;
} catch (error) {
if (error instanceof Error) {
if (error.name === "TimeoutError") {
throw new Error(
"API connection timeout - check your network connection",
);
}
throw new Error(`API connection failed: ${error.message}`);
}
throw new Error("Unknown API connection error");
}
}
export async function validateFiles(
patterns: string[],
options: FileValidationOptions = {},
): Promise<ValidationResult & { files: string[] }> {
const {
maxFileSize = 10 * 1024 * 1024, // 10MB default
allowedExtensions = [".ts", ".tsx", ".js", ".jsx"],
minFiles = 0,
maxFiles = 1000,
} = options;
const errors: string[] = [];
const warnings: string[] = [];
const files: string[] = [];
try {
// Resolve file patterns
for (const pattern of patterns) {
const matches = await glob(pattern, {
absolute: true,
cwd: process.cwd(),
ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
});
files.push(...matches);
}
// Remove duplicates
const uniqueFiles = [...new Set(files)];
// Check file count
if (uniqueFiles.length < minFiles) {
errors.push(
`Minimum ${minFiles} files required, found ${uniqueFiles.length}`,
);
}
if (uniqueFiles.length > maxFiles) {
errors.push(
`Maximum ${maxFiles} files allowed, found ${uniqueFiles.length}`,
);
}
// Validate each file
for (const filePath of uniqueFiles.slice(0, maxFiles)) {
const fileErrors = await validateSingleFile(filePath, {
maxFileSize,
allowedExtensions,
});
errors.push(...fileErrors.errors);
warnings.push(...fileErrors.warnings);
}
return {
valid: errors.length === 0,
errors,
warnings,
files: uniqueFiles.slice(0, maxFiles),
};
} catch (error) {
return {
valid: false,
errors: [
`File validation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
],
warnings: [],
files: [],
};
}
}
export async function validateSingleFile(
filePath: string,
options: { maxFileSize?: number; allowedExtensions?: string[] } = {},
): Promise<ValidationResult> {
const {
maxFileSize = 10 * 1024 * 1024,
allowedExtensions = [".ts", ".tsx", ".js", ".jsx"],
} = options;
const errors: string[] = [];
const warnings: string[] = [];
try {
// Check if file exists
if (!(await fs.pathExists(filePath))) {
errors.push(`File does not exist: ${filePath}`);
return { valid: false, errors, warnings };
}
// Check file extension
const ext = path.extname(filePath).toLowerCase();
if (!allowedExtensions.includes(ext)) {
warnings.push(`Unsupported file extension: ${ext} (${filePath})`);
}
// Check file size
const stats = await fs.stat(filePath);
if (stats.size > maxFileSize) {
errors.push(
`File too large: ${filePath} (${formatBytes(stats.size)} > ${formatBytes(maxFileSize)})`,
);
}
// Check if file is readable
try {
await fs.access(filePath, fs.constants.R_OK);
} catch {
errors.push(`File is not readable: ${filePath}`);
}
// Basic content validation
if (stats.size > 0) {
try {
const content = await fs.readFile(filePath, "utf-8");
// Check for binary content
if (containsBinaryContent(content)) {
warnings.push(`File appears to contain binary content: ${filePath}`);
}
// Check for extremely long lines (potential minified files)
const lines = content.split("\n");
const longLines = lines.filter((line) => line.length > 1000);
if (longLines.length > 0) {
warnings.push(
`File contains very long lines (possible minified): ${filePath}`,
);
}
} catch (error) {
if (error instanceof Error && error.message.includes("EMFILE")) {
warnings.push(
`Too many open files, skipping content validation for: ${filePath}`,
);
} else {
errors.push(`Cannot read file content: ${filePath}`);
}
}
}
} catch (error) {
errors.push(
`File validation error: ${filePath} - ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
return { valid: errors.length === 0, errors, warnings };
}
export function validateLayerNumbers(
layers: string | number[],
): ValidationResult {
const errors: string[] = [];
let layerArray: number[];
if (typeof layers === "string") {
layerArray = layers.split(",").map((l) => parseInt(l.trim(), 10));
} else {
layerArray = layers;
}
// Check for invalid 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");
}
return { valid: errors.length === 0, errors, warnings: [] };
}
export function validateOutputFormat(format: string): ValidationResult {
const validFormats = ["table", "json", "summary", "html", "detailed-json", "compact-json"];
const errors: string[] = [];
if (!validFormats.includes(format)) {
errors.push(
`Invalid output format: ${format}. Must be one of: ${validFormats.join(", ")}`,
);
}
return { valid: errors.length === 0, errors, warnings: [] };
}
export function validateApiUrl(url: string): ValidationResult {
const errors: string[] = [];
try {
const parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) {
errors.push("API URL must use http or https protocol");
}
if (!parsed.hostname) {
errors.push("API URL must have a valid hostname");
}
} catch {
errors.push("Invalid API URL format");
}
return { valid: errors.length === 0, errors, warnings: [] };
}
function containsBinaryContent(content: string): boolean {
// Check for null bytes or other binary indicators
return (
content.includes("\0") ||
/[\x00-\x08\x0E-\x1F\x7F]/.test(content.substring(0, 1000))
);
}
function formatBytes(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
export function createFileValidator(options: FileValidationOptions = {}) {
return {
async validateFiles(patterns: string[]) {
return validateFiles(patterns, options);
},
async validateSingleFile(filePath: string) {
return validateSingleFile(filePath, options);
},
};
}