UX-agent / backend /config /ConfigManager.js
AUXteam's picture
Deploying UX Analyst AI to Hugging Face (V2)
21cac8a verified
const fs = require('fs').promises;
const path = require('path');
const Joi = require('joi');
const configSchema = require('./schema');
class ConfigManager {
constructor() {
this.config = null;
this.environment = process.env.NODE_ENV || 'development';
this.configCache = new Map();
this.watchers = new Map();
this.validationErrors = [];
this.isInitialized = false;
}
/**
* Initialize the configuration manager
*/
async initialize() {
if (this.isInitialized) {
return this.config;
}
try {
// Load base configuration
const baseConfig = await this.loadBaseConfig();
// Load environment-specific configuration
const envConfig = await this.loadEnvironmentConfig();
// Merge configurations (environment overrides base)
const mergedConfig = this.mergeConfigs(baseConfig, envConfig);
// Load environment variables
const envVarsConfig = this.loadEnvironmentVariables();
// Final merge (env vars override everything)
const finalConfig = this.mergeConfigs(mergedConfig, envVarsConfig);
// Validate configuration
await this.validateConfig(finalConfig);
this.config = finalConfig;
this.isInitialized = true;
console.log(`Configuration loaded successfully for environment: ${this.environment}`);
return this.config;
} catch (error) {
console.error('Failed to initialize configuration:', error);
throw error;
}
}
/**
* Load base configuration
*/
async loadBaseConfig() {
const configPath = path.join(__dirname, 'default.json');
try {
const configFile = await fs.readFile(configPath, 'utf8');
return JSON.parse(configFile);
} catch (error) {
if (error.code === 'ENOENT') {
console.warn('Default configuration file not found, using schema defaults');
return {};
}
throw new Error(`Failed to load base configuration: ${error.message}`);
}
}
/**
* Load environment-specific configuration
*/
async loadEnvironmentConfig() {
const configPath = path.join(__dirname, `${this.environment}.json`);
try {
const configFile = await fs.readFile(configPath, 'utf8');
return JSON.parse(configFile);
} catch (error) {
if (error.code === 'ENOENT') {
console.warn(`Environment configuration file not found: ${configPath}`);
return {};
}
throw new Error(`Failed to load environment configuration: ${error.message}`);
}
}
/**
* Load configuration from environment variables
*/
loadEnvironmentVariables() {
const envConfig = {};
// Map environment variables to configuration structure
const envMappings = {
// Server
'PORT': 'server.port',
'HOST': 'server.host',
'NODE_ENV': 'server.environment',
'CORS_ENABLED': 'server.corsEnabled',
'RATE_LIMIT_ENABLED': 'server.rateLimitEnabled',
'RATE_LIMIT_WINDOW_MS': 'server.rateLimitWindowMs',
'RATE_LIMIT_MAX_REQUESTS': 'server.rateLimitMaxRequests',
// Database
'DATABASE_TYPE': 'database.type',
'DATABASE_PATH': 'database.path',
'DATABASE_HOST': 'database.host',
'DATABASE_PORT': 'database.port',
'DATABASE_NAME': 'database.name',
'DATABASE_USERNAME': 'database.username',
'DATABASE_PASSWORD': 'database.password',
// AI Services
'GEMINI_API_KEY': 'ai.gemini.apiKey',
'GEMINI_MODEL': 'ai.gemini.model',
'GEMINI_MAX_RETRIES': 'ai.gemini.maxRetries',
'GEMINI_TIMEOUT_MS': 'ai.gemini.timeoutMs',
// Analysis
'MAX_CONCURRENT_ANALYSES': 'analysis.maxConcurrentAnalyses',
'ANALYSIS_TIMEOUT_MS': 'analysis.timeoutMs',
'MAX_STUCK_TIME_MS': 'analysis.maxStuckTimeMs',
'CLEANUP_INTERVAL_MS': 'analysis.cleanupIntervalMs',
'ENABLE_ACCESSIBILITY': 'analysis.enableAccessibility',
'ENABLE_VISUAL_ANALYSIS': 'analysis.enableVisualAnalysis',
'ENABLE_AI_CRITIQUE': 'analysis.enableAICritique',
// Screenshots
'SCREENSHOT_STORAGE_PATH': 'screenshots.storagePath',
'SCREENSHOT_TIMEOUT_MS': 'screenshots.timeoutMs',
'SCREENSHOT_WAIT_FOR_MS': 'screenshots.waitForMs',
// Browser
'BROWSER_POOL_SIZE': 'browser.poolSize',
'BROWSER_MAX_IDLE_TIME_MS': 'browser.maxIdleTimeMs',
'BROWSER_MAX_LIFETIME_MS': 'browser.maxLifetimeMs',
'BROWSER_HEADLESS': 'browser.headless',
// Logging
'LOG_LEVEL': 'logging.level',
'LOG_ENABLE_CONSOLE': 'logging.enableConsole',
'LOG_ENABLE_FILE': 'logging.enableFile',
'LOG_FILE_PATH': 'logging.filePath',
// Features
'FEATURE_ACCESSIBILITY_ANALYSIS': 'features.accessibilityAnalysis',
'FEATURE_VISUAL_DESIGN_ANALYSIS': 'features.visualDesignAnalysis',
'FEATURE_AI_CRITIQUE': 'features.aiCritique',
'FEATURE_QUEUE_SYSTEM': 'features.queueSystem',
'FEATURE_CACHING': 'features.caching',
// Redis/Queue
'REDIS_HOST': 'queue.redis.host',
'REDIS_PORT': 'queue.redis.port',
'REDIS_PASSWORD': 'queue.redis.password',
'REDIS_DB': 'queue.redis.db'
};
// Process environment variables
for (const [envVar, configPath] of Object.entries(envMappings)) {
const value = process.env[envVar];
if (value !== undefined) {
this.setNestedValue(envConfig, configPath, this.parseEnvValue(value));
}
}
return envConfig;
}
/**
* Parse environment variable value to appropriate type
*/
parseEnvValue(value) {
// Boolean
if (value.toLowerCase() === 'true') return true;
if (value.toLowerCase() === 'false') return false;
// Number
if (/^\d+$/.test(value)) return parseInt(value, 10);
if (/^\d+\.\d+$/.test(value)) return parseFloat(value);
// Array (comma-separated)
if (value.includes(',')) {
return value.split(',').map(item => item.trim());
}
// String
return value;
}
/**
* Set nested object value using dot notation
*/
setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current) || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
/**
* Merge two configuration objects
*/
mergeConfigs(base, override) {
const result = JSON.parse(JSON.stringify(base));
function merge(target, source) {
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
merge(result, override);
return result;
}
/**
* Validate configuration against schema
*/
async validateConfig(config) {
try {
const { error, value } = configSchema.validate(config, {
allowUnknown: false,
abortEarly: false,
stripUnknown: true
});
if (error) {
this.validationErrors = error.details.map(detail => ({
path: detail.path.join('.'),
message: detail.message,
value: detail.context?.value
}));
const errorMsg = `Configuration validation failed:\n${this.validationErrors
.map(err => ` - ${err.path}: ${err.message}`)
.join('\n')}`;
throw new Error(errorMsg);
}
// Use validated and default-filled configuration
Object.assign(config, value);
} catch (error) {
console.error('Configuration validation error:', error.message);
throw error;
}
}
/**
* Get configuration value by path
*/
get(path, defaultValue = undefined) {
if (!this.isInitialized) {
throw new Error('Configuration not initialized. Call initialize() first.');
}
if (!path) {
return this.config;
}
const keys = path.split('.');
let current = this.config;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
} else {
return defaultValue;
}
}
return current;
}
/**
* Check if a feature is enabled
*/
isFeatureEnabled(featureName) {
return this.get(`features.${featureName}`, false);
}
/**
* Get database configuration
*/
getDatabaseConfig() {
return this.get('database');
}
/**
* Get server configuration
*/
getServerConfig() {
return this.get('server');
}
/**
* Get AI service configuration
*/
getAIConfig() {
return this.get('ai');
}
/**
* Get analysis configuration
*/
getAnalysisConfig() {
return this.get('analysis');
}
/**
* Get browser configuration
*/
getBrowserConfig() {
return this.get('browser');
}
/**
* Get logging configuration
*/
getLoggingConfig() {
return this.get('logging');
}
/**
* Get health check configuration
*/
getHealthConfig() {
return this.get('health');
}
/**
* Get security configuration
*/
getSecurityConfig() {
return this.get('security');
}
/**
* Watch configuration file for changes
*/
async watchConfig(callback) {
if (!this.isFeatureEnabled('configReloading')) {
return;
}
const configFiles = [
path.join(__dirname, 'default.json'),
path.join(__dirname, `${this.environment}.json`)
];
for (const filePath of configFiles) {
try {
const watcher = fs.watch(filePath, async (eventType) => {
if (eventType === 'change') {
console.log(`Configuration file changed: ${filePath}`);
try {
await this.reload();
if (callback) callback(this.config);
} catch (error) {
console.error('Failed to reload configuration:', error);
}
}
});
this.watchers.set(filePath, watcher);
} catch (error) {
console.warn(`Cannot watch configuration file: ${filePath}`);
}
}
}
/**
* Reload configuration
*/
async reload() {
this.isInitialized = false;
this.config = null;
this.configCache.clear();
await this.initialize();
}
/**
* Stop watching configuration files
*/
stopWatching() {
for (const [filePath, watcher] of this.watchers) {
try {
watcher.close();
console.log(`Stopped watching: ${filePath}`);
} catch (error) {
console.warn(`Error closing watcher for ${filePath}:`, error);
}
}
this.watchers.clear();
}
/**
* Validate required environment variables
*/
validateRequiredEnvVars() {
const required = [
'GEMINI_API_KEY'
];
const missing = required.filter(envVar => !process.env[envVar]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
}
/**
* Get current environment
*/
getEnvironment() {
return this.environment;
}
/**
* Check if running in development mode
*/
isDevelopment() {
return this.environment === 'development';
}
/**
* Check if running in production mode
*/
isProduction() {
return this.environment === 'production';
}
/**
* Get configuration summary for logging
*/
getSummary() {
if (!this.isInitialized) {
return { status: 'not_initialized' };
}
return {
environment: this.environment,
server: {
port: this.get('server.port'),
host: this.get('server.host')
},
features: this.get('features'),
database: {
type: this.get('database.type'),
path: this.get('database.path')
},
analysis: {
maxConcurrent: this.get('analysis.maxConcurrentAnalyses'),
timeoutMs: this.get('analysis.timeoutMs')
}
};
}
}
// Create singleton instance
const configManager = new ConfigManager();
module.exports = configManager;