UNPKG

@xec-sh/core

Version:

Universal shell execution engine

318 lines 10.3 kB
export class ProgressReporter { constructor(options = {}) { this.options = options; this.lineCount = 0; this.byteCount = 0; this.options = { enabled: true, updateInterval: 500, reportLines: false, ...options }; } start(message) { if (!this.options.enabled) return; this.startTime = Date.now(); this.lastUpdate = this.startTime; this.lineCount = 0; this.byteCount = 0; this.emit({ type: 'start', message: message || 'Starting command execution...' }); } reportOutput(data) { if (!this.options.enabled) return; const dataStr = data.toString(); const lines = dataStr.split('\n').length - 1; this.lineCount += lines; this.byteCount += dataStr.length; if (this.options.reportLines && lines > 0) { this.progress(`Processed ${this.lineCount} lines`); } this.maybeUpdateProgress(); } progress(message, current, total) { if (!this.options.enabled) return; const now = Date.now(); const duration = this.startTime ? now - this.startTime : 0; const event = { type: 'progress', message, current, total, duration }; if (current !== undefined && total !== undefined && total > 0) { event.percentage = (current / total) * 100; if (duration > 0) { event.rate = current / (duration / 1000); if (event.rate > 0) { const remaining = total - current; event.eta = remaining / event.rate * 1000; } } } this.emit(event); this.lastUpdate = now; } complete(message) { if (!this.options.enabled) return; const duration = this.startTime ? Date.now() - this.startTime : 0; this.emit({ type: 'complete', message: message || 'Command completed successfully', duration, data: { linesProcessed: this.lineCount, bytesProcessed: this.byteCount } }); } error(error, message) { if (!this.options.enabled) return; const duration = this.startTime ? Date.now() - this.startTime : 0; this.emit({ type: 'error', message: message || `Command failed: ${error.message}`, duration, data: { error } }); } maybeUpdateProgress() { if (!this.lastUpdate || !this.options.updateInterval) return; const now = Date.now(); if (now - this.lastUpdate >= this.options.updateInterval) { if (this.byteCount > 0) { this.progress(`Processed ${this.formatBytes(this.byteCount)}`); } } } emit(event) { if (this.options.prefix && event.message) { event.message = `${this.options.prefix}: ${event.message}`; } if (this.options.onProgress) { this.options.onProgress(event); } else { this.defaultProgressHandler(event); } } defaultProgressHandler(event) { switch (event.type) { case 'start': console.log(`▶ ${event.message}`); break; case 'progress': if (event.percentage !== undefined) { const bar = this.createProgressBar(event.percentage); console.log(`${bar} ${event.percentage.toFixed(1)}% ${event.message}`); } else { console.log(`⏳ ${event.message}`); } break; case 'complete': const durationStr = event.duration ? ` (${this.formatDuration(event.duration)})` : ''; console.log(`✅ ${event.message}${durationStr}`); break; case 'error': console.error(`❌ ${event.message}`); break; } } createProgressBar(percentage, width = 20) { const filled = Math.round((percentage / 100) * width); const empty = width - filled; return `[${'█'.repeat(filled)}${' '.repeat(empty)}]`; } formatBytes(bytes) { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } formatDuration(ms) { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`; return `${(ms / 3600000).toFixed(1)}h`; } } export function createProgressReporter(options) { return new ProgressReporter(options); } export class ProgressBar { constructor(options = {}) { this.options = options; this.current = 0; this.lastRender = 0; this.options = { total: 100, width: 40, complete: '=', incomplete: ' ', head: '>', format: ':bar :percent :etas', renderThrottle: 16, ...options }; this.startTime = Date.now(); } update(value) { this.current = Math.min(value, this.options.total || 100); this.render(); } increment(delta = 1) { this.update(this.current + delta); } complete() { this.update(this.options.total || 100); process.stdout.write('\n'); } render() { const now = Date.now(); if (now - this.lastRender < (this.options.renderThrottle || 16)) { return; } this.lastRender = now; const total = this.options.total || 100; const percent = total > 0 ? (this.current / total) * 100 : 100; const filled = Math.round((percent / 100) * (this.options.width || 40)); const empty = (this.options.width || 40) - filled; let bar = this.options.complete?.repeat(filled) || ''; if (filled < (this.options.width || 40)) { bar += this.options.head || ''; bar += this.options.incomplete?.repeat(Math.max(0, empty - 1)) || ''; } let output = this.options.format || ':bar :percent :etas'; output = output.replace(':bar', bar); output = output.replace(':percent', `${percent.toFixed(0)}%`); output = output.replace(':current', String(this.current)); output = output.replace(':total', String(total)); if (this.startTime && percent > 0 && percent < 100) { const elapsed = now - this.startTime; const eta = (elapsed / percent) * (100 - percent); output = output.replace(':etas', this.formatTime(eta / 1000)); output = output.replace(':eta', this.formatTime(eta / 1000)); } else { output = output.replace(':etas', '0s'); output = output.replace(':eta', '0s'); } if (this.options.tokens) { for (const [key, value] of Object.entries(this.options.tokens)) { output = output.replace(`:${key}`, value); } } process.stdout.write('\r' + output); } formatTime(seconds) { if (seconds < 60) return `${Math.round(seconds)}s`; if (seconds < 3600) return `${Math.round(seconds / 60)}m`; return `${Math.round(seconds / 3600)}h`; } } export function createProgressBar(options) { return new ProgressBar(options); } export class Spinner { constructor(options = {}) { this.options = options; this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; this.frameIndex = 0; this.options = { interval: 80, ...options }; } start(text) { this.stop(); if (text) this.options.text = text; this.intervalId = setInterval(() => { const frame = this.frames[this.frameIndex]; const output = `\r${frame} ${this.options.text || ''}`; process.stdout.write(output); this.frameIndex = (this.frameIndex + 1) % this.frames.length; }, this.options.interval || 80); } stop() { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = undefined; process.stdout.write('\r\x1b[K'); } } succeed(text) { this.stop(); process.stdout.write(`✓ ${text || this.options.text || ''}\n`); } fail(text) { this.stop(); process.stdout.write(`✗ ${text || this.options.text || ''}\n`); } info(text) { this.stop(); process.stdout.write(`ℹ ${text || this.options.text || ''}\n`); } warn(text) { this.stop(); process.stdout.write(`⚠ ${text || this.options.text || ''}\n`); } } export function createSpinner(options) { return new Spinner(options); } export class MultiProgress { constructor() { this.bars = new Map(); this.spinners = new Map(); this.lineCount = 0; } create(id, options) { const bar = new ProgressBar(options); this.bars.set(id, bar); return bar; } createSpinner(id, options) { const spinner = new Spinner(options); this.spinners.set(id, spinner); return spinner; } remove(id) { const bar = this.bars.get(id); if (bar) { this.bars.delete(id); } const spinner = this.spinners.get(id); if (spinner) { spinner.stop(); this.spinners.delete(id); } } clear() { for (const spinner of this.spinners.values()) { spinner.stop(); } this.bars.clear(); this.spinners.clear(); } } //# sourceMappingURL=progress.js.map