@kya-os/cli
Version:
CLI for KYA-OS MCP-I setup and management
210 lines • 6.74 kB
JavaScript
/**
* 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