@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.
Files changed (68) hide show
  1. package/LICENSE +25 -25
  2. package/README.md +1166 -1166
  3. package/actions/autopublish.old.js +293 -293
  4. package/actions/config.js +182 -182
  5. package/actions/create.js +466 -466
  6. package/actions/help.js +164 -164
  7. package/actions/iris/buildStage.js +874 -874
  8. package/actions/iris/delete.js +256 -256
  9. package/actions/iris/dev.js +391 -391
  10. package/actions/iris/index.js +6 -6
  11. package/actions/iris/link.js +375 -375
  12. package/actions/iris/recover.js +268 -268
  13. package/actions/main.js +80 -80
  14. package/actions/publish.js +1420 -1420
  15. package/actions/pull.js +684 -684
  16. package/actions/setup.js +148 -148
  17. package/actions/status.js +17 -17
  18. package/actions/update.js +248 -248
  19. package/bin/magentrix.js +393 -393
  20. package/package.json +55 -55
  21. package/utils/assetPaths.js +158 -158
  22. package/utils/autopublishLock.js +77 -77
  23. package/utils/cacher.js +206 -206
  24. package/utils/cli/checkInstanceUrl.js +76 -74
  25. package/utils/cli/helpers/compare.js +282 -282
  26. package/utils/cli/helpers/ensureApiKey.js +63 -63
  27. package/utils/cli/helpers/ensureCredentials.js +68 -68
  28. package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
  29. package/utils/cli/writeRecords.js +262 -262
  30. package/utils/compare.js +135 -135
  31. package/utils/compress.js +17 -17
  32. package/utils/config.js +527 -527
  33. package/utils/debug.js +144 -144
  34. package/utils/diagnostics/testPublishLogic.js +96 -96
  35. package/utils/diff.js +49 -49
  36. package/utils/downloadAssets.js +291 -291
  37. package/utils/filetag.js +115 -115
  38. package/utils/hash.js +14 -14
  39. package/utils/iris/backup.js +411 -411
  40. package/utils/iris/builder.js +541 -541
  41. package/utils/iris/config-reader.js +664 -664
  42. package/utils/iris/deleteHelper.js +150 -150
  43. package/utils/iris/errors.js +537 -537
  44. package/utils/iris/linker.js +601 -601
  45. package/utils/iris/lock.js +360 -360
  46. package/utils/iris/validation.js +360 -360
  47. package/utils/iris/validator.js +281 -281
  48. package/utils/iris/zipper.js +248 -248
  49. package/utils/logger.js +291 -291
  50. package/utils/magentrix/api/assets.js +220 -220
  51. package/utils/magentrix/api/auth.js +107 -107
  52. package/utils/magentrix/api/createEntity.js +61 -61
  53. package/utils/magentrix/api/deleteEntity.js +55 -55
  54. package/utils/magentrix/api/iris.js +251 -251
  55. package/utils/magentrix/api/meqlQuery.js +36 -36
  56. package/utils/magentrix/api/retrieveEntity.js +86 -86
  57. package/utils/magentrix/api/updateEntity.js +66 -66
  58. package/utils/magentrix/fetch.js +168 -168
  59. package/utils/merge.js +22 -22
  60. package/utils/permissionError.js +70 -70
  61. package/utils/preferences.js +40 -40
  62. package/utils/progress.js +469 -469
  63. package/utils/spinner.js +43 -43
  64. package/utils/template.js +52 -52
  65. package/utils/updateFileBase.js +121 -121
  66. package/utils/workspaces.js +108 -108
  67. package/vars/config.js +11 -11
  68. 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
+ }