@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
405 lines • 14.8 kB
JavaScript
/**
* Configuration Manager for Code Quality Orchestrator
* Handles database initialization, service configuration, and environment setup
*/
import path from "node:path";
import * as fs from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { initializeDatabase, closeDatabase } from "../database/connection.js";
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import { getStorageService, resetStorageService } from "./storage-service.js";
// ============================================================================
// Default Configurations
// ============================================================================
export const DEFAULT_CONFIG = {
database: {
path: process.env["CQO_DB_PATH"] || "./data/code-quality.db",
enableWAL: true,
enableMetrics: true,
maxHistoryDays: 30,
},
performance: {
batchSize: 100,
maxMemoryMB: 256,
enableCaching: true,
},
scheduling: {
defaultFrequencyMs: 30_000, // 30 seconds
adaptivePolling: true,
maxConcurrentChecks: 3,
},
watch: {
enabled: true,
intervalMs: 3000, // 3 seconds
debounceMs: 500,
autoCleanup: true,
},
monitoring: {
enablePerformanceMetrics: true,
logLevel: "info",
metricsRetentionDays: 7,
},
};
// ============================================================================
// Configuration Manager Class
// ============================================================================
export class ConfigManager {
config;
databaseInitialized = false;
servicesInitialized = false;
constructor(config = {}) {
this.config = this.mergeConfigs(DEFAULT_CONFIG, config);
}
/**
* Deep merge configuration objects
*/
mergeConfigs(defaultConfig, userConfig) {
const merged = { ...defaultConfig };
for (const [key, value] of Object.entries(userConfig)) {
if (value && typeof value === "object" && !Array.isArray(value)) {
merged[key] = {
...defaultConfig[key],
...value,
};
}
else if (value !== undefined) {
merged[key] = value;
}
}
return merged;
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Update configuration (will require reinitialization)
*/
updateConfig(updates) {
this.config = this.mergeConfigs(this.config, updates);
// Mark as needing reinitialization
this.databaseInitialized = false;
this.servicesInitialized = false;
}
/**
* Initialize database with current configuration
*/
async initializeDatabase() {
if (this.databaseInitialized) {
const connectionModule = await import("../database/connection.js");
return connectionModule.getDatabase();
}
// Ensure data directory exists
const databasePath = this.config.database.path;
const databaseDirectory = path.dirname(databasePath);
await fs.mkdir(databaseDirectory, { recursive: true });
// Create database configuration
const databaseConfig = {
path: databasePath,
enableWAL: this.config.database.enableWAL ?? true,
pragmas: {
journal_mode: (this.config.database.enableWAL ?? true) ? "WAL" : "DELETE",
synchronous: "NORMAL",
cache_size: Math.floor((this.config.performance.maxMemoryMB * 1024 * 1024) / 1024), // Convert MB to KB
foreign_keys: "ON",
temp_store: "memory",
mmap_size: this.config.performance.maxMemoryMB * 1024 * 1024, // Convert MB to bytes
},
migrations: {
enabled: false, // Use direct schema initialization
path: path.join(__dirname, "../database/migrations"),
},
};
console.log(`[ConfigManager] Initializing database at: ${databasePath}`);
const database = await initializeDatabase(databaseConfig);
this.databaseInitialized = true;
console.log("[ConfigManager] Database initialized successfully");
return database;
}
/**
* Initialize all services
*/
async initializeServices() {
if (!this.databaseInitialized) {
await this.initializeDatabase();
}
if (this.servicesInitialized) {
return {
storageService: getStorageService(),
};
}
console.log("[ConfigManager] Initializing services...");
// Reset any existing service instances
resetStorageService();
// Create storage service configuration
const storageConfig = {
database: {
path: this.config.database.path,
enableWAL: this.config.database.enableWAL ?? true,
pragmas: {},
migrations: { enabled: false, path: "" },
},
batchSize: this.config.performance.batchSize ?? 1000,
maxHistoryAge: this.config.database.maxHistoryDays ?? 30,
enablePerformanceMetrics: this.config.monitoring.enablePerformanceMetrics ?? true,
};
// Initialize storage service
const storageService = getStorageService(storageConfig);
this.servicesInitialized = true;
console.log("[ConfigManager] Services initialized successfully");
return { storageService };
}
/**
* Perform health checks on all services
*/
async healthCheck() {
const results = {
database: false,
storageService: false,
overall: false,
};
try {
// Check database health
const { DatabaseConnection } = await import("../database/connection.js");
results.database = await DatabaseConnection.healthCheck();
// Check storage service
if (this.servicesInitialized) {
const storageService = getStorageService();
const stats = await storageService.getStorageStats();
results.storageService = typeof stats.totalViolations === "number";
}
results.overall = results.database && results.storageService;
}
catch (error) {
console.error("[ConfigManager] Health check failed:", error);
}
return results;
}
/**
* Get comprehensive system statistics
*/
async getSystemStats() {
const stats = {
config: this.config,
database: undefined,
storage: undefined,
performance: {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
},
};
try {
// Get database stats
if (this.databaseInitialized) {
const { DatabaseConnection } = await import("../database/connection.js");
stats.database = await DatabaseConnection.getStats();
}
// Get storage stats
if (this.servicesInitialized) {
const storageService = getStorageService();
stats.storage = await storageService.getStorageStats();
}
}
catch (error) {
console.error("[ConfigManager] Failed to get system stats:", error);
}
return stats;
}
/**
* Perform maintenance tasks
*/
async performMaintenance() {
const results = {
dataCleanup: undefined,
databaseOptimization: false,
errors: [],
};
try {
// Clean up old data
if (this.servicesInitialized) {
const storageService = getStorageService();
results.dataCleanup = await storageService.cleanupOldData();
}
// Optimize database
if (this.databaseInitialized) {
const { DatabaseConnection } = await import("../database/connection.js");
await DatabaseConnection.analyze();
results.databaseOptimization = true;
}
console.log("[ConfigManager] Maintenance completed successfully");
}
catch (error) {
const errorMessage = `Maintenance failed: ${error}`;
results.errors.push(errorMessage);
console.error("[ConfigManager]", errorMessage);
}
return results;
}
/**
* Gracefully shutdown all services
*/
async shutdown() {
console.log("[ConfigManager] Shutting down services...");
try {
// Close database connection
if (this.databaseInitialized) {
await closeDatabase();
this.databaseInitialized = false;
}
// Reset service instances
resetStorageService();
this.servicesInitialized = false;
console.log("[ConfigManager] Shutdown completed successfully");
}
catch (error) {
console.error("[ConfigManager] Shutdown error:", error);
throw error;
}
}
/**
* Load configuration from file
*/
static async loadFromFile(configPath) {
try {
const configData = await fs.readFile(configPath, "utf8");
const config = JSON.parse(configData);
return new ConfigManager(config);
}
catch (error) {
console.warn(`[ConfigManager] Could not load config from ${configPath}, using defaults:`, error);
return new ConfigManager();
}
}
/**
* Save configuration to file
*/
async saveToFile(configPath) {
try {
const configDirectory = path.dirname(configPath);
await fs.mkdir(configDirectory, { recursive: true });
const configData = JSON.stringify(this.config, undefined, 2);
await fs.writeFile(configPath, configData, "utf8");
console.log(`[ConfigManager] Configuration saved to: ${configPath}`);
}
catch (error) {
console.error(`[ConfigManager] Failed to save config to ${configPath}:`, error);
throw error;
}
}
/**
* Create configuration for different environments
*/
static createEnvironmentConfig(environment) {
const baseConfig = { ...DEFAULT_CONFIG };
switch (environment) {
case "development": {
return new ConfigManager({
...baseConfig,
database: {
...baseConfig.database,
path: "./data/dev-code-quality.db",
},
monitoring: {
...baseConfig.monitoring,
logLevel: "debug",
},
watch: {
...baseConfig.watch,
intervalMs: 1000, // Faster updates for development
},
});
}
case "test": {
return new ConfigManager({
...baseConfig,
database: {
...baseConfig.database,
path: ":memory:", // In-memory database for tests
maxHistoryDays: 1,
},
performance: {
...baseConfig.performance,
batchSize: 10, // Smaller batches for testing
},
monitoring: {
...baseConfig.monitoring,
enablePerformanceMetrics: false,
logLevel: "warn",
},
});
}
case "production": {
return new ConfigManager({
...baseConfig,
database: {
...baseConfig.database,
path: "./data/prod-code-quality.db",
maxHistoryDays: 90, // Longer retention in production
},
performance: {
...baseConfig.performance,
batchSize: 200, // Larger batches for efficiency
maxMemoryMB: 512,
},
monitoring: {
...baseConfig.monitoring,
logLevel: "info",
metricsRetentionDays: 30,
},
});
}
default: {
return new ConfigManager(baseConfig);
}
}
}
}
// ============================================================================
// Configuration Utilities
// ============================================================================
/**
* Validate configuration object
* @archaeology-exclude temporary "Will be replaced by Zod schema validation in next version"
* @archaeology-recheck-after 0.3.0
* @since 0.2.0
*/
export function validateConfig(config) {
const errors = [];
// Validate database path
if (config.database?.path && config.database.path !== ":memory:") {
const databasePath = config.database.path;
if (!path.isAbsolute(databasePath) && !databasePath.startsWith("./")) {
errors.push("Database path must be absolute or relative (starting with ./)");
}
}
// Validate performance settings
if (config.performance?.batchSize && config.performance.batchSize < 1) {
errors.push("Batch size must be at least 1");
}
if (config.performance?.maxMemoryMB && config.performance.maxMemoryMB < 64) {
errors.push("Max memory must be at least 64MB");
}
// Validate scheduling settings
if (config.scheduling?.defaultFrequencyMs &&
config.scheduling.defaultFrequencyMs < 1000) {
errors.push("Default frequency must be at least 1000ms");
}
// Validate watch settings
if (config.watch?.intervalMs && config.watch.intervalMs < 100) {
errors.push("Watch interval must be at least 100ms");
}
return errors;
}
/**
* Get configuration based on environment
*/
export function getEnvironmentConfig() {
const environment = process.env["NODE_ENV"] ||
"development";
return ConfigManager.createEnvironmentConfig(environment);
}
//# sourceMappingURL=config-manager.js.map