agileflow 2.89.3 → 2.90.1

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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/placeholder-registry.js +617 -0
  3. package/lib/smart-json-file.js +228 -1
  4. package/lib/table-formatter.js +519 -0
  5. package/lib/transient-status.js +374 -0
  6. package/lib/ui-manager.js +612 -0
  7. package/lib/validate-args.js +213 -0
  8. package/lib/validate-names.js +143 -0
  9. package/lib/validate-paths.js +434 -0
  10. package/lib/validate.js +37 -737
  11. package/package.json +3 -1
  12. package/scripts/check-update.js +17 -3
  13. package/scripts/lib/sessionRegistry.js +678 -0
  14. package/scripts/session-manager.js +77 -10
  15. package/scripts/tui/App.js +151 -0
  16. package/scripts/tui/index.js +31 -0
  17. package/scripts/tui/lib/crashRecovery.js +304 -0
  18. package/scripts/tui/lib/eventStream.js +309 -0
  19. package/scripts/tui/lib/keyboard.js +261 -0
  20. package/scripts/tui/lib/loopControl.js +371 -0
  21. package/scripts/tui/panels/OutputPanel.js +242 -0
  22. package/scripts/tui/panels/SessionPanel.js +170 -0
  23. package/scripts/tui/panels/TracePanel.js +298 -0
  24. package/scripts/tui/simple-tui.js +390 -0
  25. package/tools/cli/commands/config.js +7 -31
  26. package/tools/cli/commands/doctor.js +28 -39
  27. package/tools/cli/commands/list.js +47 -35
  28. package/tools/cli/commands/status.js +20 -38
  29. package/tools/cli/commands/tui.js +59 -0
  30. package/tools/cli/commands/uninstall.js +12 -39
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +382 -0
  33. package/tools/cli/lib/config-manager.js +394 -0
  34. package/tools/cli/lib/ide-registry.js +186 -0
  35. package/tools/cli/lib/npm-utils.js +17 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. 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
+ };