UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

297 lines • 11 kB
/** * Enhanced Progress Reporter using cli-progress * Prevents flickering by using proper progress bar library */ import * as cliProgress from 'cli-progress'; import chalk from 'chalk'; import ProgressEventEmitter from '../../events/ProgressEventEmitter.js'; export class EnhancedProgressReporter { multiBar; bars = new Map(); entityCounts = {}; phaseStartTimes = new Map(); verboseMode; currentProject; projectSections = new Map(); // Track which projects have been displayed projects = new Map(); totalSteps = 0; completedSteps = 0; startTime = Date.now(); constructor(options = {}) { this.verboseMode = options.verbose || false; // Create multi-bar with auto-cleanup this.multiBar = new cliProgress.MultiBar({ clearOnComplete: true, // Remove completed bars hideCursor: true, forceRedraw: true, autopadding: true, linewrap: false, format: ' {entity} |' + chalk.cyan('{bar}') + '| {percentage}% | {value}/{total} {status}', barCompleteChar: '\u2588', barIncompleteChar: '\u2591', }, cliProgress.Presets.shades_classic); // Listen to progress events ProgressEventEmitter.on('progress', this.handleProgressEvent.bind(this)); } /** * Start sync operation */ startSync(operation, options) { console.log(chalk.bold.blue(`\nšŸš€ Optimizely Cache Sync`)); console.log(chalk.gray(`${operation} • ${new Date().toLocaleTimeString()}\n`)); } /** * Update progress for a specific phase */ updateProgress(progress) { const { phase, current, total, message, percent } = progress; // Skip project-level progress entirely - we'll handle headers from entity phases if (phase === 'projects') { return; } // Extract project info from phase name (project_12345_flags -> project 12345, entity flags) const parts = phase.split('_'); let projectId = ''; let entityType = phase; let projectName = 'Unknown Project'; if (parts.length >= 3 && parts[0] === 'project') { projectId = parts[1]; entityType = parts.slice(2).join('_'); } // Extract project name from message - multiple format attempts let projectMatch = message.match(/^([^:]+):\s*/); // "ProjectName: message" if (!projectMatch) { projectMatch = message.match(/(?:Syncing|Completed|Failed to sync) project:\s*([^,)]+)/i); // "Syncing project: Name" } if (!projectMatch) { projectMatch = message.match(/project:\s*([^,)]+)/i); // "project: Name" } if (projectMatch) { projectName = projectMatch[1].trim(); } else if (projectId) { projectName = `Project ${projectId}`; } // Show project header when we start a new project - use project name as key to avoid duplicates if (projectName !== 'Unknown Project' && !this.projectSections.has(projectName)) { console.log(chalk.bold.cyan(`\nšŸ“¦ ${projectName}:`)); this.projectSections.set(projectName, true); this.currentProject = projectName; } // Format entity type for display const entityMap = { 'flags': 'Flags', 'experiments': 'Experiments', 'entities': 'Entities', 'audiences': 'Audiences', 'events': 'Events', 'attributes': 'Attributes', 'experiment_results': 'Results', 'flag_environments': 'Flag Envs', 'environments': 'Environments', // Feature Experimentation environments 'groups': 'Groups', 'extensions': 'Extensions', 'pages': 'Pages', 'campaigns': 'Campaigns', 'collaborators': 'Users', 'webhooks': 'Webhooks', 'change_history': 'History', 'list_attributes': 'List Attrs', 'web_environments': 'Web Envs', // Web Experimentation environments 'features': 'Features' }; const displayEntity = (entityMap[entityType] || entityType).substring(0, 12).padEnd(12); // Get or create progress bar for this phase (only create once per phase) let bar = this.bars.get(phase); if (!bar) { // Only create bar if we're in the correct project context if (this.currentProject && projectName && projectName.includes(this.currentProject)) { bar = this.multiBar.create(total, 0, { entity: chalk.yellow(displayEntity), status: '' }); this.bars.set(phase, bar); this.phaseStartTimes.set(phase, Date.now()); } else { return; // Skip if not current project } } // Update the bar with current progress if (bar) { bar.update(current, { entity: percent >= 100 ? chalk.green(`${displayEntity} āœ“`) : chalk.yellow(displayEntity), status: percent >= 100 ? chalk.green('āœ“') : '' }); } // Extract entity counts for summary this.extractEntityCounts(message); // Complete bar if at 100% if (percent >= 100) { bar.stop(); } } /** * Extract entity counts from messages */ extractEntityCounts(message) { 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] = count; } } } /** * Complete sync operation */ completeSync(result) { // Stop all individual bars first this.bars.forEach(bar => { bar.stop(); }); // Stop the multibar this.multiBar.stop(); console.log(chalk.bold.green('\n✨ Sync Completed Successfully!')); // Show performance metrics if (result.duration) { console.log(chalk.gray(`ā±ļø Completed in ${(result.duration / 1000).toFixed(1)}s`)); } } /** * Report error */ error(error) { this.multiBar.stop(); console.error(chalk.red.bold('\nāŒ Sync Failed!')); console.error(chalk.red(error.message)); } /** * Handle progress events from ProgressEventEmitter */ handleProgressEvent(event) { if (event.type === 'start') { if (event.entity === 'project' && event.projectName) { this.startProject(event.projectName); } else if (event.projectName) { this.startEntity(event.projectName, event.entity); } } else if (event.type === 'progress' && event.count !== undefined) { this.updateEntity(event.projectName || '', event.entity, event.count, event.isOngoing, event.total); } else if (event.type === 'complete') { this.completeEntity(event.projectName || '', event.entity, event.count); this.completedSteps++; } } /** * Start a new project */ startProject(projectName) { if (!this.projectSections.has(projectName)) { // Stop all current bars before starting new project this.multiBar.stop(); // Print project header console.log(chalk.bold.cyan(`\nšŸ“¦ ${projectName}`)); this.projectSections.set(projectName, true); this.projects.set(projectName, { entities: new Map() }); this.currentProject = projectName; // Recreate multibar for this project's entities this.multiBar = new cliProgress.MultiBar({ clearOnComplete: true, hideCursor: true, forceRedraw: true, autopadding: true, linewrap: false, format: ' {entity} |' + chalk.cyan('{bar}') + '| {percentage}% | {value}/{total} {status}', barCompleteChar: '\u2588', barIncompleteChar: '\u2591', }, cliProgress.Presets.shades_classic); this.bars.clear(); } } /** * Start tracking an entity */ startEntity(projectName, entityName) { const project = this.projects.get(projectName); if (!project) return; project.entities.set(entityName, { current: 0 }); } /** * Update entity progress */ updateEntity(projectName, entityName, count, isOngoing, total) { const project = this.projects.get(projectName); if (!project) return; const entity = project.entities.get(entityName); if (!entity) return; entity.current = count; if (total !== undefined) { entity.total = total; } else if (!isOngoing) { entity.total = count; } // Update progress bar const phase = `project_${projectName}_${entityName}`; this.updateProgress({ phase, current: entity.current, total: entity.total || entity.current, message: `${entityName}: ${entity.current}`, percent: entity.total ? (entity.current / entity.total) * 100 : 0 }); } /** * Complete an entity */ completeEntity(projectName, entityName, finalCount) { const project = this.projects.get(projectName); if (!project) return; const entity = project.entities.get(entityName); if (!entity) return; entity.current = finalCount || entity.current; entity.total = entity.current; // Update progress bar to completion const phase = `project_${projectName}_${entityName}`; const bar = this.bars.get(phase); if (bar) { // Update to 100% with checkmark bar.update(entity.total, { entity: chalk.green(`${entityName.padEnd(12)} āœ“`), status: chalk.green(`āœ“`) }); // Stop and remove this bar after a short delay setTimeout(() => { bar.stop(); this.bars.delete(phase); }, 500); } } /** * Set total steps */ setTotalSteps(projectCount, entitiesPerProject, totalSteps) { this.totalSteps = totalSteps || (projectCount * entitiesPerProject); } /** * Clean up */ dispose() { this.multiBar.stop(); ProgressEventEmitter.removeListener('progress', this.handleProgressEvent.bind(this)); } } //# sourceMappingURL=EnhancedProgressReporter.js.map