@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -25
- package/README.md +1166 -1166
- package/actions/autopublish.old.js +293 -293
- package/actions/config.js +182 -182
- package/actions/create.js +466 -466
- package/actions/help.js +164 -164
- package/actions/iris/buildStage.js +874 -874
- package/actions/iris/delete.js +256 -256
- package/actions/iris/dev.js +391 -391
- package/actions/iris/index.js +6 -6
- package/actions/iris/link.js +375 -375
- package/actions/iris/recover.js +268 -268
- package/actions/main.js +80 -80
- package/actions/publish.js +1420 -1420
- package/actions/pull.js +684 -684
- package/actions/setup.js +148 -148
- package/actions/status.js +17 -17
- package/actions/update.js +248 -248
- package/bin/magentrix.js +393 -393
- package/package.json +55 -55
- package/utils/assetPaths.js +158 -158
- package/utils/autopublishLock.js +77 -77
- package/utils/cacher.js +206 -206
- package/utils/cli/checkInstanceUrl.js +76 -74
- package/utils/cli/helpers/compare.js +282 -282
- package/utils/cli/helpers/ensureApiKey.js +63 -63
- package/utils/cli/helpers/ensureCredentials.js +68 -68
- package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
- package/utils/cli/writeRecords.js +262 -262
- package/utils/compare.js +135 -135
- package/utils/compress.js +17 -17
- package/utils/config.js +527 -527
- package/utils/debug.js +144 -144
- package/utils/diagnostics/testPublishLogic.js +96 -96
- package/utils/diff.js +49 -49
- package/utils/downloadAssets.js +291 -291
- package/utils/filetag.js +115 -115
- package/utils/hash.js +14 -14
- package/utils/iris/backup.js +411 -411
- package/utils/iris/builder.js +541 -541
- package/utils/iris/config-reader.js +664 -664
- package/utils/iris/deleteHelper.js +150 -150
- package/utils/iris/errors.js +537 -537
- package/utils/iris/linker.js +601 -601
- package/utils/iris/lock.js +360 -360
- package/utils/iris/validation.js +360 -360
- package/utils/iris/validator.js +281 -281
- package/utils/iris/zipper.js +248 -248
- package/utils/logger.js +291 -291
- package/utils/magentrix/api/assets.js +220 -220
- package/utils/magentrix/api/auth.js +107 -107
- package/utils/magentrix/api/createEntity.js +61 -61
- package/utils/magentrix/api/deleteEntity.js +55 -55
- package/utils/magentrix/api/iris.js +251 -251
- package/utils/magentrix/api/meqlQuery.js +36 -36
- package/utils/magentrix/api/retrieveEntity.js +86 -86
- package/utils/magentrix/api/updateEntity.js +66 -66
- package/utils/magentrix/fetch.js +168 -168
- package/utils/merge.js +22 -22
- package/utils/permissionError.js +70 -70
- package/utils/preferences.js +40 -40
- package/utils/progress.js +469 -469
- package/utils/spinner.js +43 -43
- package/utils/template.js +52 -52
- package/utils/updateFileBase.js +121 -121
- package/utils/workspaces.js +108 -108
- package/vars/config.js +11 -11
- package/vars/global.js +50 -50
package/utils/progress.js
CHANGED
|
@@ -1,469 +1,469 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Comprehensive progress tracker for multi-step operations with sub-progress.
|
|
5
|
-
*
|
|
6
|
-
* Features:
|
|
7
|
-
* - Multiple step tracking
|
|
8
|
-
* - Progress bars for individual steps
|
|
9
|
-
* - Nested progress (e.g., batches within a step)
|
|
10
|
-
* - Time tracking
|
|
11
|
-
* - Dynamic message updates
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* const progress = new ProgressTracker('Pull Operation');
|
|
15
|
-
* progress.addStep('auth', 'Authenticating...');
|
|
16
|
-
* progress.addStep('download', 'Downloading files...');
|
|
17
|
-
* progress.addStep('process', 'Processing files...');
|
|
18
|
-
*
|
|
19
|
-
* progress.start();
|
|
20
|
-
* progress.startStep('auth');
|
|
21
|
-
* // ... do auth work
|
|
22
|
-
* progress.completeStep('auth', 'Authenticated');
|
|
23
|
-
*
|
|
24
|
-
* progress.startStep('download');
|
|
25
|
-
* progress.updateProgress('download', 0, 100);
|
|
26
|
-
* // ... download files
|
|
27
|
-
* progress.updateProgress('download', 50, 100);
|
|
28
|
-
* progress.completeStep('download', 'Downloaded 100 files');
|
|
29
|
-
*
|
|
30
|
-
* progress.finish();
|
|
31
|
-
*/
|
|
32
|
-
export class ProgressTracker {
|
|
33
|
-
constructor(title = 'Operation') {
|
|
34
|
-
this.title = title;
|
|
35
|
-
this.steps = new Map();
|
|
36
|
-
this.currentStep = null;
|
|
37
|
-
this.startTime = null;
|
|
38
|
-
this.spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
39
|
-
this.spinnerIndex = 0;
|
|
40
|
-
this.interval = null;
|
|
41
|
-
this.lastRenderLength = 0;
|
|
42
|
-
this.isFinished = false;
|
|
43
|
-
this.issues = []; // Collected issues/warnings to display at end
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Add a step to track
|
|
48
|
-
* @param {string} id - Unique identifier for the step
|
|
49
|
-
* @param {string} label - Display label for the step
|
|
50
|
-
* @param {object} options - Optional configuration
|
|
51
|
-
* @param {boolean} options.hasProgress - Whether this step has progress (0-100%)
|
|
52
|
-
*/
|
|
53
|
-
addStep(id, label, options = {}) {
|
|
54
|
-
this.steps.set(id, {
|
|
55
|
-
id,
|
|
56
|
-
label,
|
|
57
|
-
status: 'pending', // pending, active, completed, failed
|
|
58
|
-
hasProgress: options.hasProgress || false,
|
|
59
|
-
progress: { current: 0, total: 100 },
|
|
60
|
-
message: '',
|
|
61
|
-
startTime: null,
|
|
62
|
-
endTime: null,
|
|
63
|
-
error: null
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Start the progress tracker
|
|
69
|
-
*/
|
|
70
|
-
start() {
|
|
71
|
-
this.startTime = Date.now();
|
|
72
|
-
this.isFinished = false;
|
|
73
|
-
this.render();
|
|
74
|
-
this.startSpinner();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Start a specific step
|
|
79
|
-
* @param {string} id - Step identifier
|
|
80
|
-
* @param {string} message - Optional custom message
|
|
81
|
-
*/
|
|
82
|
-
startStep(id, message = '') {
|
|
83
|
-
const step = this.steps.get(id);
|
|
84
|
-
if (!step) {
|
|
85
|
-
throw new Error(`Step '${id}' not found`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Complete previous step if any
|
|
89
|
-
if (this.currentStep && this.steps.get(this.currentStep).status === 'active') {
|
|
90
|
-
this.completeStep(this.currentStep);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
step.status = 'active';
|
|
94
|
-
step.startTime = Date.now();
|
|
95
|
-
step.message = message || step.label;
|
|
96
|
-
this.currentStep = id;
|
|
97
|
-
this.render();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Update progress for a step
|
|
102
|
-
* @param {string} id - Step identifier
|
|
103
|
-
* @param {number} current - Current progress value
|
|
104
|
-
* @param {number} total - Total progress value
|
|
105
|
-
* @param {string} message - Optional message to display
|
|
106
|
-
*/
|
|
107
|
-
updateProgress(id, current, total, message = '') {
|
|
108
|
-
const step = this.steps.get(id);
|
|
109
|
-
if (!step) return;
|
|
110
|
-
|
|
111
|
-
step.progress.current = current;
|
|
112
|
-
step.progress.total = total;
|
|
113
|
-
if (message) {
|
|
114
|
-
step.message = message;
|
|
115
|
-
}
|
|
116
|
-
this.render();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Complete a step
|
|
121
|
-
* @param {string} id - Step identifier
|
|
122
|
-
* @param {string} message - Optional completion message
|
|
123
|
-
*/
|
|
124
|
-
completeStep(id, message = '') {
|
|
125
|
-
const step = this.steps.get(id);
|
|
126
|
-
if (!step) return;
|
|
127
|
-
|
|
128
|
-
step.status = 'completed';
|
|
129
|
-
step.endTime = Date.now();
|
|
130
|
-
if (message) {
|
|
131
|
-
step.message = message;
|
|
132
|
-
}
|
|
133
|
-
this.render();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Mark a step as failed
|
|
138
|
-
* @param {string} id - Step identifier
|
|
139
|
-
* @param {string} error - Error message
|
|
140
|
-
*/
|
|
141
|
-
failStep(id, error) {
|
|
142
|
-
const step = this.steps.get(id);
|
|
143
|
-
if (!step) return;
|
|
144
|
-
|
|
145
|
-
step.status = 'failed';
|
|
146
|
-
step.endTime = Date.now();
|
|
147
|
-
step.error = error;
|
|
148
|
-
this.render();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Add an issue/warning to be displayed at the end
|
|
153
|
-
* @param {string} type - 'error' | 'warning' | 'info'
|
|
154
|
-
* @param {string} message - The issue message
|
|
155
|
-
* @param {string} [hint] - Optional hint for resolution
|
|
156
|
-
*/
|
|
157
|
-
addIssue(type, message, hint = null) {
|
|
158
|
-
this.issues.push({ type, message, hint });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Check if there are any issues collected
|
|
163
|
-
* @returns {boolean}
|
|
164
|
-
*/
|
|
165
|
-
hasIssues() {
|
|
166
|
-
return this.issues.length > 0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Get issues by type
|
|
171
|
-
* @param {string} type - 'error' | 'warning' | 'info'
|
|
172
|
-
* @returns {Array}
|
|
173
|
-
*/
|
|
174
|
-
getIssues(type = null) {
|
|
175
|
-
if (type) {
|
|
176
|
-
return this.issues.filter(i => i.type === type);
|
|
177
|
-
}
|
|
178
|
-
return this.issues;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Update the message for the current step
|
|
183
|
-
* @param {string} message - New message
|
|
184
|
-
*/
|
|
185
|
-
updateMessage(message) {
|
|
186
|
-
if (!this.currentStep) return;
|
|
187
|
-
const step = this.steps.get(this.currentStep);
|
|
188
|
-
if (step) {
|
|
189
|
-
step.message = message;
|
|
190
|
-
this.render();
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Render the progress display
|
|
196
|
-
*/
|
|
197
|
-
render() {
|
|
198
|
-
if (this.isFinished) return;
|
|
199
|
-
|
|
200
|
-
// Clear previous render
|
|
201
|
-
if (this.lastRenderLength > 0) {
|
|
202
|
-
for (let i = 0; i < this.lastRenderLength; i++) {
|
|
203
|
-
process.stdout.write('\x1B[1A\x1B[2K'); // Move up and clear line
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const lines = [];
|
|
208
|
-
|
|
209
|
-
// Title with elapsed time
|
|
210
|
-
const elapsed = this.formatDuration(Date.now() - this.startTime);
|
|
211
|
-
lines.push(chalk.bold.blue(`\n${this.title}`) + chalk.gray(` (${elapsed})`));
|
|
212
|
-
lines.push('');
|
|
213
|
-
|
|
214
|
-
// Render each step
|
|
215
|
-
for (const [id, step] of this.steps) {
|
|
216
|
-
const prefix = this.getStepPrefix(step);
|
|
217
|
-
const label = step.status === 'active'
|
|
218
|
-
? chalk.cyan(step.label)
|
|
219
|
-
: step.status === 'completed'
|
|
220
|
-
? chalk.gray(step.label)
|
|
221
|
-
: step.status === 'failed'
|
|
222
|
-
? chalk.red(step.label)
|
|
223
|
-
: chalk.gray(step.label);
|
|
224
|
-
|
|
225
|
-
let line = `${prefix} ${label}`;
|
|
226
|
-
|
|
227
|
-
// Add progress bar if applicable
|
|
228
|
-
if (step.hasProgress && step.status === 'active' && step.progress.total > 0) {
|
|
229
|
-
const percentage = Math.floor((step.progress.current / step.progress.total) * 100);
|
|
230
|
-
const progressBar = this.renderProgressBar(percentage);
|
|
231
|
-
line += ` ${progressBar} ${percentage}%`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Add message or duration
|
|
235
|
-
if (step.message && step.status === 'active') {
|
|
236
|
-
line += chalk.gray(` - ${step.message}`);
|
|
237
|
-
} else if (step.status === 'completed' && step.endTime && step.startTime) {
|
|
238
|
-
const duration = this.formatDuration(step.endTime - step.startTime);
|
|
239
|
-
line += chalk.gray(` (${duration})`);
|
|
240
|
-
if (step.message) {
|
|
241
|
-
line += chalk.gray(` - ${step.message}`);
|
|
242
|
-
}
|
|
243
|
-
} else if (step.status === 'failed' && step.error) {
|
|
244
|
-
line += chalk.red(` - ${step.error}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
lines.push(line);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
lines.push(''); // Empty line at the end
|
|
251
|
-
|
|
252
|
-
this.lastRenderLength = lines.length;
|
|
253
|
-
process.stdout.write(lines.join('\n'));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Get the prefix icon for a step
|
|
258
|
-
*/
|
|
259
|
-
getStepPrefix(step) {
|
|
260
|
-
switch (step.status) {
|
|
261
|
-
case 'pending':
|
|
262
|
-
return chalk.gray('◯');
|
|
263
|
-
case 'active':
|
|
264
|
-
return chalk.cyan(this.spinnerChars[this.spinnerIndex]);
|
|
265
|
-
case 'completed':
|
|
266
|
-
return chalk.green('✓');
|
|
267
|
-
case 'failed':
|
|
268
|
-
return chalk.red('✗');
|
|
269
|
-
default:
|
|
270
|
-
return chalk.gray('◯');
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Render a progress bar
|
|
276
|
-
* @param {number} percentage - Progress percentage (0-100)
|
|
277
|
-
*/
|
|
278
|
-
renderProgressBar(percentage) {
|
|
279
|
-
const width = 20;
|
|
280
|
-
// Clamp percentage between 0 and 100 to prevent negative values
|
|
281
|
-
const clampedPercentage = Math.max(0, Math.min(100, percentage));
|
|
282
|
-
const filled = Math.floor((clampedPercentage / 100) * width);
|
|
283
|
-
const empty = width - filled;
|
|
284
|
-
|
|
285
|
-
const bar = chalk.cyan('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
286
|
-
return `[${bar}]`;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Format duration in ms to human readable format
|
|
291
|
-
*/
|
|
292
|
-
formatDuration(ms) {
|
|
293
|
-
if (ms < 1000) return `${ms}ms`;
|
|
294
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
295
|
-
const minutes = Math.floor(ms / 60000);
|
|
296
|
-
const seconds = Math.floor((ms % 60000) / 1000);
|
|
297
|
-
return `${minutes}m ${seconds}s`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Start the spinner animation
|
|
302
|
-
*/
|
|
303
|
-
startSpinner() {
|
|
304
|
-
this.interval = setInterval(() => {
|
|
305
|
-
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length;
|
|
306
|
-
this.render();
|
|
307
|
-
}, 80);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Stop the spinner animation
|
|
312
|
-
*/
|
|
313
|
-
stopSpinner() {
|
|
314
|
-
if (this.interval) {
|
|
315
|
-
clearInterval(this.interval);
|
|
316
|
-
this.interval = null;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Finish the progress tracker
|
|
322
|
-
* @param {string} message - Optional final message
|
|
323
|
-
* @param {object} options - Optional configuration
|
|
324
|
-
* @param {boolean} options.showIssues - Whether to show collected issues (default: false)
|
|
325
|
-
*/
|
|
326
|
-
finish(message = '', options = {}) {
|
|
327
|
-
const { showIssues = false } = options;
|
|
328
|
-
|
|
329
|
-
this.stopSpinner();
|
|
330
|
-
this.isFinished = true;
|
|
331
|
-
|
|
332
|
-
// Complete any active steps
|
|
333
|
-
if (this.currentStep && this.steps.get(this.currentStep).status === 'active') {
|
|
334
|
-
this.completeStep(this.currentStep);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
this.render();
|
|
338
|
-
|
|
339
|
-
// Print summary
|
|
340
|
-
const totalTime = this.formatDuration(Date.now() - this.startTime);
|
|
341
|
-
const completed = Array.from(this.steps.values()).filter(s => s.status === 'completed').length;
|
|
342
|
-
const failed = Array.from(this.steps.values()).filter(s => s.status === 'failed').length;
|
|
343
|
-
|
|
344
|
-
if (message) {
|
|
345
|
-
console.log(chalk.bold.green(`\n${message}`));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
console.log(chalk.gray(`Total time: ${totalTime}`));
|
|
349
|
-
|
|
350
|
-
if (failed > 0) {
|
|
351
|
-
console.log(chalk.red(`✗ ${failed} step(s) failed`));
|
|
352
|
-
} else {
|
|
353
|
-
console.log(chalk.green(`✓ All ${completed} step(s) completed successfully`));
|
|
354
|
-
}
|
|
355
|
-
console.log('');
|
|
356
|
-
|
|
357
|
-
// Only display collected issues if explicitly requested
|
|
358
|
-
if (showIssues && this.issues.length > 0) {
|
|
359
|
-
this.displayIssues();
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Display collected issues
|
|
365
|
-
* Can be called manually after finish() if needed
|
|
366
|
-
*/
|
|
367
|
-
displayIssues() {
|
|
368
|
-
if (this.issues.length === 0) return;
|
|
369
|
-
|
|
370
|
-
const errors = this.issues.filter(i => i.type === 'error');
|
|
371
|
-
const warnings = this.issues.filter(i => i.type === 'warning');
|
|
372
|
-
const infos = this.issues.filter(i => i.type === 'info');
|
|
373
|
-
|
|
374
|
-
console.log(chalk.bold('Issues Summary:'));
|
|
375
|
-
|
|
376
|
-
if (errors.length > 0) {
|
|
377
|
-
console.log(chalk.red(` ✗ ${errors.length} error(s)`));
|
|
378
|
-
errors.forEach((issue, i) => {
|
|
379
|
-
console.log(chalk.red(` ${i + 1}. ${issue.message}`));
|
|
380
|
-
if (issue.hint) {
|
|
381
|
-
console.log(chalk.yellow(` ${issue.hint}`));
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (warnings.length > 0) {
|
|
387
|
-
console.log(chalk.yellow(` ⚠ ${warnings.length} warning(s)`));
|
|
388
|
-
warnings.forEach((issue, i) => {
|
|
389
|
-
console.log(chalk.yellow(` ${i + 1}. ${issue.message}`));
|
|
390
|
-
if (issue.hint) {
|
|
391
|
-
console.log(chalk.gray(` ${issue.hint}`));
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (infos.length > 0) {
|
|
397
|
-
console.log(chalk.cyan(` ℹ ${infos.length} info(s)`));
|
|
398
|
-
infos.forEach((issue, i) => {
|
|
399
|
-
console.log(chalk.cyan(` ${i + 1}. ${issue.message}`));
|
|
400
|
-
if (issue.hint) {
|
|
401
|
-
console.log(chalk.gray(` ${issue.hint}`));
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
console.log('');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Abort the progress tracker with an error
|
|
411
|
-
* @param {string} error - Error message
|
|
412
|
-
*/
|
|
413
|
-
abort(error) {
|
|
414
|
-
this.stopSpinner();
|
|
415
|
-
this.isFinished = true;
|
|
416
|
-
|
|
417
|
-
if (this.currentStep) {
|
|
418
|
-
this.failStep(this.currentStep, error);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
this.render();
|
|
422
|
-
console.log(chalk.bold.red(`\n✗ ${error}\n`));
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Helper function to create and use a simple progress tracker
|
|
428
|
-
* @param {string} title - Title of the operation
|
|
429
|
-
* @param {Array} steps - Array of step definitions
|
|
430
|
-
* @param {Function} fn - Async function that receives the progress tracker
|
|
431
|
-
* @returns {Promise} Result of the function
|
|
432
|
-
*
|
|
433
|
-
* @example
|
|
434
|
-
* await withProgress('Pull Operation', [
|
|
435
|
-
* { id: 'auth', label: 'Authenticating...' },
|
|
436
|
-
* { id: 'download', label: 'Downloading files...', hasProgress: true },
|
|
437
|
-
* { id: 'process', label: 'Processing files...' }
|
|
438
|
-
* ], async (progress) => {
|
|
439
|
-
* progress.startStep('auth');
|
|
440
|
-
* await authenticate();
|
|
441
|
-
* progress.completeStep('auth');
|
|
442
|
-
*
|
|
443
|
-
* progress.startStep('download');
|
|
444
|
-
* for (let i = 0; i <= 100; i += 10) {
|
|
445
|
-
* progress.updateProgress('download', i, 100);
|
|
446
|
-
* await downloadBatch(i);
|
|
447
|
-
* }
|
|
448
|
-
* progress.completeStep('download');
|
|
449
|
-
* });
|
|
450
|
-
*/
|
|
451
|
-
export async function withProgress(title, steps, fn) {
|
|
452
|
-
const progress = new ProgressTracker(title);
|
|
453
|
-
|
|
454
|
-
// Add all steps
|
|
455
|
-
for (const step of steps) {
|
|
456
|
-
progress.addStep(step.id, step.label, step);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
progress.start();
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
const result = await fn(progress);
|
|
463
|
-
progress.finish();
|
|
464
|
-
return result;
|
|
465
|
-
} catch (error) {
|
|
466
|
-
progress.abort(error.message);
|
|
467
|
-
throw error;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Comprehensive progress tracker for multi-step operations with sub-progress.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Multiple step tracking
|
|
8
|
+
* - Progress bars for individual steps
|
|
9
|
+
* - Nested progress (e.g., batches within a step)
|
|
10
|
+
* - Time tracking
|
|
11
|
+
* - Dynamic message updates
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const progress = new ProgressTracker('Pull Operation');
|
|
15
|
+
* progress.addStep('auth', 'Authenticating...');
|
|
16
|
+
* progress.addStep('download', 'Downloading files...');
|
|
17
|
+
* progress.addStep('process', 'Processing files...');
|
|
18
|
+
*
|
|
19
|
+
* progress.start();
|
|
20
|
+
* progress.startStep('auth');
|
|
21
|
+
* // ... do auth work
|
|
22
|
+
* progress.completeStep('auth', 'Authenticated');
|
|
23
|
+
*
|
|
24
|
+
* progress.startStep('download');
|
|
25
|
+
* progress.updateProgress('download', 0, 100);
|
|
26
|
+
* // ... download files
|
|
27
|
+
* progress.updateProgress('download', 50, 100);
|
|
28
|
+
* progress.completeStep('download', 'Downloaded 100 files');
|
|
29
|
+
*
|
|
30
|
+
* progress.finish();
|
|
31
|
+
*/
|
|
32
|
+
export class ProgressTracker {
|
|
33
|
+
constructor(title = 'Operation') {
|
|
34
|
+
this.title = title;
|
|
35
|
+
this.steps = new Map();
|
|
36
|
+
this.currentStep = null;
|
|
37
|
+
this.startTime = null;
|
|
38
|
+
this.spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
39
|
+
this.spinnerIndex = 0;
|
|
40
|
+
this.interval = null;
|
|
41
|
+
this.lastRenderLength = 0;
|
|
42
|
+
this.isFinished = false;
|
|
43
|
+
this.issues = []; // Collected issues/warnings to display at end
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add a step to track
|
|
48
|
+
* @param {string} id - Unique identifier for the step
|
|
49
|
+
* @param {string} label - Display label for the step
|
|
50
|
+
* @param {object} options - Optional configuration
|
|
51
|
+
* @param {boolean} options.hasProgress - Whether this step has progress (0-100%)
|
|
52
|
+
*/
|
|
53
|
+
addStep(id, label, options = {}) {
|
|
54
|
+
this.steps.set(id, {
|
|
55
|
+
id,
|
|
56
|
+
label,
|
|
57
|
+
status: 'pending', // pending, active, completed, failed
|
|
58
|
+
hasProgress: options.hasProgress || false,
|
|
59
|
+
progress: { current: 0, total: 100 },
|
|
60
|
+
message: '',
|
|
61
|
+
startTime: null,
|
|
62
|
+
endTime: null,
|
|
63
|
+
error: null
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Start the progress tracker
|
|
69
|
+
*/
|
|
70
|
+
start() {
|
|
71
|
+
this.startTime = Date.now();
|
|
72
|
+
this.isFinished = false;
|
|
73
|
+
this.render();
|
|
74
|
+
this.startSpinner();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start a specific step
|
|
79
|
+
* @param {string} id - Step identifier
|
|
80
|
+
* @param {string} message - Optional custom message
|
|
81
|
+
*/
|
|
82
|
+
startStep(id, message = '') {
|
|
83
|
+
const step = this.steps.get(id);
|
|
84
|
+
if (!step) {
|
|
85
|
+
throw new Error(`Step '${id}' not found`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Complete previous step if any
|
|
89
|
+
if (this.currentStep && this.steps.get(this.currentStep).status === 'active') {
|
|
90
|
+
this.completeStep(this.currentStep);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
step.status = 'active';
|
|
94
|
+
step.startTime = Date.now();
|
|
95
|
+
step.message = message || step.label;
|
|
96
|
+
this.currentStep = id;
|
|
97
|
+
this.render();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Update progress for a step
|
|
102
|
+
* @param {string} id - Step identifier
|
|
103
|
+
* @param {number} current - Current progress value
|
|
104
|
+
* @param {number} total - Total progress value
|
|
105
|
+
* @param {string} message - Optional message to display
|
|
106
|
+
*/
|
|
107
|
+
updateProgress(id, current, total, message = '') {
|
|
108
|
+
const step = this.steps.get(id);
|
|
109
|
+
if (!step) return;
|
|
110
|
+
|
|
111
|
+
step.progress.current = current;
|
|
112
|
+
step.progress.total = total;
|
|
113
|
+
if (message) {
|
|
114
|
+
step.message = message;
|
|
115
|
+
}
|
|
116
|
+
this.render();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Complete a step
|
|
121
|
+
* @param {string} id - Step identifier
|
|
122
|
+
* @param {string} message - Optional completion message
|
|
123
|
+
*/
|
|
124
|
+
completeStep(id, message = '') {
|
|
125
|
+
const step = this.steps.get(id);
|
|
126
|
+
if (!step) return;
|
|
127
|
+
|
|
128
|
+
step.status = 'completed';
|
|
129
|
+
step.endTime = Date.now();
|
|
130
|
+
if (message) {
|
|
131
|
+
step.message = message;
|
|
132
|
+
}
|
|
133
|
+
this.render();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Mark a step as failed
|
|
138
|
+
* @param {string} id - Step identifier
|
|
139
|
+
* @param {string} error - Error message
|
|
140
|
+
*/
|
|
141
|
+
failStep(id, error) {
|
|
142
|
+
const step = this.steps.get(id);
|
|
143
|
+
if (!step) return;
|
|
144
|
+
|
|
145
|
+
step.status = 'failed';
|
|
146
|
+
step.endTime = Date.now();
|
|
147
|
+
step.error = error;
|
|
148
|
+
this.render();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Add an issue/warning to be displayed at the end
|
|
153
|
+
* @param {string} type - 'error' | 'warning' | 'info'
|
|
154
|
+
* @param {string} message - The issue message
|
|
155
|
+
* @param {string} [hint] - Optional hint for resolution
|
|
156
|
+
*/
|
|
157
|
+
addIssue(type, message, hint = null) {
|
|
158
|
+
this.issues.push({ type, message, hint });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if there are any issues collected
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
hasIssues() {
|
|
166
|
+
return this.issues.length > 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get issues by type
|
|
171
|
+
* @param {string} type - 'error' | 'warning' | 'info'
|
|
172
|
+
* @returns {Array}
|
|
173
|
+
*/
|
|
174
|
+
getIssues(type = null) {
|
|
175
|
+
if (type) {
|
|
176
|
+
return this.issues.filter(i => i.type === type);
|
|
177
|
+
}
|
|
178
|
+
return this.issues;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Update the message for the current step
|
|
183
|
+
* @param {string} message - New message
|
|
184
|
+
*/
|
|
185
|
+
updateMessage(message) {
|
|
186
|
+
if (!this.currentStep) return;
|
|
187
|
+
const step = this.steps.get(this.currentStep);
|
|
188
|
+
if (step) {
|
|
189
|
+
step.message = message;
|
|
190
|
+
this.render();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Render the progress display
|
|
196
|
+
*/
|
|
197
|
+
render() {
|
|
198
|
+
if (this.isFinished) return;
|
|
199
|
+
|
|
200
|
+
// Clear previous render
|
|
201
|
+
if (this.lastRenderLength > 0) {
|
|
202
|
+
for (let i = 0; i < this.lastRenderLength; i++) {
|
|
203
|
+
process.stdout.write('\x1B[1A\x1B[2K'); // Move up and clear line
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const lines = [];
|
|
208
|
+
|
|
209
|
+
// Title with elapsed time
|
|
210
|
+
const elapsed = this.formatDuration(Date.now() - this.startTime);
|
|
211
|
+
lines.push(chalk.bold.blue(`\n${this.title}`) + chalk.gray(` (${elapsed})`));
|
|
212
|
+
lines.push('');
|
|
213
|
+
|
|
214
|
+
// Render each step
|
|
215
|
+
for (const [id, step] of this.steps) {
|
|
216
|
+
const prefix = this.getStepPrefix(step);
|
|
217
|
+
const label = step.status === 'active'
|
|
218
|
+
? chalk.cyan(step.label)
|
|
219
|
+
: step.status === 'completed'
|
|
220
|
+
? chalk.gray(step.label)
|
|
221
|
+
: step.status === 'failed'
|
|
222
|
+
? chalk.red(step.label)
|
|
223
|
+
: chalk.gray(step.label);
|
|
224
|
+
|
|
225
|
+
let line = `${prefix} ${label}`;
|
|
226
|
+
|
|
227
|
+
// Add progress bar if applicable
|
|
228
|
+
if (step.hasProgress && step.status === 'active' && step.progress.total > 0) {
|
|
229
|
+
const percentage = Math.floor((step.progress.current / step.progress.total) * 100);
|
|
230
|
+
const progressBar = this.renderProgressBar(percentage);
|
|
231
|
+
line += ` ${progressBar} ${percentage}%`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Add message or duration
|
|
235
|
+
if (step.message && step.status === 'active') {
|
|
236
|
+
line += chalk.gray(` - ${step.message}`);
|
|
237
|
+
} else if (step.status === 'completed' && step.endTime && step.startTime) {
|
|
238
|
+
const duration = this.formatDuration(step.endTime - step.startTime);
|
|
239
|
+
line += chalk.gray(` (${duration})`);
|
|
240
|
+
if (step.message) {
|
|
241
|
+
line += chalk.gray(` - ${step.message}`);
|
|
242
|
+
}
|
|
243
|
+
} else if (step.status === 'failed' && step.error) {
|
|
244
|
+
line += chalk.red(` - ${step.error}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
lines.push(line);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push(''); // Empty line at the end
|
|
251
|
+
|
|
252
|
+
this.lastRenderLength = lines.length;
|
|
253
|
+
process.stdout.write(lines.join('\n'));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get the prefix icon for a step
|
|
258
|
+
*/
|
|
259
|
+
getStepPrefix(step) {
|
|
260
|
+
switch (step.status) {
|
|
261
|
+
case 'pending':
|
|
262
|
+
return chalk.gray('◯');
|
|
263
|
+
case 'active':
|
|
264
|
+
return chalk.cyan(this.spinnerChars[this.spinnerIndex]);
|
|
265
|
+
case 'completed':
|
|
266
|
+
return chalk.green('✓');
|
|
267
|
+
case 'failed':
|
|
268
|
+
return chalk.red('✗');
|
|
269
|
+
default:
|
|
270
|
+
return chalk.gray('◯');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Render a progress bar
|
|
276
|
+
* @param {number} percentage - Progress percentage (0-100)
|
|
277
|
+
*/
|
|
278
|
+
renderProgressBar(percentage) {
|
|
279
|
+
const width = 20;
|
|
280
|
+
// Clamp percentage between 0 and 100 to prevent negative values
|
|
281
|
+
const clampedPercentage = Math.max(0, Math.min(100, percentage));
|
|
282
|
+
const filled = Math.floor((clampedPercentage / 100) * width);
|
|
283
|
+
const empty = width - filled;
|
|
284
|
+
|
|
285
|
+
const bar = chalk.cyan('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
286
|
+
return `[${bar}]`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Format duration in ms to human readable format
|
|
291
|
+
*/
|
|
292
|
+
formatDuration(ms) {
|
|
293
|
+
if (ms < 1000) return `${ms}ms`;
|
|
294
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
295
|
+
const minutes = Math.floor(ms / 60000);
|
|
296
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
297
|
+
return `${minutes}m ${seconds}s`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Start the spinner animation
|
|
302
|
+
*/
|
|
303
|
+
startSpinner() {
|
|
304
|
+
this.interval = setInterval(() => {
|
|
305
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length;
|
|
306
|
+
this.render();
|
|
307
|
+
}, 80);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Stop the spinner animation
|
|
312
|
+
*/
|
|
313
|
+
stopSpinner() {
|
|
314
|
+
if (this.interval) {
|
|
315
|
+
clearInterval(this.interval);
|
|
316
|
+
this.interval = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Finish the progress tracker
|
|
322
|
+
* @param {string} message - Optional final message
|
|
323
|
+
* @param {object} options - Optional configuration
|
|
324
|
+
* @param {boolean} options.showIssues - Whether to show collected issues (default: false)
|
|
325
|
+
*/
|
|
326
|
+
finish(message = '', options = {}) {
|
|
327
|
+
const { showIssues = false } = options;
|
|
328
|
+
|
|
329
|
+
this.stopSpinner();
|
|
330
|
+
this.isFinished = true;
|
|
331
|
+
|
|
332
|
+
// Complete any active steps
|
|
333
|
+
if (this.currentStep && this.steps.get(this.currentStep).status === 'active') {
|
|
334
|
+
this.completeStep(this.currentStep);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.render();
|
|
338
|
+
|
|
339
|
+
// Print summary
|
|
340
|
+
const totalTime = this.formatDuration(Date.now() - this.startTime);
|
|
341
|
+
const completed = Array.from(this.steps.values()).filter(s => s.status === 'completed').length;
|
|
342
|
+
const failed = Array.from(this.steps.values()).filter(s => s.status === 'failed').length;
|
|
343
|
+
|
|
344
|
+
if (message) {
|
|
345
|
+
console.log(chalk.bold.green(`\n${message}`));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(chalk.gray(`Total time: ${totalTime}`));
|
|
349
|
+
|
|
350
|
+
if (failed > 0) {
|
|
351
|
+
console.log(chalk.red(`✗ ${failed} step(s) failed`));
|
|
352
|
+
} else {
|
|
353
|
+
console.log(chalk.green(`✓ All ${completed} step(s) completed successfully`));
|
|
354
|
+
}
|
|
355
|
+
console.log('');
|
|
356
|
+
|
|
357
|
+
// Only display collected issues if explicitly requested
|
|
358
|
+
if (showIssues && this.issues.length > 0) {
|
|
359
|
+
this.displayIssues();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Display collected issues
|
|
365
|
+
* Can be called manually after finish() if needed
|
|
366
|
+
*/
|
|
367
|
+
displayIssues() {
|
|
368
|
+
if (this.issues.length === 0) return;
|
|
369
|
+
|
|
370
|
+
const errors = this.issues.filter(i => i.type === 'error');
|
|
371
|
+
const warnings = this.issues.filter(i => i.type === 'warning');
|
|
372
|
+
const infos = this.issues.filter(i => i.type === 'info');
|
|
373
|
+
|
|
374
|
+
console.log(chalk.bold('Issues Summary:'));
|
|
375
|
+
|
|
376
|
+
if (errors.length > 0) {
|
|
377
|
+
console.log(chalk.red(` ✗ ${errors.length} error(s)`));
|
|
378
|
+
errors.forEach((issue, i) => {
|
|
379
|
+
console.log(chalk.red(` ${i + 1}. ${issue.message}`));
|
|
380
|
+
if (issue.hint) {
|
|
381
|
+
console.log(chalk.yellow(` ${issue.hint}`));
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (warnings.length > 0) {
|
|
387
|
+
console.log(chalk.yellow(` ⚠ ${warnings.length} warning(s)`));
|
|
388
|
+
warnings.forEach((issue, i) => {
|
|
389
|
+
console.log(chalk.yellow(` ${i + 1}. ${issue.message}`));
|
|
390
|
+
if (issue.hint) {
|
|
391
|
+
console.log(chalk.gray(` ${issue.hint}`));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (infos.length > 0) {
|
|
397
|
+
console.log(chalk.cyan(` ℹ ${infos.length} info(s)`));
|
|
398
|
+
infos.forEach((issue, i) => {
|
|
399
|
+
console.log(chalk.cyan(` ${i + 1}. ${issue.message}`));
|
|
400
|
+
if (issue.hint) {
|
|
401
|
+
console.log(chalk.gray(` ${issue.hint}`));
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log('');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Abort the progress tracker with an error
|
|
411
|
+
* @param {string} error - Error message
|
|
412
|
+
*/
|
|
413
|
+
abort(error) {
|
|
414
|
+
this.stopSpinner();
|
|
415
|
+
this.isFinished = true;
|
|
416
|
+
|
|
417
|
+
if (this.currentStep) {
|
|
418
|
+
this.failStep(this.currentStep, error);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.render();
|
|
422
|
+
console.log(chalk.bold.red(`\n✗ ${error}\n`));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Helper function to create and use a simple progress tracker
|
|
428
|
+
* @param {string} title - Title of the operation
|
|
429
|
+
* @param {Array} steps - Array of step definitions
|
|
430
|
+
* @param {Function} fn - Async function that receives the progress tracker
|
|
431
|
+
* @returns {Promise} Result of the function
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* await withProgress('Pull Operation', [
|
|
435
|
+
* { id: 'auth', label: 'Authenticating...' },
|
|
436
|
+
* { id: 'download', label: 'Downloading files...', hasProgress: true },
|
|
437
|
+
* { id: 'process', label: 'Processing files...' }
|
|
438
|
+
* ], async (progress) => {
|
|
439
|
+
* progress.startStep('auth');
|
|
440
|
+
* await authenticate();
|
|
441
|
+
* progress.completeStep('auth');
|
|
442
|
+
*
|
|
443
|
+
* progress.startStep('download');
|
|
444
|
+
* for (let i = 0; i <= 100; i += 10) {
|
|
445
|
+
* progress.updateProgress('download', i, 100);
|
|
446
|
+
* await downloadBatch(i);
|
|
447
|
+
* }
|
|
448
|
+
* progress.completeStep('download');
|
|
449
|
+
* });
|
|
450
|
+
*/
|
|
451
|
+
export async function withProgress(title, steps, fn) {
|
|
452
|
+
const progress = new ProgressTracker(title);
|
|
453
|
+
|
|
454
|
+
// Add all steps
|
|
455
|
+
for (const step of steps) {
|
|
456
|
+
progress.addStep(step.id, step.label, step);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
progress.start();
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const result = await fn(progress);
|
|
463
|
+
progress.finish();
|
|
464
|
+
return result;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
progress.abort(error.message);
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
}
|