UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

256 lines 9.87 kB
/** * Enhanced CLI Progress Reporter with Real-time Progress Bars * @description Provides visual progress feedback for cache sync operations using * the progress callback infrastructure implemented in Phase 3.1 * * Features: * - Real-time progress bars for each sync phase * - Multi-line progress display for concurrent operations * - Color-coded status indicators * - Detailed performance metrics * - Summary reports with SQL operation counts * * @author Optimizely MCP Server * @version 2.0.0 */ import ora from 'ora'; import chalk from 'chalk'; /** * Enhanced CLI Progress Reporter */ export class ProgressReporter { spinners = new Map(); startTime = Date.now(); phaseMetrics = new Map(); entityCounts = {}; sqlMetrics = { before: 0, after: 0 }; verboseMode; constructor(options = {}) { this.verboseMode = options.verbose || false; } /** * Handle progress update from CacheManager */ updateProgress(progress) { const { phase, current, total, message, percent } = progress; // Track phase metrics if (!this.phaseMetrics.has(phase)) { this.phaseMetrics.set(phase, { start: Date.now(), count: 0 }); } const metrics = this.phaseMetrics.get(phase); metrics.count++; // Get or create spinner for this phase let spinner = this.spinners.get(phase); if (!spinner) { spinner = ora({ text: this.formatProgressText(phase, current, total, message, percent), prefixText: chalk.cyan(`[${phase}]`), color: 'cyan' }).start(); this.spinners.set(phase, spinner); } else { spinner.text = this.formatProgressText(phase, current, total, message, percent); } // Update spinner state based on progress if (percent >= 100) { spinner.succeed(chalk.green(`✓ ${phase} completed (${metrics.count} operations)`)); metrics.end = Date.now(); this.spinners.delete(phase); } else if (message.toLowerCase().includes('error')) { spinner.fail(chalk.red(`✗ ${phase} failed: ${message}`)); this.spinners.delete(phase); } // Extract entity counts from messages this.extractEntityCounts(phase, message); // Log verbose details if (this.verboseMode) { this.logVerboseProgress(progress); } } /** * Format progress text with bar visualization */ formatProgressText(phase, current, total, message, percent) { const barLength = 30; const filledLength = Math.round((percent / 100) * barLength); const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength); return `${bar} ${percent.toFixed(0)}% | ${current}/${total} | ${message}`; } /** * Extract entity counts from progress messages */ extractEntityCounts(phase, message) { // Match patterns like "Syncing 17 audiences" or "Saved 76 events" const match = message.match(/(\d+)\s+(\w+)/); if (match) { const count = parseInt(match[1]); const entityType = match[2].toLowerCase(); if (entityType && !isNaN(count)) { this.entityCounts[entityType] = (this.entityCounts[entityType] || 0) + count; } } // Extract SQL operation counts if (message.includes('SQL operations')) { const sqlMatch = message.match(/(\d+)\s*→\s*(\d+)/); if (sqlMatch) { this.sqlMetrics.before += parseInt(sqlMatch[1]); this.sqlMetrics.after += parseInt(sqlMatch[2]); } } } /** * Log verbose progress details */ logVerboseProgress(progress) { const timestamp = new Date().toISOString(); console.log(chalk.gray(`[${timestamp}] ${JSON.stringify(progress)}`)); } /** * Display sync start message */ startSync(operation, options) { console.log('\n' + chalk.bold.blue('🚀 Optimizely Cache Sync - Performance Optimized')); console.log(chalk.gray('─'.repeat(60))); console.log(chalk.cyan(`Operation: ${operation}`)); if (options?.projectId) { console.log(chalk.cyan(`Project: ${options.projectId}`)); } console.log(chalk.cyan(`Started: ${new Date().toLocaleString()}`)); console.log(chalk.gray('─'.repeat(60)) + '\n'); this.startTime = Date.now(); } /** * Display sync completion with summary */ completeSync(result) { const duration = Date.now() - this.startTime; // Stop any remaining spinners this.spinners.forEach(spinner => spinner.stop()); this.spinners.clear(); console.log('\n' + chalk.gray('─'.repeat(60))); console.log(chalk.bold.green('✨ Sync Completed Successfully!')); console.log(chalk.gray('─'.repeat(60))); // Display summary metrics console.log(chalk.bold('\n📊 Summary:')); console.log(chalk.white(` Duration: ${(duration / 1000).toFixed(2)}s`)); console.log(chalk.white(` Projects: ${result.projectsSynced || 0}`)); // Entity counts if (Object.keys(this.entityCounts).length > 0) { console.log(chalk.bold('\n📦 Entities Processed:')); Object.entries(this.entityCounts) .sort(([a], [b]) => a.localeCompare(b)) .forEach(([entity, count]) => { console.log(chalk.white(` ${entity}: ${count}`)); }); } // Phase timings console.log(chalk.bold('\n⏱️ Phase Timings:')); this.phaseMetrics.forEach((metrics, phase) => { const duration = metrics.end ? metrics.end - metrics.start : Date.now() - metrics.start; console.log(chalk.white(` ${phase}: ${(duration / 1000).toFixed(2)}s`)); }); // SQL optimization metrics if (this.sqlMetrics.before > 0) { const reduction = this.sqlMetrics.before - this.sqlMetrics.after; const reductionPercent = (reduction / this.sqlMetrics.before) * 100; console.log(chalk.bold('\n🚀 Performance Optimization:')); console.log(chalk.green(` SQL Operations: ${this.sqlMetrics.before}${this.sqlMetrics.after}`)); console.log(chalk.green(` Reduction: ${reduction} operations (${reductionPercent.toFixed(1)}%)`)); if (reductionPercent >= 90) { console.log(chalk.bold.green(` 🎯 Achieved target 90%+ reduction!`)); } } // Additional result data if (result.totalChanges !== undefined) { console.log(chalk.bold('\n🔄 Changes:')); console.log(chalk.white(` Total: ${result.totalChanges}`)); console.log(chalk.green(` Created: ${result.totalCreated || 0}`)); console.log(chalk.yellow(` Updated: ${result.totalUpdated || 0}`)); console.log(chalk.red(` Deleted: ${result.totalDeleted || 0}`)); } console.log('\n' + chalk.gray('─'.repeat(60)) + '\n'); } /** * Display error message */ error(error) { // Stop all spinners this.spinners.forEach(spinner => spinner.fail()); this.spinners.clear(); console.log('\n' + chalk.bold.red('❌ Sync Failed!')); console.log(chalk.red(`Error: ${error.message}`)); if (this.verboseMode && error.stack) { console.log(chalk.gray('\nStack trace:')); console.log(chalk.gray(error.stack)); } } /** * Create a simple progress bar without spinners (for non-TTY environments) */ simpleProgress(progress) { const { phase, percent, message } = progress; const dots = '.'.repeat(Math.floor(percent / 10)); const spaces = ' '.repeat(10 - dots.length); process.stdout.write(`\r[${phase}] [${dots}${spaces}] ${percent}% - ${message}`); if (percent >= 100) { process.stdout.write('\n'); } } /** * Check if terminal supports interactive progress */ static supportsProgress() { return process.stdout.isTTY && !process.env.CI; } } /** * Progress aggregator for multi-project sync */ export class MultiProjectProgressAggregator { projectProgress = new Map(); reporter; constructor(reporter) { this.reporter = reporter; } /** * Handle progress for specific project */ updateProjectProgress(projectId, progress) { if (!this.projectProgress.has(projectId)) { this.projectProgress.set(projectId, []); } this.projectProgress.get(projectId).push(progress); // Aggregate progress across projects const aggregatedProgress = this.aggregateProgress(progress, projectId); this.reporter.updateProgress(aggregatedProgress); } /** * Aggregate progress across multiple projects */ aggregateProgress(progress, projectId) { // Add project context to phase const phase = `${progress.phase} (Project ${projectId})`; return { ...progress, phase, message: `[P${projectId}] ${progress.message}` }; } /** * Get summary for all projects */ getSummary() { const summary = {}; this.projectProgress.forEach((updates, projectId) => { summary[projectId] = { totalUpdates: updates.length, phases: [...new Set(updates.map(u => u.phase))].length, lastUpdate: updates[updates.length - 1] }; }); return summary; } } //# sourceMappingURL=ProgressReporter.js.map