qraft
Version:
A powerful CLI tool to qraft structured project setups from GitHub template repositories
297 lines • 11.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProgressIndicator = void 0;
const chalk_1 = __importDefault(require("chalk"));
class ProgressIndicator {
constructor(options = {}) {
this.steps = [];
this.currentStepIndex = -1;
this.startTime = 0;
this.spinnerIndex = 0;
this.options = {
showPercentage: true,
showElapsed: true,
showETA: true,
showStepDetails: true,
clearOnComplete: false,
spinnerChars: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
updateInterval: 100,
...options
};
}
addStep(step) {
this.steps.push({
...step,
status: 'pending',
weight: step.weight || 1
});
}
addSteps(steps) {
steps.forEach(step => this.addStep(step));
}
start() {
this.startTime = Date.now();
this.currentStepIndex = -1;
this.render();
this.startSpinner();
}
nextStep() {
// Complete current step if running
if (this.currentStepIndex >= 0 && this.currentStepIndex < this.steps.length) {
const currentStep = this.steps[this.currentStepIndex];
if (currentStep.status === 'running') {
currentStep.status = 'completed';
currentStep.endTime = Date.now();
}
}
// Move to next step
this.currentStepIndex++;
if (this.currentStepIndex < this.steps.length) {
const nextStep = this.steps[this.currentStepIndex];
nextStep.status = 'running';
nextStep.startTime = Date.now();
}
this.render();
}
completeStep(stepId, success = true) {
let step;
if (stepId) {
step = this.steps.find(s => s.id === stepId);
}
else if (this.currentStepIndex >= 0 && this.currentStepIndex < this.steps.length) {
step = this.steps[this.currentStepIndex];
}
if (step) {
step.status = success ? 'completed' : 'failed';
step.endTime = Date.now();
this.render();
}
}
failStep(stepId, error) {
let step;
if (stepId) {
step = this.steps.find(s => s.id === stepId);
}
else if (this.currentStepIndex >= 0 && this.currentStepIndex < this.steps.length) {
step = this.steps[this.currentStepIndex];
}
if (step) {
step.status = 'failed';
step.endTime = Date.now();
step.error = error;
this.render();
}
}
skipStep(stepId, reason) {
let step;
if (stepId) {
step = this.steps.find(s => s.id === stepId);
}
else if (this.currentStepIndex >= 0 && this.currentStepIndex < this.steps.length) {
step = this.steps[this.currentStepIndex];
}
if (step) {
step.status = 'skipped';
step.endTime = Date.now();
step.error = reason;
this.render();
}
}
complete() {
// Complete any remaining running step
if (this.currentStepIndex >= 0 && this.currentStepIndex < this.steps.length) {
const currentStep = this.steps[this.currentStepIndex];
if (currentStep.status === 'running') {
currentStep.status = 'completed';
currentStep.endTime = Date.now();
}
}
this.stopSpinner();
this.render();
if (this.options.clearOnComplete) {
this.clear();
}
}
render() {
if (!process.stdout.isTTY)
return;
this.clear();
const progress = this.calculateProgress();
const elapsed = Date.now() - this.startTime;
const eta = this.calculateETA(progress, elapsed);
// Progress bar
const barWidth = 30;
const filledWidth = Math.round(barWidth * progress);
const emptyWidth = barWidth - filledWidth;
const progressBar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
let output = '';
// Main progress line
output += chalk_1.default.cyan('Progress: ');
output += chalk_1.default.green(`[${progressBar}]`);
if (this.options.showPercentage) {
output += chalk_1.default.yellow(` ${(progress * 100).toFixed(1)}%`);
}
if (this.options.showElapsed) {
output += chalk_1.default.gray(` | Elapsed: ${this.formatTime(elapsed)}`);
}
if (this.options.showETA && eta > 0) {
output += chalk_1.default.gray(` | ETA: ${this.formatTime(eta)}`);
}
output += '\n';
// Current step with spinner
if (this.currentStepIndex >= 0 && this.currentStepIndex < this.steps.length) {
const currentStep = this.steps[this.currentStepIndex];
const spinner = currentStep.status === 'running' ? (this.options.spinnerChars?.[this.spinnerIndex] || '') : '';
output += chalk_1.default.blue(`${spinner} ${currentStep.name}`);
if (currentStep.description) {
output += chalk_1.default.gray(` - ${currentStep.description}`);
}
output += '\n';
}
// Step details
if (this.options.showStepDetails) {
this.steps.forEach((step, index) => {
const icon = this.getStepIcon(step.status);
const color = this.getStepColor(step.status);
const prefix = index === this.currentStepIndex ? '→ ' : ' ';
output += `${prefix}${color(icon)} ${color(step.name)}`;
if (step.status === 'failed' && step.error) {
output += chalk_1.default.red(` (${step.error})`);
}
else if (step.status === 'skipped' && step.error) {
output += chalk_1.default.yellow(` (${step.error})`);
}
output += '\n';
});
}
process.stdout.write(output);
}
calculateProgress() {
const totalWeight = this.steps.reduce((sum, step) => sum + (step.weight || 1), 0);
const completedWeight = this.steps
.filter(step => step.status === 'completed')
.reduce((sum, step) => sum + (step.weight || 1), 0);
// Add partial progress for current running step
let runningWeight = 0;
if (this.currentStepIndex >= 0 && this.currentStepIndex < this.steps.length) {
const currentStep = this.steps[this.currentStepIndex];
if (currentStep.status === 'running' && currentStep.startTime) {
// Assume 50% progress for running step (could be more sophisticated)
runningWeight = (currentStep.weight || 1) * 0.5;
}
}
return totalWeight > 0 ? (completedWeight + runningWeight) / totalWeight : 0;
}
calculateETA(progress, elapsed) {
if (progress <= 0)
return 0;
const totalEstimated = elapsed / progress;
return Math.max(0, totalEstimated - elapsed);
}
formatTime(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
}
else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
else {
return `${seconds}s`;
}
}
getStepIcon(status) {
switch (status) {
case 'pending': return '○';
case 'running': return '●';
case 'completed': return '✓';
case 'failed': return '✗';
case 'skipped': return '⊘';
default: return '○';
}
}
getStepColor(status) {
switch (status) {
case 'pending': return chalk_1.default.gray;
case 'running': return chalk_1.default.blue;
case 'completed': return chalk_1.default.green;
case 'failed': return chalk_1.default.red;
case 'skipped': return chalk_1.default.yellow;
default: return chalk_1.default.gray;
}
}
startSpinner() {
this.intervalId = setInterval(() => {
this.spinnerIndex = (this.spinnerIndex + 1) % (this.options.spinnerChars?.length || 1);
this.render();
}, this.options.updateInterval);
}
stopSpinner() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
}
clear() {
if (!process.stdout.isTTY)
return;
// Clear previous output
const linesToClear = this.options.showStepDetails ? this.steps.length + 3 : 3;
for (let i = 0; i < linesToClear; i++) {
process.stdout.write('\x1b[1A\x1b[2K'); // Move up and clear line
}
process.stdout.write('\x1b[1G'); // Move to beginning of line
}
// Get summary of completed operations
getSummary() {
const total = this.steps.length;
const completed = this.steps.filter(s => s.status === 'completed').length;
const failed = this.steps.filter(s => s.status === 'failed').length;
const skipped = this.steps.filter(s => s.status === 'skipped').length;
const duration = Date.now() - this.startTime;
const success = failed === 0 && completed + skipped === total;
return { total, completed, failed, skipped, duration, success };
}
// Display final summary
displaySummary() {
const summary = this.getSummary();
console.log(chalk_1.default.cyan('\n📊 Operation Summary:'));
console.log(chalk_1.default.gray('─'.repeat(40)));
console.log(`${chalk_1.default.yellow('Total Steps:')} ${summary.total}`);
console.log(`${chalk_1.default.green('Completed:')} ${summary.completed}`);
if (summary.failed > 0) {
console.log(`${chalk_1.default.red('Failed:')} ${summary.failed}`);
}
if (summary.skipped > 0) {
console.log(`${chalk_1.default.yellow('Skipped:')} ${summary.skipped}`);
}
console.log(`${chalk_1.default.blue('Duration:')} ${this.formatTime(summary.duration)}`);
console.log(`${chalk_1.default.cyan('Status:')} ${summary.success ? chalk_1.default.green('Success') : chalk_1.default.red('Failed')}`);
console.log(chalk_1.default.gray('─'.repeat(40)));
}
// Test helper methods
getSteps() {
return [...this.steps];
}
getCurrentStepIndex() {
return this.currentStepIndex;
}
getStartTime() {
return this.startTime;
}
// Simulate progress for testing
simulateProgress(stepId, _progressPercent) {
const step = this.steps.find(s => s.id === stepId);
if (step && step.status === 'running') {
// This is a simplified simulation - in real use, progress would be tracked differently
this.render();
}
}
}
exports.ProgressIndicator = ProgressIndicator;
//# sourceMappingURL=progressIndicator.js.map