UNPKG

@kya-os/cli

Version:

CLI for KYA-OS MCP-I setup and management

210 lines 6.74 kB
/** * Safety wrapper for effect execution with fallback mechanisms * Ensures effects never break core CLI functionality */ import { detectTerminalCapabilities } from "./utils.js"; const DEFAULT_SAFETY_CONFIG = { maxExecutionTime: 3000, // 3 seconds maxMemoryUsage: 50, // 50MB enableMonitoring: true, forceFallback: false, }; /** * Creates a timeout promise that rejects after specified milliseconds */ function timeout(ms) { return new Promise((_, reject) => { setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms); }); } /** * Monitors memory usage during effect execution */ class MemoryMonitor { constructor(maxMemoryMB) { this.initialMemory = process.memoryUsage().heapUsed; this.maxMemory = maxMemoryMB * 1024 * 1024; // Convert MB to bytes } check() { const currentMemory = process.memoryUsage().heapUsed; const usedMemory = currentMemory - this.initialMemory; if (usedMemory > this.maxMemory) { throw new Error(`Memory limit exceeded: ${Math.round(usedMemory / 1024 / 1024)}MB used`); } } getUsage() { const currentMemory = process.memoryUsage().heapUsed; return (currentMemory - this.initialMemory) / 1024 / 1024; // Return in MB } } /** * Safe executor for effects with comprehensive error handling */ export class SafeEffectExecutor { constructor(config = {}) { this.metrics = { frameCount: 0, totalDuration: 0, averageFrameTime: 0, maxFrameTime: 0, memoryUsage: 0, }; this.config = { ...DEFAULT_SAFETY_CONFIG, ...config }; } /** * Execute an effect with safety mechanisms */ async execute(effect, text, effectConfig = {}) { // Check if we should force fallback if (this.config.forceFallback || this.shouldUseFallback()) { return this.executeFallback(effect, text); } const memoryMonitor = new MemoryMonitor(this.config.maxMemoryUsage); const startTime = Date.now(); try { // Initialize effect effect.initialize(text, effectConfig); // Execute with timeout protection const frames = await Promise.race([ this.executeEffect(effect, memoryMonitor), timeout(this.config.maxExecutionTime).then(() => { throw new Error("Effect execution timeout"); }), ]); const duration = Date.now() - startTime; if (this.config.enableMonitoring) { this.updateMetrics(duration, memoryMonitor.getUsage()); } return { success: true, frames, duration, }; } catch (error) { // Log error for debugging but don't expose to user if (process.env.DEBUG) { console.error("Effect execution failed:", error); } // Fall back to plain text return this.executeFallback(effect, text, error); } finally { // Always cleanup try { effect.cleanup(); } catch (cleanupError) { // Ignore cleanup errors } } } /** * Execute the effect and collect frames */ async executeEffect(effect, memoryMonitor) { const frames = []; const frameStartTimes = []; while (!effect.isComplete()) { const frameStart = Date.now(); // Check memory usage memoryMonitor.check(); // Render frame const frame = await effect.render(); frames.push(...frame); const frameTime = Date.now() - frameStart; frameStartTimes.push(frameTime); if (frameTime > this.metrics.maxFrameTime) { this.metrics.maxFrameTime = frameTime; } this.metrics.frameCount++; } return frames; } /** * Execute fallback rendering */ executeFallback(effect, text, error) { try { effect.initialize(text, { noColor: true }); const frames = effect.renderFallback(); return { success: true, frames, error, }; } catch (fallbackError) { // Ultimate fallback - just return the original text return { success: false, frames: [text], error: error || fallbackError, }; } } /** * Determine if we should use fallback based on environment */ shouldUseFallback() { // Force fallback in CI environments if (process.env.CI || process.env.NO_EFFECTS) { return true; } // Lazy load terminal capabilities if (!this.terminalCapabilities) { this.terminalCapabilities = detectTerminalCapabilities(); } // Use fallback if terminal doesn't support colors or is non-interactive // At this point, terminalCapabilities is guaranteed to be defined return (!this.terminalCapabilities.supportsColor || !this.terminalCapabilities.isInteractive); } /** * Update performance metrics */ updateMetrics(duration, memoryUsage) { this.metrics.totalDuration += duration; this.metrics.memoryUsage = Math.max(this.metrics.memoryUsage, memoryUsage); this.metrics.averageFrameTime = this.metrics.totalDuration / Math.max(this.metrics.frameCount, 1); } /** * Get current performance metrics */ getMetrics() { return { ...this.metrics }; } /** * Reset performance metrics */ resetMetrics() { this.metrics = { frameCount: 0, totalDuration: 0, averageFrameTime: 0, maxFrameTime: 0, memoryUsage: 0, }; } /** * Create a wrapped effect executor for easy usage */ static wrap(effect, config) { const executor = new SafeEffectExecutor(config); return async (text, effectConfig) => { return executor.execute(effect, text, effectConfig); }; } } /** * Global effect executor instance */ export const defaultExecutor = new SafeEffectExecutor(); /** * Execute an effect safely with default configuration */ export async function executeSafely(effect, text, effectConfig) { return defaultExecutor.execute(effect, text, effectConfig); } //# sourceMappingURL=safe-executor.js.map