UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and

302 lines (301 loc) 11 kB
/** * NeuroLink Config Manager with Backup/Restore System * Industry standard configuration management with safety mechanisms */ import { promises as fs } from "fs"; import path from "path"; import crypto from "crypto"; import { logger } from "../utils/logger.js"; const { readFile, writeFile, readdir, mkdir, unlink, access } = fs; /** * Enhanced Config Manager with automatic backup/restore capabilities */ export class NeuroLinkConfigManager { configPath = ".neurolink.config"; backupDir = ".neurolink.backups"; config = null; configCache = new Map(); /** * Load configuration with caching */ async loadConfig() { if (!this.config) { this.config = await this.readConfigFile(); } return this.config; } /** * Update configuration with automatic backup */ async updateConfig(updates, options = {}) { const { createBackup = true, validate = true, merge = true, reason = "update", silent = false, } = options; // ALWAYS create backup before updating (unless explicitly disabled) if (createBackup) { await this.createBackup(reason); if (!silent) { logger.info("💾 Backup created before config update"); } } const existing = await this.loadConfig(); // Merge or replace based on options this.config = merge ? { ...existing, ...updates, lastUpdated: Date.now() } : { ...updates, lastUpdated: Date.now() }; // Validate config if requested if (validate) { const validation = await this.validateConfig(this.config); if (!validation.valid) { throw new Error(`Config validation failed: ${validation.errors.join(", ")}`); } } try { await this.persistConfig(this.config); if (!silent) { logger.info("✅ Configuration updated successfully"); } } catch (error) { // Auto-restore on failure if (createBackup) { await this.restoreLatestBackup(); if (!silent) { logger.info("🔄 Auto-restored from backup due to error"); } } throw new Error(`Config update failed, restored from backup: ${error.message}`); } } /** * Create a backup with metadata */ async createBackup(reason = "manual") { await this.ensureBackupDirectory(); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const backupFilename = `neurolink-config-${timestamp}.js`; const backupPath = path.join(this.backupDir, backupFilename); const currentConfig = await this.loadConfig(); const configHash = this.generateConfigHash(currentConfig); const backupMetadata = { reason, timestamp: Date.now(), version: currentConfig.configVersion || "unknown", originalPath: this.configPath, hash: configHash, size: JSON.stringify(currentConfig).length, createdBy: "NeuroLinkConfigManager", }; const backupContent = `// NeuroLink Config Backup - ${reason} // Created: ${new Date().toISOString()} // Reason: ${reason} // Hash: ${configHash} export const metadata = ${JSON.stringify(backupMetadata, null, 2)}; export default ${JSON.stringify(currentConfig, null, 2)};`; await writeFile(backupPath, backupContent, "utf-8"); return backupPath; } /** * List all available backups */ async listBackups() { await this.ensureBackupDirectory(); try { const files = await readdir(this.backupDir); const backupFiles = files.filter((f) => f.startsWith("neurolink-config-") && f.endsWith(".js")); const backups = []; for (const file of backupFiles) { try { const filePath = path.join(this.backupDir, file); const content = await readFile(filePath, "utf-8"); const metadata = this.extractMetadataFromBackup(content); const config = this.extractConfigFromBackup(content); backups.push({ filename: file, path: filePath, metadata, config, }); } catch (error) { logger.warn(`Failed to read backup ${file}:`, error.message); } } return backups.sort((a, b) => b.metadata.timestamp - a.metadata.timestamp); } catch (error) { logger.warn("Failed to list backups:", error.message); return []; } } /** * Restore from specific backup */ async restoreFromBackup(backupFilename) { const backupPath = path.join(this.backupDir, backupFilename); // Create backup of current config before restore await this.createBackup("pre-restore"); try { const content = await readFile(backupPath, "utf-8"); const restoredConfig = this.extractConfigFromBackup(content); // Validate restored config const validation = await this.validateConfig(restoredConfig); if (!validation.valid) { throw new Error(`Backup config is invalid: ${validation.errors.join(", ")}`); } this.config = restoredConfig; await this.persistConfig(this.config); logger.info(`✅ Config restored from backup: ${backupFilename}`); } catch (error) { throw new Error(`Failed to restore from backup ${backupFilename}: ${error.message}`); } } /** * Restore from latest backup */ async restoreLatestBackup() { const backups = await this.listBackups(); if (backups.length === 0) { throw new Error("No backups available for restore"); } await this.restoreFromBackup(backups[0].filename); } /** * Clean up old backups */ async cleanupOldBackups(keepCount = 10) { const backups = await this.listBackups(); const toDelete = backups.slice(keepCount); for (const backup of toDelete) { try { await unlink(backup.path); logger.info(`🗑️ Deleted old backup: ${backup.filename}`); } catch (error) { logger.warn(`Failed to delete backup ${backup.filename}:`, error.message); } } } /** * Update provider status */ async updateProviderStatus(providerId, status) { const config = await this.loadConfig(); if (!config.providers) { config.providers = {}; } config.providers[providerId] = { ...config.providers[providerId], ...status, lastCheck: Date.now(), }; await this.updateConfig({ providers: config.providers }, { reason: `provider-${providerId}-update` }); } /** * Validate configuration */ async validateConfig(config) { const result = { valid: true, errors: [], warnings: [], suggestions: [], }; // Basic validation if (!config || typeof config !== "object") { result.errors.push("Config must be a valid object"); result.valid = false; return result; } // Version validation if (config.configVersion && typeof config.configVersion !== "string") { result.errors.push("configVersion must be a string"); result.valid = false; } // Provider validation if (config.providers) { if (typeof config.providers !== "object") { result.errors.push("providers must be an object"); result.valid = false; } else { // Check for default provider if (!config.providers.defaultProvider) { result.warnings.push("No default provider specified"); result.suggestions.push('Consider setting providers.defaultProvider to "googleAi"'); } } } // Performance validation if (config.performance?.cache?.ttlMs) { if (config.performance.cache.ttlMs < 1000) { result.warnings.push("Cache TTL is very low (< 1 second)"); } } return result; } /** * Generate default configuration */ async generateDefaultConfig() { const { DEFAULT_CONFIG } = await import("./types.js"); return { ...DEFAULT_CONFIG, lastUpdated: Date.now(), }; } // Private helper methods async ensureBackupDirectory() { try { await mkdir(this.backupDir, { recursive: true }); } catch (error) { logger.warn("Failed to create backup directory:", error.message); } } async readConfigFile() { try { // Check if config file exists await access(this.configPath); const content = await readFile(this.configPath, "utf-8"); // Parse as JavaScript module const configMatch = content.match(/export default ([\s\S]+);?$/); if (configMatch) { const configJson = configMatch[1].trim(); return JSON.parse(configJson); } throw new Error("Invalid config file format"); } catch (error) { logger.info("Config file not found, generating default..."); return await this.generateDefaultConfig(); } } async persistConfig(config) { const configContent = `export default ${JSON.stringify(config, null, 2)};`; await writeFile(this.configPath, configContent, "utf-8"); // Clear cache after persisting this.config = config; } generateConfigHash(config) { const configString = JSON.stringify(config, Object.keys(config).sort()); return crypto .createHash("sha256") .update(configString) .digest("hex") .substring(0, 8); } extractMetadataFromBackup(content) { const metadataMatch = content.match(/export const metadata = ([\s\S]+?);/); if (metadataMatch) { return JSON.parse(metadataMatch[1]); } throw new Error("No metadata found in backup file"); } extractConfigFromBackup(content) { const configMatch = content.match(/export default ([\s\S]+);$/); if (configMatch) { return JSON.parse(configMatch[1]); } throw new Error("No config found in backup file"); } }