@xec-sh/core
Version:
Universal shell execution engine
318 lines • 10.3 kB
JavaScript
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