agileflow 2.89.3 → 2.90.0
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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/lib/placeholder-registry.js +617 -0
- package/lib/smart-json-file.js +205 -1
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +37 -737
- package/package.json +4 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +7 -30
- package/tools/cli/commands/doctor.js +18 -38
- package/tools/cli/commands/list.js +47 -35
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +9 -38
- package/tools/cli/installers/core/installer.js +13 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UiManager - Unified Progress Feedback System
|
|
5
|
+
*
|
|
6
|
+
* Provides consistent UI patterns across AgileFlow:
|
|
7
|
+
* - Spinners with progress indication
|
|
8
|
+
* - Micro-progress displays (current/total)
|
|
9
|
+
* - ETA calculation for long operations
|
|
10
|
+
* - Timing metrics for performance analysis
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { c } = require('./colors');
|
|
14
|
+
|
|
15
|
+
// Spinner frames
|
|
16
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
17
|
+
const SPINNER_INTERVAL = 80; // ms
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format milliseconds to human readable string
|
|
21
|
+
* @param {number} ms - Milliseconds
|
|
22
|
+
* @returns {string} Formatted time
|
|
23
|
+
*/
|
|
24
|
+
function formatTime(ms) {
|
|
25
|
+
if (ms < 1000) return `${ms}ms`;
|
|
26
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
27
|
+
const mins = Math.floor(ms / 60000);
|
|
28
|
+
const secs = Math.floor((ms % 60000) / 1000);
|
|
29
|
+
return `${mins}m ${secs}s`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format bytes to human readable string
|
|
34
|
+
* @param {number} bytes - Bytes
|
|
35
|
+
* @returns {string} Formatted size
|
|
36
|
+
*/
|
|
37
|
+
function formatBytes(bytes) {
|
|
38
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
39
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
40
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculate ETA based on progress
|
|
45
|
+
* @param {number} current - Current count
|
|
46
|
+
* @param {number} total - Total count
|
|
47
|
+
* @param {number} startTime - Start time in ms
|
|
48
|
+
* @returns {string} ETA string
|
|
49
|
+
*/
|
|
50
|
+
function calculateETA(current, total, startTime) {
|
|
51
|
+
if (current === 0) return 'calculating...';
|
|
52
|
+
if (current >= total) return 'complete';
|
|
53
|
+
|
|
54
|
+
const elapsed = Date.now() - startTime;
|
|
55
|
+
const rate = current / elapsed;
|
|
56
|
+
const remaining = total - current;
|
|
57
|
+
const etaMs = remaining / rate;
|
|
58
|
+
|
|
59
|
+
return formatTime(etaMs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Spinner class for animated progress indication
|
|
64
|
+
*/
|
|
65
|
+
class Spinner {
|
|
66
|
+
constructor(options = {}) {
|
|
67
|
+
this.text = options.text || 'Loading...';
|
|
68
|
+
this.frames = options.frames || SPINNER_FRAMES;
|
|
69
|
+
this.interval = options.interval || SPINNER_INTERVAL;
|
|
70
|
+
this.stream = options.stream || process.stderr;
|
|
71
|
+
this.color = options.color || 'cyan';
|
|
72
|
+
|
|
73
|
+
this._frameIndex = 0;
|
|
74
|
+
this._timer = null;
|
|
75
|
+
this._isSpinning = false;
|
|
76
|
+
this._current = 0;
|
|
77
|
+
this._total = 0;
|
|
78
|
+
this._startTime = null;
|
|
79
|
+
this._showProgress = options.showProgress || false;
|
|
80
|
+
this._showETA = options.showETA || false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Start the spinner
|
|
85
|
+
* @param {string} text - Optional text to display
|
|
86
|
+
*/
|
|
87
|
+
start(text) {
|
|
88
|
+
if (text) this.text = text;
|
|
89
|
+
if (this._isSpinning) return this;
|
|
90
|
+
|
|
91
|
+
this._isSpinning = true;
|
|
92
|
+
this._startTime = Date.now();
|
|
93
|
+
this._render();
|
|
94
|
+
this._timer = setInterval(() => this._render(), this.interval);
|
|
95
|
+
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Stop the spinner
|
|
101
|
+
* @param {Object} options - Stop options
|
|
102
|
+
*/
|
|
103
|
+
stop(options = {}) {
|
|
104
|
+
if (!this._isSpinning) return this;
|
|
105
|
+
|
|
106
|
+
clearInterval(this._timer);
|
|
107
|
+
this._timer = null;
|
|
108
|
+
this._isSpinning = false;
|
|
109
|
+
|
|
110
|
+
// Clear the line
|
|
111
|
+
this._clearLine();
|
|
112
|
+
|
|
113
|
+
// Show final message if provided
|
|
114
|
+
if (options.text) {
|
|
115
|
+
const symbol = options.symbol || '✓';
|
|
116
|
+
const color = options.color || 'green';
|
|
117
|
+
this.stream.write(`${c[color]}${symbol}${c.reset} ${options.text}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update spinner text
|
|
125
|
+
* @param {string} text - New text
|
|
126
|
+
*/
|
|
127
|
+
update(text) {
|
|
128
|
+
this.text = text;
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Set progress for micro-progress display
|
|
134
|
+
* @param {number} current - Current count
|
|
135
|
+
* @param {number} total - Total count
|
|
136
|
+
*/
|
|
137
|
+
setProgress(current, total) {
|
|
138
|
+
this._current = current;
|
|
139
|
+
this._total = total;
|
|
140
|
+
this._showProgress = true;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Success - stop with success indicator
|
|
146
|
+
* @param {string} text - Success message
|
|
147
|
+
*/
|
|
148
|
+
succeed(text) {
|
|
149
|
+
return this.stop({ text, symbol: '✓', color: 'green' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Fail - stop with failure indicator
|
|
154
|
+
* @param {string} text - Failure message
|
|
155
|
+
*/
|
|
156
|
+
fail(text) {
|
|
157
|
+
return this.stop({ text, symbol: '✗', color: 'red' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Warn - stop with warning indicator
|
|
162
|
+
* @param {string} text - Warning message
|
|
163
|
+
*/
|
|
164
|
+
warn(text) {
|
|
165
|
+
return this.stop({ text, symbol: '⚠', color: 'yellow' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Info - stop with info indicator
|
|
170
|
+
* @param {string} text - Info message
|
|
171
|
+
*/
|
|
172
|
+
info(text) {
|
|
173
|
+
return this.stop({ text, symbol: 'ℹ', color: 'blue' });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Render the spinner frame
|
|
178
|
+
*/
|
|
179
|
+
_render() {
|
|
180
|
+
const frame = this.frames[this._frameIndex];
|
|
181
|
+
this._frameIndex = (this._frameIndex + 1) % this.frames.length;
|
|
182
|
+
|
|
183
|
+
let line = `${c[this.color]}${frame}${c.reset} ${this.text}`;
|
|
184
|
+
|
|
185
|
+
// Add progress if available
|
|
186
|
+
if (this._showProgress && this._total > 0) {
|
|
187
|
+
const pct = Math.round((this._current / this._total) * 100);
|
|
188
|
+
line += ` ${c.dim}(${this._current}/${this._total} - ${pct}%)${c.reset}`;
|
|
189
|
+
|
|
190
|
+
// Add ETA if enabled
|
|
191
|
+
if (this._showETA && this._startTime) {
|
|
192
|
+
const eta = calculateETA(this._current, this._total, this._startTime);
|
|
193
|
+
line += ` ${c.dim}ETA: ${eta}${c.reset}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this._clearLine();
|
|
198
|
+
this.stream.write(line);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Clear the current line
|
|
203
|
+
*/
|
|
204
|
+
_clearLine() {
|
|
205
|
+
if (this.stream.isTTY) {
|
|
206
|
+
this.stream.clearLine(0);
|
|
207
|
+
this.stream.cursorTo(0);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Progress bar for visual progress indication
|
|
214
|
+
*/
|
|
215
|
+
class ProgressBar {
|
|
216
|
+
constructor(options = {}) {
|
|
217
|
+
this.total = options.total || 100;
|
|
218
|
+
this.width = options.width || 30;
|
|
219
|
+
this.fillChar = options.fillChar || '█';
|
|
220
|
+
this.emptyChar = options.emptyChar || '░';
|
|
221
|
+
this.stream = options.stream || process.stderr;
|
|
222
|
+
this.showPercent = options.showPercent !== false;
|
|
223
|
+
this.showETA = options.showETA || false;
|
|
224
|
+
this.label = options.label || '';
|
|
225
|
+
|
|
226
|
+
this._current = 0;
|
|
227
|
+
this._startTime = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Start the progress bar
|
|
232
|
+
* @param {number} total - Total items
|
|
233
|
+
*/
|
|
234
|
+
start(total) {
|
|
235
|
+
if (total !== undefined) this.total = total;
|
|
236
|
+
this._startTime = Date.now();
|
|
237
|
+
this._current = 0;
|
|
238
|
+
this._render();
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update progress
|
|
244
|
+
* @param {number} current - Current progress
|
|
245
|
+
*/
|
|
246
|
+
update(current) {
|
|
247
|
+
this._current = Math.min(current, this.total);
|
|
248
|
+
this._render();
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Increment progress by amount
|
|
254
|
+
* @param {number} amount - Amount to increment
|
|
255
|
+
*/
|
|
256
|
+
increment(amount = 1) {
|
|
257
|
+
return this.update(this._current + amount);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Complete the progress bar
|
|
262
|
+
*/
|
|
263
|
+
complete() {
|
|
264
|
+
this._current = this.total;
|
|
265
|
+
this._render();
|
|
266
|
+
this.stream.write('\n');
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Render the progress bar
|
|
272
|
+
*/
|
|
273
|
+
_render() {
|
|
274
|
+
const percent = this.total > 0 ? this._current / this.total : 0;
|
|
275
|
+
const filled = Math.round(percent * this.width);
|
|
276
|
+
const empty = this.width - filled;
|
|
277
|
+
|
|
278
|
+
let bar = `${this.fillChar.repeat(filled)}${this.emptyChar.repeat(empty)}`;
|
|
279
|
+
|
|
280
|
+
// Color based on progress
|
|
281
|
+
let barColor = 'red';
|
|
282
|
+
if (percent >= 0.8) barColor = 'green';
|
|
283
|
+
else if (percent >= 0.5) barColor = 'yellow';
|
|
284
|
+
|
|
285
|
+
let line = '';
|
|
286
|
+
if (this.label) {
|
|
287
|
+
line += `${this.label} `;
|
|
288
|
+
}
|
|
289
|
+
line += `${c[barColor]}${bar}${c.reset}`;
|
|
290
|
+
|
|
291
|
+
if (this.showPercent) {
|
|
292
|
+
line += ` ${Math.round(percent * 100)}%`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
line += ` ${c.dim}(${this._current}/${this.total})${c.reset}`;
|
|
296
|
+
|
|
297
|
+
if (this.showETA && this._startTime && this._current > 0) {
|
|
298
|
+
const eta = calculateETA(this._current, this.total, this._startTime);
|
|
299
|
+
line += ` ${c.dim}ETA: ${eta}${c.reset}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Clear and render
|
|
303
|
+
if (this.stream.isTTY) {
|
|
304
|
+
this.stream.clearLine(0);
|
|
305
|
+
this.stream.cursorTo(0);
|
|
306
|
+
}
|
|
307
|
+
this.stream.write(line);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Timing tracker for performance analysis
|
|
313
|
+
*/
|
|
314
|
+
class TimingTracker {
|
|
315
|
+
constructor(options = {}) {
|
|
316
|
+
this.name = options.name || 'operation';
|
|
317
|
+
this.phases = {};
|
|
318
|
+
this._currentPhase = null;
|
|
319
|
+
this._startTime = null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Start overall timing
|
|
324
|
+
*/
|
|
325
|
+
start() {
|
|
326
|
+
this._startTime = Date.now();
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Start a phase
|
|
332
|
+
* @param {string} name - Phase name
|
|
333
|
+
*/
|
|
334
|
+
startPhase(name) {
|
|
335
|
+
if (this._currentPhase) {
|
|
336
|
+
this.endPhase();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this._currentPhase = name;
|
|
340
|
+
this.phases[name] = {
|
|
341
|
+
start: Date.now(),
|
|
342
|
+
end: null,
|
|
343
|
+
duration: null,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* End the current phase
|
|
351
|
+
*/
|
|
352
|
+
endPhase() {
|
|
353
|
+
if (!this._currentPhase) return this;
|
|
354
|
+
|
|
355
|
+
const phase = this.phases[this._currentPhase];
|
|
356
|
+
if (phase) {
|
|
357
|
+
phase.end = Date.now();
|
|
358
|
+
phase.duration = phase.end - phase.start;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this._currentPhase = null;
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get total elapsed time
|
|
367
|
+
* @returns {number} Elapsed milliseconds
|
|
368
|
+
*/
|
|
369
|
+
getElapsed() {
|
|
370
|
+
if (!this._startTime) return 0;
|
|
371
|
+
return Date.now() - this._startTime;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get timing summary
|
|
376
|
+
* @returns {Object} Timing summary
|
|
377
|
+
*/
|
|
378
|
+
getSummary() {
|
|
379
|
+
// End current phase if any
|
|
380
|
+
if (this._currentPhase) {
|
|
381
|
+
this.endPhase();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const total = this.getElapsed();
|
|
385
|
+
const phaseStats = {};
|
|
386
|
+
|
|
387
|
+
for (const [name, phase] of Object.entries(this.phases)) {
|
|
388
|
+
phaseStats[name] = {
|
|
389
|
+
duration: phase.duration || 0,
|
|
390
|
+
durationFormatted: formatTime(phase.duration || 0),
|
|
391
|
+
percent: total > 0 ? Math.round(((phase.duration || 0) / total) * 100) : 0,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
name: this.name,
|
|
397
|
+
totalMs: total,
|
|
398
|
+
totalFormatted: formatTime(total),
|
|
399
|
+
phases: phaseStats,
|
|
400
|
+
startTime: this._startTime ? new Date(this._startTime).toISOString() : null,
|
|
401
|
+
endTime: new Date().toISOString(),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Format summary for display
|
|
407
|
+
* @returns {string} Formatted summary
|
|
408
|
+
*/
|
|
409
|
+
formatSummary() {
|
|
410
|
+
const summary = this.getSummary();
|
|
411
|
+
const lines = [];
|
|
412
|
+
|
|
413
|
+
lines.push(`${c.cyan}Timing Summary: ${summary.name}${c.reset}`);
|
|
414
|
+
lines.push(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
415
|
+
|
|
416
|
+
// Sort phases by duration (descending)
|
|
417
|
+
const sorted = Object.entries(summary.phases).sort((a, b) => b[1].duration - a[1].duration);
|
|
418
|
+
|
|
419
|
+
for (const [name, stats] of sorted) {
|
|
420
|
+
const bar = '█'.repeat(Math.max(1, Math.round(stats.percent / 5)));
|
|
421
|
+
lines.push(
|
|
422
|
+
` ${name.padEnd(20)} ${stats.durationFormatted.padStart(8)} ${c.dim}(${stats.percent}%)${c.reset} ${c.green}${bar}${c.reset}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
lines.push(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
427
|
+
lines.push(`${c.bold}Total:${c.reset} ${summary.totalFormatted}`);
|
|
428
|
+
|
|
429
|
+
return lines.join('\n');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Export to JSON for trending
|
|
434
|
+
* @returns {string} JSON string
|
|
435
|
+
*/
|
|
436
|
+
toJSON() {
|
|
437
|
+
return JSON.stringify(this.getSummary(), null, 2);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* UiManager - Main class for unified UI feedback
|
|
443
|
+
*/
|
|
444
|
+
class UiManager {
|
|
445
|
+
constructor(options = {}) {
|
|
446
|
+
this.stream = options.stream || process.stderr;
|
|
447
|
+
this.verbose = options.verbose || false;
|
|
448
|
+
this.timing = options.timing || false;
|
|
449
|
+
this.quiet = options.quiet || false;
|
|
450
|
+
|
|
451
|
+
this._spinner = null;
|
|
452
|
+
this._progress = null;
|
|
453
|
+
this._tracker = null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Create a spinner
|
|
458
|
+
* @param {Object} options - Spinner options
|
|
459
|
+
* @returns {Spinner} Spinner instance
|
|
460
|
+
*/
|
|
461
|
+
spinner(options = {}) {
|
|
462
|
+
return new Spinner({ stream: this.stream, ...options });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Create a progress bar
|
|
467
|
+
* @param {Object} options - Progress bar options
|
|
468
|
+
* @returns {ProgressBar} Progress bar instance
|
|
469
|
+
*/
|
|
470
|
+
progressBar(options = {}) {
|
|
471
|
+
return new ProgressBar({ stream: this.stream, ...options });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Create a timing tracker
|
|
476
|
+
* @param {string} name - Operation name
|
|
477
|
+
* @returns {TimingTracker} Timing tracker instance
|
|
478
|
+
*/
|
|
479
|
+
tracker(name) {
|
|
480
|
+
return new TimingTracker({ name });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Log a message (respects quiet mode)
|
|
485
|
+
* @param {string} message - Message to log
|
|
486
|
+
*/
|
|
487
|
+
log(message) {
|
|
488
|
+
if (!this.quiet) {
|
|
489
|
+
this.stream.write(message + '\n');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Log verbose message (only if verbose mode)
|
|
495
|
+
* @param {string} message - Message to log
|
|
496
|
+
*/
|
|
497
|
+
debug(message) {
|
|
498
|
+
if (this.verbose && !this.quiet) {
|
|
499
|
+
this.stream.write(`${c.dim}[debug] ${message}${c.reset}\n`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Log success message
|
|
505
|
+
* @param {string} message - Success message
|
|
506
|
+
*/
|
|
507
|
+
success(message) {
|
|
508
|
+
if (!this.quiet) {
|
|
509
|
+
this.stream.write(`${c.green}✓${c.reset} ${message}\n`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Log error message
|
|
515
|
+
* @param {string} message - Error message
|
|
516
|
+
*/
|
|
517
|
+
error(message) {
|
|
518
|
+
this.stream.write(`${c.red}✗${c.reset} ${message}\n`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Log warning message
|
|
523
|
+
* @param {string} message - Warning message
|
|
524
|
+
*/
|
|
525
|
+
warn(message) {
|
|
526
|
+
if (!this.quiet) {
|
|
527
|
+
this.stream.write(`${c.yellow}⚠${c.reset} ${message}\n`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Log info message
|
|
533
|
+
* @param {string} message - Info message
|
|
534
|
+
*/
|
|
535
|
+
info(message) {
|
|
536
|
+
if (!this.quiet) {
|
|
537
|
+
this.stream.write(`${c.blue}ℹ${c.reset} ${message}\n`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Display a section header
|
|
543
|
+
* @param {string} title - Section title
|
|
544
|
+
*/
|
|
545
|
+
section(title) {
|
|
546
|
+
if (!this.quiet) {
|
|
547
|
+
this.stream.write(`\n${c.cyan}${c.bold}${title}${c.reset}\n`);
|
|
548
|
+
this.stream.write(`${c.dim}${'─'.repeat(title.length)}${c.reset}\n`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Display a list of items with progress
|
|
554
|
+
* @param {Array} items - Items to display
|
|
555
|
+
* @param {Function} formatter - Function to format each item
|
|
556
|
+
*/
|
|
557
|
+
list(items, formatter = item => String(item)) {
|
|
558
|
+
if (this.quiet) return;
|
|
559
|
+
|
|
560
|
+
const total = items.length;
|
|
561
|
+
items.forEach((item, i) => {
|
|
562
|
+
const formatted = formatter(item, i);
|
|
563
|
+
this.stream.write(` ${c.dim}[${i + 1}/${total}]${c.reset} ${formatted}\n`);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Display timing summary if timing enabled
|
|
569
|
+
* @param {TimingTracker} tracker - Timing tracker
|
|
570
|
+
*/
|
|
571
|
+
showTiming(tracker) {
|
|
572
|
+
if (this.timing && tracker) {
|
|
573
|
+
this.stream.write('\n' + tracker.formatSummary() + '\n');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Singleton instance
|
|
579
|
+
let _instance = null;
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Get singleton UiManager instance
|
|
583
|
+
* @param {Object} options - Options
|
|
584
|
+
* @returns {UiManager} UiManager instance
|
|
585
|
+
*/
|
|
586
|
+
function getUiManager(options = {}) {
|
|
587
|
+
if (!_instance || options.forceNew) {
|
|
588
|
+
_instance = new UiManager(options);
|
|
589
|
+
}
|
|
590
|
+
return _instance;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Reset singleton (for testing)
|
|
595
|
+
*/
|
|
596
|
+
function resetUiManager() {
|
|
597
|
+
_instance = null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
module.exports = {
|
|
601
|
+
UiManager,
|
|
602
|
+
Spinner,
|
|
603
|
+
ProgressBar,
|
|
604
|
+
TimingTracker,
|
|
605
|
+
getUiManager,
|
|
606
|
+
resetUiManager,
|
|
607
|
+
formatTime,
|
|
608
|
+
formatBytes,
|
|
609
|
+
calculateETA,
|
|
610
|
+
SPINNER_FRAMES,
|
|
611
|
+
SPINNER_INTERVAL,
|
|
612
|
+
};
|