UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

480 lines (479 loc) • 19.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.configWatcher = exports.ConfigWatcher = void 0; exports.setupConfigHotReload = setupConfigHotReload; exports.startDevMode = startDevMode; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const chokidar = __importStar(require("chokidar")); const chalk_1 = __importDefault(require("chalk")); const events_1 = require("events"); const config_1 = require("./config"); const error_handler_1 = require("./error-handler"); // Configuration watcher class class ConfigWatcher extends events_1.EventEmitter { constructor(options = {}) { super(); this.watchers = new Map(); this.debounceTimers = new Map(); this.isWatching = false; this.lastConfigs = new Map(); this.backupIds = new Map(); this.options = { enabled: true, debounceMs: 500, validateOnChange: true, autoBackup: false, restoreOnError: false, verbose: false, includeWorkspaces: true, excludePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**'], ...options }; } // Start watching configuration files async startWatching() { if (this.isWatching) { if (this.options.verbose) { console.log(chalk_1.default.yellow('Configuration watcher is already running')); } return; } if (!this.options.enabled) { if (this.options.verbose) { console.log(chalk_1.default.gray('Configuration hot-reloading is disabled')); } return; } this.isWatching = true; try { // Watch global configuration await this.watchGlobalConfig(); // Watch project configuration await this.watchProjectConfig(); // Watch workspace configurations if enabled if (this.options.includeWorkspaces) { await this.watchWorkspaceConfigs(); } if (this.options.verbose) { console.log(chalk_1.default.green('šŸ”„ Configuration hot-reloading started')); this.logWatchedFiles(); } // Store initial configurations for comparison await this.storeInitialConfigs(); this.emit('watching-started'); } catch (error) { this.isWatching = false; if (this.options.verbose) { console.error(chalk_1.default.red('Failed to start configuration watcher:'), error); } throw error; } } // Stop watching configuration files async stopWatching() { if (!this.isWatching) return; // Clear all debounce timers for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); // Close all watchers for (const [path, watcher] of this.watchers) { await watcher.close(); if (this.options.verbose) { console.log(chalk_1.default.gray(`Stopped watching: ${path}`)); } } this.watchers.clear(); this.isWatching = false; this.lastConfigs.clear(); this.backupIds.clear(); if (this.options.verbose) { console.log(chalk_1.default.green('šŸ›‘ Configuration hot-reloading stopped')); } this.emit('watching-stopped'); } // Check if watcher is currently active isActive() { return this.isWatching; } // Get current watch status getStatus() { return { isWatching: this.isWatching, watchedPaths: Array.from(this.watchers.keys()), options: this.options, lastChanges: [] // TODO: Implement change history }; } // Update watcher options updateOptions(newOptions) { const wasWatching = this.isWatching; if (wasWatching) { this.stopWatching(); } this.options = { ...this.options, ...newOptions }; if (wasWatching && this.options.enabled) { this.startWatching(); } } // Force reload all configurations async forceReload() { if (this.options.verbose) { console.log(chalk_1.default.cyan('šŸ”„ Force reloading all configurations...')); } try { // Reload global config const globalConfig = await config_1.configManager.loadGlobalConfig(); this.emit('config-changed', { type: 'changed', path: config_1.CONFIG_PATHS.GLOBAL_CONFIG, configType: 'global', timestamp: new Date(), config: globalConfig }); // Reload project config try { const projectConfig = await config_1.configManager.loadProjectConfig(); this.emit('config-changed', { type: 'changed', path: path.join(process.cwd(), config_1.CONFIG_PATHS.PROJECT_CONFIG), configType: 'project', timestamp: new Date(), config: projectConfig }); } catch (error) { // Project config might not exist } if (this.options.verbose) { console.log(chalk_1.default.green('āœ… Force reload completed')); } } catch (error) { if (this.options.verbose) { console.error(chalk_1.default.red('āŒ Force reload failed:'), error); } throw error; } } // Private methods async watchGlobalConfig() { const globalConfigPath = config_1.CONFIG_PATHS.GLOBAL_CONFIG; const globalConfigDir = path.dirname(globalConfigPath); // Ensure global config directory exists await fs.ensureDir(globalConfigDir); const watcher = chokidar.watch(globalConfigPath, { ignored: this.options.excludePatterns, persistent: true, ignoreInitial: true }); watcher .on('change', (filePath) => this.handleConfigChange(filePath, 'global', 'changed')) .on('add', (filePath) => this.handleConfigChange(filePath, 'global', 'added')) .on('unlink', (filePath) => this.handleConfigChange(filePath, 'global', 'unlinked')) .on('error', (error) => { if (this.options.verbose) { console.error(chalk_1.default.red(`Global config watcher error: ${error}`)); } this.emit('error', error); }); this.watchers.set(globalConfigPath, watcher); } async watchProjectConfig() { const projectConfigPath = path.join(process.cwd(), config_1.CONFIG_PATHS.PROJECT_CONFIG); const projectConfigDir = path.dirname(projectConfigPath); // Only watch if project config directory exists if (!(await fs.pathExists(projectConfigDir))) { return; } const watcher = chokidar.watch(projectConfigPath, { ignored: this.options.excludePatterns, persistent: true, ignoreInitial: true }); watcher .on('change', (filePath) => this.handleConfigChange(filePath, 'project', 'changed')) .on('add', (filePath) => this.handleConfigChange(filePath, 'project', 'added')) .on('unlink', (filePath) => this.handleConfigChange(filePath, 'project', 'unlinked')) .on('error', (error) => { if (this.options.verbose) { console.error(chalk_1.default.red(`Project config watcher error: ${error}`)); } this.emit('error', error); }); this.watchers.set(projectConfigPath, watcher); } async watchWorkspaceConfigs() { // Find workspace configuration files const searchPaths = [ path.join(process.cwd(), 'apps'), path.join(process.cwd(), 'packages'), path.join(process.cwd(), 'libs'), path.join(process.cwd(), 'tools') ]; for (const searchPath of searchPaths) { if (await fs.pathExists(searchPath)) { const workspacePattern = path.join(searchPath, '**/.re-shell/config.yaml'); const watcher = chokidar.watch(workspacePattern, { ignored: this.options.excludePatterns, persistent: true, ignoreInitial: true }); watcher .on('change', (filePath) => this.handleConfigChange(filePath, 'workspace', 'changed')) .on('add', (filePath) => this.handleConfigChange(filePath, 'workspace', 'added')) .on('unlink', (filePath) => this.handleConfigChange(filePath, 'workspace', 'unlinked')) .on('error', (error) => { if (this.options.verbose) { console.error(chalk_1.default.red(`Workspace config watcher error: ${error}`)); } this.emit('error', error); }); this.watchers.set(workspacePattern, watcher); } } } handleConfigChange(filePath, configType, changeType) { // Clear existing debounce timer const existingTimer = this.debounceTimers.get(filePath); if (existingTimer) { clearTimeout(existingTimer); } // Set new debounce timer const timer = setTimeout(async () => { await this.processConfigChange(filePath, configType, changeType); this.debounceTimers.delete(filePath); }, this.options.debounceMs); this.debounceTimers.set(filePath, timer); } async processConfigChange(filePath, configType, changeType) { const event = { type: changeType, path: filePath, configType, timestamp: new Date() }; if (this.options.verbose) { console.log(chalk_1.default.cyan(`šŸ”„ Config ${changeType}: ${path.relative(process.cwd(), filePath)} (${configType})`)); } try { if (changeType === 'unlinked') { // Handle file deletion this.lastConfigs.delete(filePath); this.emit('config-changed', event); return; } // Create backup if enabled if (this.options.autoBackup && changeType === 'changed') { await this.createChangeBackup(filePath, configType); } // Load and validate the new configuration let newConfig; switch (configType) { case 'global': newConfig = await config_1.configManager.loadGlobalConfig(); break; case 'project': newConfig = await config_1.configManager.loadProjectConfig(); break; case 'workspace': const workspacePath = path.dirname(path.dirname(filePath)); newConfig = await config_1.configManager.loadWorkspaceConfig(workspacePath); break; } // Validate configuration if enabled if (this.options.validateOnChange) { await this.validateConfig(newConfig, configType); } // Check if configuration actually changed const lastConfig = this.lastConfigs.get(filePath); const configChanged = !lastConfig || JSON.stringify(lastConfig) !== JSON.stringify(newConfig); if (configChanged) { this.lastConfigs.set(filePath, newConfig); event.config = newConfig; this.emit('config-changed', event); if (this.options.verbose) { console.log(chalk_1.default.green(`āœ… Configuration reloaded: ${path.basename(filePath)}`)); } } else if (this.options.verbose) { console.log(chalk_1.default.gray(`ā­ļø No changes detected: ${path.basename(filePath)}`)); } } catch (error) { event.error = error; if (this.options.verbose) { console.error(chalk_1.default.red(`āŒ Failed to reload config: ${error}`)); } // Restore from backup if enabled and available if (this.options.restoreOnError) { await this.restoreFromBackup(filePath, configType); } this.emit('config-error', event); } } async validateConfig(config, configType) { // Basic validation - can be extended with schema validation if (!config || typeof config !== 'object') { throw new error_handler_1.ValidationError(`Invalid ${configType} configuration: must be an object`); } // Type-specific validation switch (configType) { case 'global': if (!config.version) { throw new error_handler_1.ValidationError('Global configuration missing version field'); } break; case 'project': if (!config.name) { throw new error_handler_1.ValidationError('Project configuration missing name field'); } break; case 'workspace': if (!config.name || !config.type) { throw new error_handler_1.ValidationError('Workspace configuration missing required fields (name, type)'); } break; } } async createChangeBackup(filePath, configType) { try { const { configBackupManager } = require('./config-backup'); const backupName = `auto-${configType}-${Date.now()}`; const backupId = await configBackupManager.createSelectiveBackup(backupName, { [configType]: true }, `Automatic backup before configuration change`, ['auto', 'hot-reload', configType]); this.backupIds.set(filePath, backupId); } catch (error) { if (this.options.verbose) { console.warn(chalk_1.default.yellow(`Warning: Failed to create backup: ${error}`)); } } } async restoreFromBackup(filePath, configType) { const backupId = this.backupIds.get(filePath); if (!backupId) return; try { const { configBackupManager } = require('./config-backup'); await configBackupManager.restoreFromBackup(backupId, { force: true }); if (this.options.verbose) { console.log(chalk_1.default.green(`šŸ”„ Restored configuration from backup: ${backupId}`)); } } catch (error) { if (this.options.verbose) { console.warn(chalk_1.default.yellow(`Warning: Failed to restore from backup: ${error}`)); } } } async storeInitialConfigs() { try { // Store initial global config const globalPath = config_1.CONFIG_PATHS.GLOBAL_CONFIG; if (await fs.pathExists(globalPath)) { const globalConfig = await config_1.configManager.loadGlobalConfig(); this.lastConfigs.set(globalPath, globalConfig); } // Store initial project config const projectPath = path.join(process.cwd(), config_1.CONFIG_PATHS.PROJECT_CONFIG); if (await fs.pathExists(projectPath)) { const projectConfig = await config_1.configManager.loadProjectConfig(); this.lastConfigs.set(projectPath, projectConfig); } } catch (error) { if (this.options.verbose) { console.warn(chalk_1.default.yellow(`Warning: Failed to store initial configs: ${error}`)); } } } logWatchedFiles() { console.log(chalk_1.default.cyan('\nšŸ‘€ Watching configuration files:')); for (const path of this.watchers.keys()) { console.log(` • ${chalk_1.default.gray(path)}`); } console.log(); } } exports.ConfigWatcher = ConfigWatcher; // Export singleton instance exports.configWatcher = new ConfigWatcher(); // Utility function to setup hot reloading with default options async function setupConfigHotReload(options = {}) { const watcher = new ConfigWatcher(options); // Setup default event handlers watcher.on('config-changed', (event) => { if (options.verbose) { console.log(chalk_1.default.green(`šŸ”„ Configuration updated: ${event.configType}`)); } }); watcher.on('config-error', (event) => { if (options.verbose) { console.error(chalk_1.default.red(`āŒ Configuration error: ${event.error?.message}`)); } }); await watcher.startWatching(); return watcher; } // Utility function for development mode async function startDevMode(options = {}) { const devOptions = { enabled: true, verbose: true, validateOnChange: true, autoBackup: true, restoreOnError: true, debounceMs: 300, ...options }; const watcher = await setupConfigHotReload(devOptions); console.log(chalk_1.default.green('šŸš€ Development mode started with configuration hot-reloading')); console.log(chalk_1.default.gray('Press Ctrl+C to stop\n')); // Handle graceful shutdown process.on('SIGINT', async () => { console.log(chalk_1.default.yellow('\nšŸ›‘ Shutting down development mode...')); await watcher.stopWatching(); process.exit(0); }); return Promise.resolve(); }