@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
337 lines • 12.5 kB
JavaScript
/**
* 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