import { Configuration, ConfigurationBuilder } from './Configuration'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as yaml from 'yaml'; export interface ConfigFile { readonly path: string; readonly format: 'json' | 'yaml' | 'yml'; readonly content: any; } export class ConfigLoader { private static readonly CONFIG_FILE_NAMES = [ 'ai-dev.config.json', 'ai-dev.config.yaml', 'ai-dev.config.yml', '.ai-dev.json', '.ai-dev.yaml', '.ai-dev.yml', 'package.json', ]; private static readonly CONFIG_DIRS = [ process.cwd(), path.join(process.cwd(), '.ai-dev'), path.join(process.cwd(), 'config'), path.join(process.cwd(), '.config'), path.join(process.env.HOME || '', '.ai-dev'), path.join(process.env.HOME || '', '.config', 'ai-dev'), ]; static async loadConfiguration(configPath?: string): Promise { try { // If specific config path is provided, load it if (configPath) { const config = await this.loadConfigFile(configPath); return this.parseConfiguration(config); } // Search for configuration files const configFile = await this.findConfigFile(); if (configFile) { return this.parseConfiguration(configFile); } // Fall back to environment variables and defaults return this.loadFromEnvironment(); } catch (error) { console.warn(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); return this.loadFromEnvironment(); } } static async saveConfiguration(config: Configuration, configPath: string): Promise { try { const format = path.extname(configPath).toLowerCase(); let content: string; if (format === '.json') { content = JSON.stringify(config, null, 2); } else if (format === '.yaml' || format === '.yml') { content = yaml.stringify(config); } else { throw new Error(`Unsupported configuration format: ${format}`); } await fs.writeFile(configPath, content, 'utf-8'); } catch (error) { throw new Error(`Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); } } static async validateConfiguration(config: Configuration): Promise<{ readonly isValid: boolean; readonly errors: string[]; readonly warnings: string[]; }> { const errors: string[] = []; const warnings: string[] = []; // Validate LLM configuration if (!config.llm) { errors.push('LLM configuration is required'); } else { if (!config.llm.provider) { errors.push('LLM provider is required'); } if (config.llm.provider === 'openai' && !config.openai?.apiKey) { errors.push('OpenAI API key is required when using OpenAI provider'); } if (config.llm.provider === 'ollama' && !config.ollama?.enabled) { warnings.push('Ollama is not enabled but selected as LLM provider'); } } // Validate OpenAI configuration if (config.openai && !config.openai.apiKey) { errors.push('OpenAI API key is required'); } // Validate GitHub configuration if (config.github?.token && !config.github.token.startsWith('ghp_') && !config.github.token.startsWith('gho_') && !config.github.token.startsWith('ghu_') && !config.github.token.startsWith('ghs_') && !config.github.token.startsWith('ghr_')) { warnings.push('GitHub token format may be invalid'); } // Validate file patterns if (config.filePatterns && config.filePatterns.length === 0) { warnings.push('No file patterns specified, all files will be processed'); } // Validate output configuration if (config.output.format === 'file' && !config.output.outputPath) { errors.push('Output path is required when using file output format'); } // Validate security configuration if (config.security.enabled && config.security.severity.length === 0) { warnings.push('No security severity levels specified'); } // Validate test configuration if (config.test.enabled && !config.test.framework) { errors.push('Test framework is required when tests are enabled'); } // Validate documentation configuration if (config.documentation.enabled && !config.documentation.format) { errors.push('Documentation format is required when documentation is enabled'); } return { isValid: errors.length === 0, errors, warnings, }; } private static async findConfigFile(): Promise { for (const dir of this.CONFIG_DIRS) { try { const files = await fs.readdir(dir); for (const fileName of this.CONFIG_FILE_NAMES) { if (files.includes(fileName)) { const filePath = path.join(dir, fileName); return await this.loadConfigFile(filePath); } } } catch (error) { // Directory doesn't exist or can't be read, continue to next continue; } } return null; } private static async loadConfigFile(filePath: string): Promise { try { const content = await fs.readFile(filePath, 'utf-8'); const format = path.extname(filePath).toLowerCase(); let parsed: any; if (format === '.json') { parsed = JSON.parse(content); } else if (format === '.yaml' || format === '.yml') { parsed = yaml.parse(content); } else { throw new Error(`Unsupported configuration format: ${format}`); } // Handle package.json case if (path.basename(filePath) === 'package.json') { parsed = parsed['ai-dev'] || parsed.aiDev || {}; } return { path: filePath, format: format === '.json' ? 'json' : 'yaml', content: parsed, }; } catch (error) { throw new Error(`Failed to load config file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } private static substituteEnvironmentVariables(obj: any): any { if (typeof obj === 'string') { return obj.replace(/\$\{([^}]+)\}/g, (match, varName) => { return process.env[varName] || match; }); } else if (Array.isArray(obj)) { return obj.map(item => this.substituteEnvironmentVariables(item)); } else if (obj && typeof obj === 'object') { const result: any = {}; for (const [key, value] of Object.entries(obj)) { result[key] = this.substituteEnvironmentVariables(value); } return result; } return obj; } private static parseConfiguration(configFile: ConfigFile): Configuration { const builder = new ConfigurationBuilder(); const config = this.substituteEnvironmentVariables(configFile.content); // Parse LLM configuration if (config.llm) { builder.llm(config.llm); } // Parse OpenAI configuration if (config.openai) { builder.openai(config.openai); } // Parse Ollama configuration if (config.ollama) { builder.ollama(config.ollama); } // Parse Gemini configuration if (config.gemini) { builder.gemini(config.gemini); } // Parse GitHub configuration if (config.github) { builder.github(config.github); } // Parse Git configuration if (config.git) { builder.git(config.git); } // Parse Output configuration if (config.output) { builder.output(config.output); } // Parse Security configuration if (config.security) { builder.security(config.security); } // Parse Test configuration if (config.test) { builder.test(config.test); } // Parse Documentation configuration if (config.documentation) { builder.documentation(config.documentation); } // Parse Review configuration if (config.review) { builder.review(config.review); } // Parse global configuration if (config.filePatterns) { builder.filePatterns(config.filePatterns); } if (config.excludePatterns) { builder.excludePatterns(config.excludePatterns); } if (config.verbose !== undefined) { builder.verbose(config.verbose); } if (config.debug !== undefined) { builder.debug(config.debug); } return builder.build(); } private static loadFromEnvironment(): Configuration { // This would load from environment variables // For now, return default configuration return new ConfigurationBuilder().build(); } }