@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
318 lines • 11.2 kB
JavaScript
/**
* Progress Reporter for Orchestration Template CLI
* Inspired by the cache-sync-enhanced progress reporting
*/
import chalk from 'chalk';
import * as readline from 'readline';
/**
* Base progress reporter with TTY-aware display
*/
export class ProgressReporter {
startTime = 0;
phases = new Map();
currentPhase = '';
isTTY;
lastLineCount = 0;
constructor() {
this.isTTY = process.stdout.isTTY || false;
}
/**
* Start a new operation
*/
startOperation(operation) {
this.startTime = Date.now();
this.phases.clear();
this.currentPhase = operation;
if (this.isTTY) {
console.log(chalk.blue.bold(`\n🚀 ${operation}\n`));
}
else {
console.log(`\n[START] ${operation}\n`);
}
}
/**
* Update progress for current phase
*/
updateProgress(update) {
// Track phase timing
if (!this.phases.has(update.phase)) {
this.phases.set(update.phase, { start: Date.now() });
}
if (this.isTTY) {
this.displayProgressBar(update);
}
else {
this.displayProgressText(update);
}
// Mark phase complete if at 100%
if (update.percent >= 100) {
const phase = this.phases.get(update.phase);
if (phase && !phase.end) {
phase.end = Date.now();
}
}
}
/**
* Display progress bar for TTY environments
*/
displayProgressBar(update) {
// Clear previous lines
if (this.lastLineCount > 0) {
for (let i = 0; i < this.lastLineCount; i++) {
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
}
}
const barWidth = 40;
const filled = Math.round((update.percent / 100) * barWidth);
const empty = barWidth - filled;
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
const lines = [
chalk.cyan(`📋 ${update.phase}`),
`${bar} ${chalk.yellow(`${update.percent}%`)}`,
chalk.gray(` ${update.message}`),
chalk.gray(` Progress: ${update.current}/${update.total}`)
];
if (update.subPhase) {
lines.push(chalk.gray(` ${update.subPhase}`));
}
lines.forEach(line => console.log(line));
this.lastLineCount = lines.length;
}
/**
* Display progress text for non-TTY environments
*/
displayProgressText(update) {
console.log(`[${update.phase}] ${update.percent}% - ${update.message} (${update.current}/${update.total})`);
}
/**
* Report an error
*/
error(error) {
const message = error instanceof Error ? error.message : error;
if (this.isTTY) {
console.log(chalk.red.bold(`\n❌ Error: ${message}`));
}
else {
console.log(`\n[ERROR] ${message}`);
}
}
/**
* Report a warning
*/
warning(message) {
if (this.isTTY) {
console.log(chalk.yellow(`\n⚠️ Warning: ${message}`));
}
else {
console.log(`\n[WARNING] ${message}`);
}
}
/**
* Complete the operation
*/
completeOperation(result) {
const duration = Date.now() - this.startTime;
if (this.isTTY) {
// Clear any remaining progress display
if (this.lastLineCount > 0) {
for (let i = 0; i < this.lastLineCount; i++) {
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
}
}
console.log('\n' + chalk.bold('━'.repeat(50)));
if (result.success) {
console.log(chalk.green.bold(`\n✅ ${result.message}`));
}
else {
console.log(chalk.red.bold(`\n❌ ${result.message}`));
}
// Show metrics
console.log(chalk.gray(`\n⏱️ Duration: ${this.formatDuration(duration)}`));
if (result.entityCount !== undefined) {
console.log(chalk.gray(`📊 Entities processed: ${result.entityCount}`));
}
// Show phase timings
if (this.phases.size > 0) {
console.log(chalk.gray(`\n📈 Phase Breakdown:`));
this.phases.forEach((timing, phase) => {
const phaseDuration = (timing.end || Date.now()) - timing.start;
console.log(chalk.gray(` • ${phase}: ${this.formatDuration(phaseDuration)}`));
});
}
// Show warnings/errors
if (result.warnings && result.warnings.length > 0) {
console.log(chalk.yellow(`\n⚠️ Warnings (${result.warnings.length}):`));
result.warnings.forEach(w => console.log(chalk.yellow(` • ${w}`)));
}
if (result.errors && result.errors.length > 0) {
console.log(chalk.red(`\n❌ Errors (${result.errors.length}):`));
result.errors.forEach(e => console.log(chalk.red(` • ${e}`)));
}
console.log('\n' + chalk.bold('━'.repeat(50)) + '\n');
}
else {
// Non-TTY output
console.log(`\n[${result.success ? 'SUCCESS' : 'FAILED'}] ${result.message}`);
console.log(`[DURATION] ${duration}ms`);
if (result.entityCount !== undefined) {
console.log(`[ENTITIES] ${result.entityCount}`);
}
if (result.warnings && result.warnings.length > 0) {
result.warnings.forEach(w => console.log(`[WARNING] ${w}`));
}
if (result.errors && result.errors.length > 0) {
result.errors.forEach(e => console.log(`[ERROR] ${e}`));
}
}
}
/**
* Format duration in human-readable format
*/
formatDuration(ms) {
if (ms < 1000)
return `${ms}ms`;
if (ms < 60000)
return `${(ms / 1000).toFixed(1)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}m ${seconds}s`;
}
/**
* Show a spinner for indeterminate progress
*/
showSpinner(message) {
if (!this.isTTY) {
console.log(`[PROCESSING] ${message}`);
return () => { }; // No-op for non-TTY
}
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let i = 0;
const interval = setInterval(() => {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(chalk.cyan(`${frames[i]} ${message}`));
i = (i + 1) % frames.length;
}, 80);
return () => {
clearInterval(interval);
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
};
}
}
/**
* Validation-specific progress reporter
*/
export class ValidationProgressReporter extends ProgressReporter {
validationErrors = new Map();
validationWarnings = new Map();
/**
* Report validation progress
*/
reportValidation(stepId, status, message) {
if (status === 'validating') {
if (this.isTTY) {
process.stdout.write(chalk.gray(` • Validating ${stepId}...`));
}
else {
console.log(`[VALIDATING] ${stepId}`);
}
}
else {
if (this.isTTY) {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
switch (status) {
case 'passed':
console.log(chalk.green(` ✓ ${stepId}`));
break;
case 'failed':
console.log(chalk.red(` ✗ ${stepId}: ${message}`));
break;
case 'warning':
console.log(chalk.yellow(` ⚠ ${stepId}: ${message}`));
break;
}
}
else {
console.log(`[${status.toUpperCase()}] ${stepId}${message ? `: ${message}` : ''}`);
}
}
// Track errors and warnings
if (status === 'failed' && message) {
if (!this.validationErrors.has(stepId)) {
this.validationErrors.set(stepId, []);
}
this.validationErrors.get(stepId).push(message);
}
else if (status === 'warning' && message) {
if (!this.validationWarnings.has(stepId)) {
this.validationWarnings.set(stepId, []);
}
this.validationWarnings.get(stepId).push(message);
}
}
/**
* Get validation summary
*/
getValidationSummary() {
const errors = [];
const warnings = [];
this.validationErrors.forEach((messages, stepId) => {
messages.forEach(msg => errors.push(`${stepId}: ${msg}`));
});
this.validationWarnings.forEach((messages, stepId) => {
messages.forEach(msg => warnings.push(`${stepId}: ${msg}`));
});
return { errors, warnings };
}
}
/**
* Execution-specific progress reporter
*/
export class ExecutionProgressReporter extends ProgressReporter {
executedSteps = [];
/**
* Report step execution
*/
reportStepExecution(stepId, entityType, status, duration) {
if (status === 'starting') {
if (this.isTTY) {
console.log(chalk.cyan(`\n▶️ Executing step: ${stepId}`));
console.log(chalk.gray(` Creating ${entityType}...`));
}
else {
console.log(`[EXECUTING] ${stepId} (${entityType})`);
}
}
else {
const success = status === 'completed';
if (duration !== undefined) {
this.executedSteps.push({ stepId, entityType, duration, success });
}
if (this.isTTY) {
if (success) {
console.log(chalk.green(` ✓ Completed in ${this.formatDuration(duration || 0)}`));
}
else {
console.log(chalk.red(` ✗ Failed after ${this.formatDuration(duration || 0)}`));
}
}
else {
console.log(`[${status.toUpperCase()}] ${stepId} (${duration}ms)`);
}
}
}
/**
* Get execution summary
*/
getExecutionSummary() {
const totalSteps = this.executedSteps.length;
const successfulSteps = this.executedSteps.filter(s => s.success).length;
const totalDuration = this.executedSteps.reduce((sum, s) => sum + s.duration, 0);
return { totalSteps, successfulSteps, totalDuration };
}
}
//# sourceMappingURL=ProgressReporter.js.map