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