- Add test helper library with assertion functions (scripts/lib/test-helpers.sh) - Add gateway integration test script (scripts/tests/gateway-test.sh) - Add configuration validation tool (scripts/validate-config.ts) - Add health-check.ts library with Tauri command wrappers - Add HealthStatusIndicator component to ConnectionStatus.tsx - Add E2E test specs for memory, settings, and team collaboration - Update ZCLAW-DEEP-ANALYSIS.md to reflect actual project state Key improvements: - Store architecture now properly documented as migrated - Tauri backend shown as 85-90% complete - Component integration status clarified Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
311 lines
9.1 KiB
JavaScript
311 lines
9.1 KiB
JavaScript
#!/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<string>();
|
|
|
|
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<void> {
|
|
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);
|