UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

337 lines 12.5 kB
/** * Compact Progress Reporter - Clean, hierarchical progress display * Uses ANSI escape codes for in-place updates with minimal visual clutter */ import chalk from 'chalk'; import ProgressEventEmitter from '../../events/ProgressEventEmitter.js'; export class CompactProgressReporter { projects = new Map(); currentLine = 0; totalSteps = 0; completedSteps = 0; progressBarLine = 0; spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; spinnerIndex = 0; spinnerInterval = null; startTime = Date.now(); hasDrawnProgressBar = false; constructor() { // Hide cursor for cleaner display process.stdout.write('\u001B[?25l'); // Listen to progress events ProgressEventEmitter.on('progress', this.handleProgressEvent.bind(this)); // Start spinner animation this.startSpinner(); } /** * Start sync operation */ startSync(operation, options) { console.log(chalk.bold.blue(`🚀 Optimizely Cache Sync`)); console.log(chalk.gray(`${operation} • ${new Date().toLocaleTimeString()}`)); console.log(''); // Empty line for spacing this.currentLine = 3; } /** * Set total steps */ setTotalSteps(projectCount, entitiesPerProject, totalSteps) { this.totalSteps = totalSteps || (projectCount * entitiesPerProject); // Reserve space for progress bar at bottom console.log(''); // Extra line for progress bar this.progressBarLine = this.currentLine; this.currentLine++; // Draw initial progress bar this.updateProgressBar(); this.hasDrawnProgressBar = true; } /** * Handle progress events */ 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); } else if (event.type === 'complete') { this.completeEntity(event.projectName || '', event.entity, event.count); this.completedSteps++; this.updateProgressBar(); } } /** * Start a new project section */ startProject(projectName) { const isFeature = projectName.toLowerCase().includes('feature') || projectName.toLowerCase().includes('flag'); // Move cursor to correct position for new content if (this.hasDrawnProgressBar) { // Move above progress bar const moveUp = this.currentLine - this.progressBarLine + 1; process.stdout.write(`\x1B[${moveUp}A`); } // Add blank line between projects (except first) if (this.projects.size > 0) { console.log(''); this.currentLine++; } // Print project header const icon = isFeature ? '📦' : '📂'; console.log(chalk.bold.cyan(`${icon} ${projectName}`)); this.currentLine++; // Store project info this.projects.set(projectName, { name: projectName, startLine: this.currentLine, entities: new Map(), isFeature }); // Move back down if we have a progress bar if (this.hasDrawnProgressBar) { const moveDown = this.currentLine - this.progressBarLine + 1; process.stdout.write(`\x1B[${moveDown}B`); } } /** * Start tracking an entity */ startEntity(projectName, entityName) { const project = this.projects.get(projectName); if (!project) return; // Skip if already tracking if (project.entities.has(entityName)) return; // Move cursor to correct position if (this.hasDrawnProgressBar) { const moveUp = this.currentLine - this.progressBarLine + 1; process.stdout.write(`\x1B[${moveUp}A`); } // Add entity to tracking project.entities.set(entityName, { line: this.currentLine, name: entityName, current: 0, status: 'active', startTime: Date.now() }); // Print initial line const isLast = false; // Will update when we know all entities const entity = project.entities.get(entityName); console.log(this.formatEntityLine(entity, isLast)); this.currentLine++; // Move back down if (this.hasDrawnProgressBar) { const moveDown = this.currentLine - this.progressBarLine + 1; process.stdout.write(`\x1B[${moveDown}B`); } } /** * Update entity progress */ updateEntity(projectName, entityName, count, isOngoing) { const project = this.projects.get(projectName); if (!project) return; const entity = project.entities.get(entityName); if (!entity || entity.status === 'complete') return; // Update count entity.current = count; if (!isOngoing) { entity.total = count; } // Update the line in place this.updateEntityLine(project, entityName); } /** * 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; // Update status entity.status = 'complete'; entity.current = finalCount || entity.current; entity.total = entity.current; // Update the line this.updateEntityLine(project, entityName); } /** * Update entity line in place */ updateEntityLine(project, entityName) { const entity = project.entities.get(entityName); if (!entity) return; // Save current position process.stdout.write('\x1B[s'); // Calculate position relative to progress bar const targetLine = entity.line; const currentPos = this.hasDrawnProgressBar ? this.progressBarLine : this.currentLine; const linesToMoveUp = currentPos - targetLine; // Move to entity line if (linesToMoveUp > 0) { process.stdout.write(`\x1B[${linesToMoveUp}A`); } else if (linesToMoveUp < 0) { process.stdout.write(`\x1B[${-linesToMoveUp}B`); } // Clear line and write new content process.stdout.write('\r\x1B[K'); const entityNames = Array.from(project.entities.keys()); const isLast = entityNames[entityNames.length - 1] === entityName; process.stdout.write(this.formatEntityLine(entity, isLast)); // Restore position process.stdout.write('\x1B[u'); } /** * Format entity line */ formatEntityLine(entity, isLast) { const prefix = isLast ? '└─' : '│ '; const name = entity.name.padEnd(16, ' '); const dots = '.'.repeat(Math.max(1, 20 - name.length)); let status = ''; if (entity.status === 'complete') { const count = entity.total || entity.current; const countStr = count === 0 ? 'none' : count.toString(); status = chalk.green(`${countStr.padStart(6)} ✔`); } else { const countStr = entity.current.toString(); const totalStr = entity.total ? entity.total.toString() : '?'; const progress = `${countStr}/${totalStr}`; const spinner = this.spinnerFrames[this.spinnerIndex]; status = chalk.yellow(`${progress.padStart(9)} ${spinner}`); // Add context for special cases if (entity.name === 'flag_rulesets' && entity.current > 100) { const batches = Math.floor(entity.current / 40); // Approximate batches status += chalk.gray(` (${batches} batches)`); } } return `${prefix} ${chalk.white(name)} ${chalk.gray(dots)} ${status}`; } /** * Update progress bar at bottom */ updateProgressBar() { if (this.totalSteps === 0 || !this.hasDrawnProgressBar) return; // Calculate progress const percent = Math.round((this.completedSteps / this.totalSteps) * 100); const barLength = 30; const filledLength = Math.round((barLength * this.completedSteps) / this.totalSteps); const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength); const elapsed = Math.round((Date.now() - this.startTime) / 1000); const progressText = `Progress: [${bar}] ${percent}% (${this.completedSteps}/${this.totalSteps}) ${elapsed}s`; // Save position process.stdout.write('\x1B[s'); // Move to progress bar line const currentPos = Math.max(this.currentLine, this.progressBarLine); const linesToMove = currentPos - this.progressBarLine; if (linesToMove > 0) { process.stdout.write(`\x1B[${linesToMove}A`); } // Update progress bar process.stdout.write('\r\x1B[K'); process.stdout.write(chalk.cyan(progressText)); // Restore position process.stdout.write('\x1B[u'); } /** * Complete sync operation */ completeSync(result) { // Stop spinner this.stopSpinner(); // Move to after all content const finalLine = Math.max(this.currentLine, this.progressBarLine + 1); const linesToMove = finalLine - (this.hasDrawnProgressBar ? this.progressBarLine : this.currentLine); if (linesToMove > 0) { process.stdout.write(`\x1B[${linesToMove}B`); } // Show cursor again process.stdout.write('\u001B[?25h'); console.log(''); // Empty line console.log(chalk.bold.green('✨ Sync Completed Successfully!')); if (result.duration) { console.log(chalk.gray(`⏱️ Completed in ${(result.duration / 1000).toFixed(1)}s`)); } } /** * Report error */ error(error) { this.stopSpinner(); // Show cursor again process.stdout.write('\u001B[?25h'); // Move to after all content const finalLine = Math.max(this.currentLine, this.progressBarLine + 1); const linesToMove = finalLine - (this.hasDrawnProgressBar ? this.progressBarLine : this.currentLine); if (linesToMove > 0) { process.stdout.write(`\x1B[${linesToMove}B`); } console.log(''); // Empty line console.error(chalk.red.bold('❌ Sync Failed!')); console.error(chalk.red(error.message)); } /** * Start spinner animation */ startSpinner() { if (!this.spinnerInterval) { this.spinnerInterval = setInterval(() => { this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length; // Update all active entities this.projects.forEach(project => { project.entities.forEach((entity, name) => { if (entity.status === 'active') { this.updateEntityLine(project, name); } }); }); // Also update progress bar time this.updateProgressBar(); }, 100); } } /** * Stop spinner animation */ stopSpinner() { if (this.spinnerInterval) { clearInterval(this.spinnerInterval); this.spinnerInterval = null; } } /** * Clean up */ dispose() { this.stopSpinner(); // Show cursor process.stdout.write('\u001B[?25h'); // Remove event listeners ProgressEventEmitter.removeListener('progress', this.handleProgressEvent.bind(this)); } /** * Legacy updateProgress method (no-op in compact mode) */ updateProgress(progress) { // Do nothing - we use events only } } //# sourceMappingURL=CompactProgressReporter.js.map