#!/usr/bin/env node /** * ZCLAW Configuration Validator * * Validates configuration files and environment setup. * Run with: npx ts-node scripts/validate-config.ts */ import * as fs from 'fs'; import * as path from 'path'; // Types interface ValidationResult { file: string; valid: boolean; errors: string[]; warnings: string[]; } interface ConfigValidationSummary { timestamp: string; totalFiles: number; validFiles: number; invalidFiles: number; totalErrors: number; totalWarnings: number; results: ValidationResult[]; } // Color output helpers const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', gray: '\x1b[90m', }; function log(color: keyof typeof colors, message: string): void { console.log(`${colors[color]}${message}${colors.reset}`); } // Validators function validateTomlFile(filePath: string): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!fs.existsSync(filePath)) { return { file: filePath, valid: false, errors: [`File not found: ${filePath}`], warnings: [] }; } const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); // Basic TOML validation let currentSection = ''; const definedKeys = new Set(); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); const lineNum = i + 1; // Skip empty lines and comments if (!line || line.startsWith('#')) continue; // Section header const sectionMatch = line.match(/^\[([^\]]+)\]$/); if (sectionMatch) { currentSection = sectionMatch[1]; continue; } // Key-value pair const kvMatch = line.match(/^([^=]+)=(.*)$/); if (kvMatch) { const key = kvMatch[1].trim(); const value = kvMatch[2].trim(); const fullKey = currentSection ? `${currentSection}.${key}` : key; // Check for duplicate keys if (definedKeys.has(fullKey)) { warnings.push(`Line ${lineNum}: Duplicate key "${fullKey}"`); } definedKeys.add(fullKey); // Check for empty values if (!value || value === '""' || value === "''") { warnings.push(`Line ${lineNum}: Empty value for "${fullKey}"`); } // Check for unquoted strings that might need quoting if (!value.startsWith('"') && !value.startsWith("'") && !value.startsWith('[') && !value.startsWith('{') && !/^(true|false|\d+|\d+\.\d+)$/.test(value)) { if (value.includes(' ') || value.includes('#')) { errors.push(`Line ${lineNum}: Value "${value}" should be quoted`); } } // Check for environment variable references if (value.includes('${') && !value.includes('}')) { errors.push(`Line ${lineNum}: Unclosed environment variable reference`); } } else if (line && !line.startsWith('#')) { // Invalid line errors.push(`Line ${lineNum}: Invalid TOML syntax: "${line}"`); } } return { file: filePath, valid: errors.length === 0, errors, warnings, }; } function validateMainConfig(): ValidationResult { const configPath = path.join(process.cwd(), 'config/config.toml'); const result = validateTomlFile(configPath); if (result.errors.length === 0 && fs.existsSync(configPath)) { const content = fs.readFileSync(configPath, 'utf-8'); // Check for required sections const requiredSections = ['gateway', 'agent', 'models']; for (const section of requiredSections) { if (!content.includes(`[${section}]`)) { result.warnings.push(`Missing recommended section: [${section}]`); } } // Check for required keys const requiredKeys = ['gateway.url', 'agent.default_model']; for (const key of requiredKeys) { if (!content.includes(key.split('.').pop()!)) { result.warnings.push(`Missing recommended key: ${key}`); } } } return result; } function validateChineseProviders(): ValidationResult { const configPath = path.join(process.cwd(), 'config/chinese-providers.toml'); const result = validateTomlFile(configPath); if (result.errors.length === 0 && fs.existsSync(configPath)) { const content = fs.readFileSync(configPath, 'utf-8'); // Check for Chinese model providers const providers = ['glm', 'qwen', 'kimi', 'minimax', 'deepseek']; for (const provider of providers) { if (!content.includes(`[${provider}`) && !content.includes(`[${provider}]`)) { result.warnings.push(`Missing Chinese model provider: ${provider}`); } } } return result; } function validatePluginConfigs(): ValidationResult[] { const results: ValidationResult[] = []; const pluginsDir = path.join(process.cwd(), 'plugins'); if (!fs.existsSync(pluginsDir)) { return [{ file: 'plugins/', valid: true, errors: [], warnings: ['No plugins directory found'] }]; } const plugins = fs.readdirSync(pluginsDir).filter(f => fs.statSync(path.join(pluginsDir, f)).isDirectory() ); for (const plugin of plugins) { const pluginJsonPath = path.join(pluginsDir, plugin, 'plugin.json'); if (fs.existsSync(pluginJsonPath)) { const result: ValidationResult = { file: pluginJsonPath, valid: true, errors: [], warnings: [], }; try { const content = fs.readFileSync(pluginJsonPath, 'utf-8'); const config = JSON.parse(content); // Check required fields if (!config.name) result.errors.push('Missing required field: name'); if (!config.version) result.warnings.push('Missing recommended field: version'); if (!config.description) result.warnings.push('Missing recommended field: description'); result.valid = result.errors.length === 0; } catch (e) { result.valid = false; result.errors.push(`Invalid JSON: ${(e as Error).message}`); } results.push(result); } } return results; } function validateEnvironment(): ValidationResult { const result: ValidationResult = { file: 'environment', valid: true, errors: [], warnings: [], }; // Check Node.js version const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10); if (majorVersion < 18) { result.warnings.push(`Node.js version ${nodeVersion} is below recommended 18.x`); } // Check for .env file const envPath = path.join(process.cwd(), '.env'); if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf-8'); // Check for sensitive patterns const sensitivePatterns = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN']; for (const pattern of sensitivePatterns) { const regex = new RegExp(`${pattern}\\s*=\\s*[^\\s]+`, 'g'); const matches = envContent.match(regex); if (matches) { for (const match of matches) { // Check if the value is not a placeholder const value = match.split('=')[1].trim(); if (!value.includes('your_') && !value.includes('xxx') && value.length > 8) { result.warnings.push(`Potential exposed secret in .env: ${pattern}`); } } } } } return result; } // Main validation async function main(): Promise { log('blue', '\n=== ZCLAW Configuration Validator ===\n'); const results: ValidationResult[] = []; // Run all validators log('gray', 'Validating main configuration...'); results.push(validateMainConfig()); log('gray', 'Validating Chinese providers configuration...'); results.push(validateChineseProviders()); log('gray', 'Validating plugin configurations...'); results.push(...validatePluginConfigs()); log('gray', 'Validating environment...'); results.push(validateEnvironment()); // Print results console.log('\n'); for (const result of results) { const status = result.valid ? '✓' : '✗'; const statusColor = result.valid ? 'green' : 'red'; log(statusColor, `${status} ${result.file}`); for (const error of result.errors) { log('red', ` ERROR: ${error}`); } for (const warning of result.warnings) { log('yellow', ` WARN: ${warning}`); } } // Summary const summary: ConfigValidationSummary = { timestamp: new Date().toISOString(), totalFiles: results.length, validFiles: results.filter(r => r.valid).length, invalidFiles: results.filter(r => !r.valid).length, totalErrors: results.reduce((sum, r) => sum + r.errors.length, 0), totalWarnings: results.reduce((sum, r) => sum + r.warnings.length, 0), results: results, }; console.log('\n'); log('blue', '=== Summary ==='); console.log(` Files checked: ${summary.totalFiles}`); console.log(` Valid: ${colors.green}${summary.validFiles}${colors.reset}`); console.log(` Invalid: ${colors.red}${summary.invalidFiles}${colors.reset}`); console.log(` Errors: ${summary.totalErrors}`); console.log(` Warnings: ${summary.totalWarnings}`); // Write JSON report const reportPath = path.join(process.cwd(), 'config-validation-report.json'); fs.writeFileSync(reportPath, JSON.stringify(summary, null, 2)); log('gray', `\nReport saved to: ${reportPath}`); // Exit with appropriate code process.exit(summary.invalidFiles > 0 ? 1 : 0); } main().catch(console.error);