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