@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
JavaScript
"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();
}