agileflow 2.89.2 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/content-sanitizer.js +463 -0
  4. package/lib/error-codes.js +544 -0
  5. package/lib/errors.js +336 -5
  6. package/lib/feedback.js +561 -0
  7. package/lib/path-resolver.js +396 -0
  8. package/lib/placeholder-registry.js +617 -0
  9. package/lib/session-registry.js +461 -0
  10. package/lib/smart-json-file.js +653 -0
  11. package/lib/table-formatter.js +504 -0
  12. package/lib/transient-status.js +374 -0
  13. package/lib/ui-manager.js +612 -0
  14. package/lib/validate-args.js +213 -0
  15. package/lib/validate-names.js +143 -0
  16. package/lib/validate-paths.js +434 -0
  17. package/lib/validate.js +38 -584
  18. package/package.json +4 -1
  19. package/scripts/agileflow-configure.js +40 -1440
  20. package/scripts/agileflow-welcome.js +2 -1
  21. package/scripts/check-update.js +16 -3
  22. package/scripts/lib/configure-detect.js +383 -0
  23. package/scripts/lib/configure-features.js +811 -0
  24. package/scripts/lib/configure-repair.js +314 -0
  25. package/scripts/lib/configure-utils.js +115 -0
  26. package/scripts/lib/frontmatter-parser.js +3 -3
  27. package/scripts/lib/sessionRegistry.js +682 -0
  28. package/scripts/obtain-context.js +417 -113
  29. package/scripts/ralph-loop.js +1 -1
  30. package/scripts/session-manager.js +77 -10
  31. package/scripts/tui/App.js +176 -0
  32. package/scripts/tui/index.js +75 -0
  33. package/scripts/tui/lib/crashRecovery.js +302 -0
  34. package/scripts/tui/lib/eventStream.js +316 -0
  35. package/scripts/tui/lib/keyboard.js +252 -0
  36. package/scripts/tui/lib/loopControl.js +371 -0
  37. package/scripts/tui/panels/OutputPanel.js +278 -0
  38. package/scripts/tui/panels/SessionPanel.js +178 -0
  39. package/scripts/tui/panels/TracePanel.js +333 -0
  40. package/src/core/commands/tui.md +91 -0
  41. package/tools/cli/commands/config.js +10 -33
  42. package/tools/cli/commands/doctor.js +48 -40
  43. package/tools/cli/commands/list.js +49 -37
  44. package/tools/cli/commands/status.js +13 -37
  45. package/tools/cli/commands/uninstall.js +12 -41
  46. package/tools/cli/installers/core/installer.js +75 -12
  47. package/tools/cli/installers/ide/_interface.js +238 -0
  48. package/tools/cli/installers/ide/codex.js +2 -2
  49. package/tools/cli/installers/ide/manager.js +15 -0
  50. package/tools/cli/lib/command-context.js +374 -0
  51. package/tools/cli/lib/config-manager.js +394 -0
  52. package/tools/cli/lib/content-injector.js +69 -16
  53. package/tools/cli/lib/ide-errors.js +163 -29
  54. package/tools/cli/lib/ide-registry.js +186 -0
  55. package/tools/cli/lib/npm-utils.js +16 -3
  56. package/tools/cli/lib/self-update.js +148 -0
  57. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -0,0 +1,561 @@
1
+ /**
2
+ * feedback.js - Unified Progress Feedback System
3
+ *
4
+ * Consolidates all progress/feedback patterns:
5
+ * - Status messages (success, error, warning, info)
6
+ * - Spinners for indeterminate progress
7
+ * - Progress bars for determinate progress
8
+ * - Task tracking for multi-step operations
9
+ * - Consistent formatting across all output
10
+ *
11
+ * Features:
12
+ * - TTY detection (graceful degradation for non-interactive)
13
+ * - Doherty Threshold timing (operations <400ms feel instant)
14
+ * - Nested task support with indentation
15
+ * - Consistent symbol and color usage
16
+ *
17
+ * Usage:
18
+ * const { feedback } = require('./feedback');
19
+ *
20
+ * // Simple messages
21
+ * feedback.success('Task completed');
22
+ * feedback.error('Something went wrong');
23
+ *
24
+ * // With spinner
25
+ * const spinner = feedback.spinner('Processing...');
26
+ * await doWork();
27
+ * spinner.succeed('Done!');
28
+ *
29
+ * // Multi-step operations
30
+ * const task = feedback.task('Installing', 3);
31
+ * task.step('Copying files');
32
+ * task.step('Setting permissions');
33
+ * task.complete('Installed successfully');
34
+ */
35
+
36
+ const { c, BRAND_HEX } = require('./colors');
37
+ const chalk = require('chalk');
38
+
39
+ // Symbols for consistent output
40
+ const SYMBOLS = {
41
+ success: '✓',
42
+ error: '✗',
43
+ warning: '⚠',
44
+ info: 'ℹ',
45
+ bullet: '•',
46
+ arrow: '→',
47
+ pending: '○',
48
+ active: '●',
49
+ };
50
+
51
+ // Braille spinner characters (smooth animation)
52
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
53
+
54
+ // Doherty Threshold - operations faster than this feel instant
55
+ const DOHERTY_THRESHOLD_MS = 400;
56
+
57
+ /**
58
+ * Format duration for display
59
+ * @param {number} ms - Duration in milliseconds
60
+ * @returns {string} Formatted duration
61
+ */
62
+ function formatDuration(ms) {
63
+ if (ms < 1000) return `${ms}ms`;
64
+ return `${(ms / 1000).toFixed(1)}s`;
65
+ }
66
+
67
+ /**
68
+ * Unified Feedback class
69
+ * Provides consistent progress and status output
70
+ */
71
+ class Feedback {
72
+ constructor(options = {}) {
73
+ this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
74
+ this.indent = options.indent || 0;
75
+ this.quiet = options.quiet || false;
76
+ this.verbose = options.verbose || false;
77
+ }
78
+
79
+ /**
80
+ * Get indentation string
81
+ * @returns {string}
82
+ * @private
83
+ */
84
+ _indent() {
85
+ return ' '.repeat(this.indent);
86
+ }
87
+
88
+ /**
89
+ * Print a message (respects quiet mode)
90
+ * @param {string} symbol - Symbol to prefix
91
+ * @param {string} message - Message text
92
+ * @param {string} color - Color to apply
93
+ * @private
94
+ */
95
+ _print(symbol, message, color = '') {
96
+ if (this.quiet) return;
97
+ const prefix = this._indent();
98
+ console.log(`${prefix}${color}${symbol}${c.reset} ${message}`);
99
+ }
100
+
101
+ /**
102
+ * Print success message
103
+ * @param {string} message - Success message
104
+ * @returns {Feedback} this for chaining
105
+ */
106
+ success(message) {
107
+ this._print(SYMBOLS.success, message, c.green);
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Print error message
113
+ * @param {string} message - Error message
114
+ * @returns {Feedback} this for chaining
115
+ */
116
+ error(message) {
117
+ this._print(SYMBOLS.error, message, c.red);
118
+ return this;
119
+ }
120
+
121
+ /**
122
+ * Print warning message
123
+ * @param {string} message - Warning message
124
+ * @returns {Feedback} this for chaining
125
+ */
126
+ warning(message) {
127
+ this._print(SYMBOLS.warning, message, c.yellow);
128
+ return this;
129
+ }
130
+
131
+ /**
132
+ * Print info message
133
+ * @param {string} message - Info message
134
+ * @returns {Feedback} this for chaining
135
+ */
136
+ info(message) {
137
+ this._print(SYMBOLS.info, message, c.blue);
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Print dim/subtle message
143
+ * @param {string} message - Message
144
+ * @returns {Feedback} this for chaining
145
+ */
146
+ dim(message) {
147
+ if (this.quiet) return this;
148
+ const prefix = this._indent();
149
+ console.log(`${prefix}${c.dim}${message}${c.reset}`);
150
+ return this;
151
+ }
152
+
153
+ /**
154
+ * Print verbose message (only in verbose mode)
155
+ * @param {string} message - Message
156
+ * @returns {Feedback} this for chaining
157
+ */
158
+ debug(message) {
159
+ if (!this.verbose) return this;
160
+ this._print(SYMBOLS.bullet, message, c.dim);
161
+ return this;
162
+ }
163
+
164
+ /**
165
+ * Print bullet point
166
+ * @param {string} message - Message
167
+ * @returns {Feedback} this for chaining
168
+ */
169
+ bullet(message) {
170
+ this._print(SYMBOLS.bullet, message, c.dim);
171
+ return this;
172
+ }
173
+
174
+ /**
175
+ * Print branded message (uses brand color)
176
+ * @param {string} message - Message
177
+ * @returns {Feedback} this for chaining
178
+ */
179
+ brand(message) {
180
+ if (this.quiet) return this;
181
+ const prefix = this._indent();
182
+ console.log(`${prefix}${chalk.hex(BRAND_HEX)(message)}`);
183
+ return this;
184
+ }
185
+
186
+ /**
187
+ * Print empty line
188
+ * @returns {Feedback} this for chaining
189
+ */
190
+ newline() {
191
+ if (this.quiet) return this;
192
+ console.log();
193
+ return this;
194
+ }
195
+
196
+ /**
197
+ * Create a new spinner
198
+ * @param {string} message - Spinner message
199
+ * @param {Object} [options={}] - Spinner options
200
+ * @returns {FeedbackSpinner}
201
+ */
202
+ spinner(message, options = {}) {
203
+ return new FeedbackSpinner(message, {
204
+ ...options,
205
+ isTTY: this.isTTY,
206
+ indent: this.indent,
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Create a task tracker for multi-step operations
212
+ * @param {string} name - Task name
213
+ * @param {number} [totalSteps] - Total number of steps (optional)
214
+ * @returns {FeedbackTask}
215
+ */
216
+ task(name, totalSteps = null) {
217
+ return new FeedbackTask(name, totalSteps, {
218
+ isTTY: this.isTTY,
219
+ indent: this.indent,
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Create a progress bar
225
+ * @param {string} label - Progress label
226
+ * @param {number} total - Total items
227
+ * @param {Object} [options={}] - Progress bar options
228
+ * @returns {FeedbackProgressBar}
229
+ */
230
+ progressBar(label, total, options = {}) {
231
+ return new FeedbackProgressBar(label, total, {
232
+ ...options,
233
+ isTTY: this.isTTY,
234
+ indent: this.indent,
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Run an async operation with a spinner
240
+ * @param {string} message - Spinner message
241
+ * @param {Function} fn - Async function to execute
242
+ * @param {Object} [options={}] - Spinner options
243
+ * @returns {Promise<any>} Result of the async function
244
+ */
245
+ async withSpinner(message, fn, options = {}) {
246
+ const spinner = this.spinner(message, options).start();
247
+ try {
248
+ const result = await fn(spinner);
249
+ spinner.succeed();
250
+ return result;
251
+ } catch (error) {
252
+ spinner.fail(error.message);
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Create a child feedback with increased indentation
259
+ * @returns {Feedback}
260
+ */
261
+ child() {
262
+ return new Feedback({
263
+ isTTY: this.isTTY,
264
+ indent: this.indent + 1,
265
+ quiet: this.quiet,
266
+ verbose: this.verbose,
267
+ });
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Spinner for indeterminate progress
273
+ */
274
+ class FeedbackSpinner {
275
+ constructor(message, options = {}) {
276
+ this.message = message;
277
+ this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
278
+ this.indent = options.indent || 0;
279
+ this.interval = options.interval || 80;
280
+ this.frameIndex = 0;
281
+ this.timer = null;
282
+ this.startTime = null;
283
+ }
284
+
285
+ _prefix() {
286
+ return ' '.repeat(this.indent);
287
+ }
288
+
289
+ /**
290
+ * Start the spinner
291
+ * @returns {FeedbackSpinner}
292
+ */
293
+ start() {
294
+ if (!this.isTTY) {
295
+ console.log(`${this._prefix()}${c.dim}${this.message}${c.reset}`);
296
+ return this;
297
+ }
298
+
299
+ this.startTime = Date.now();
300
+ this.render();
301
+ this.timer = setInterval(() => {
302
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
303
+ this.render();
304
+ }, this.interval);
305
+
306
+ return this;
307
+ }
308
+
309
+ /**
310
+ * Update spinner message
311
+ * @param {string} message - New message
312
+ * @returns {FeedbackSpinner}
313
+ */
314
+ update(message) {
315
+ this.message = message;
316
+ if (this.isTTY && this.timer) {
317
+ this.render();
318
+ }
319
+ return this;
320
+ }
321
+
322
+ /**
323
+ * Render spinner frame
324
+ * @private
325
+ */
326
+ render() {
327
+ if (!this.isTTY) return;
328
+ const frame = SPINNER_FRAMES[this.frameIndex];
329
+ const line = `${this._prefix()}${c.cyan}${frame}${c.reset} ${this.message}`;
330
+ process.stdout.clearLine(0);
331
+ process.stdout.cursorTo(0);
332
+ process.stdout.write(line);
333
+ }
334
+
335
+ /**
336
+ * Stop spinner with result
337
+ * @param {string} symbol - Symbol to show
338
+ * @param {string} [message] - Final message
339
+ * @param {string} [color] - Color code
340
+ * @returns {FeedbackSpinner}
341
+ */
342
+ stop(symbol, message = null, color = '') {
343
+ if (this.timer) {
344
+ clearInterval(this.timer);
345
+ this.timer = null;
346
+ }
347
+
348
+ if (this.isTTY) {
349
+ process.stdout.clearLine(0);
350
+ process.stdout.cursorTo(0);
351
+ }
352
+
353
+ const elapsed = this.startTime ? Date.now() - this.startTime : 0;
354
+ const suffix =
355
+ elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
356
+ const msg = message || this.message;
357
+
358
+ console.log(`${this._prefix()}${color}${symbol}${c.reset} ${msg}${suffix}`);
359
+ return this;
360
+ }
361
+
362
+ succeed(message = null) {
363
+ return this.stop(SYMBOLS.success, message, c.green);
364
+ }
365
+ fail(message = null) {
366
+ return this.stop(SYMBOLS.error, message, c.red);
367
+ }
368
+ warn(message = null) {
369
+ return this.stop(SYMBOLS.warning, message, c.yellow);
370
+ }
371
+ info(message = null) {
372
+ return this.stop(SYMBOLS.info, message, c.blue);
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Task tracker for multi-step operations
378
+ */
379
+ class FeedbackTask {
380
+ constructor(name, totalSteps = null, options = {}) {
381
+ this.name = name;
382
+ this.totalSteps = totalSteps;
383
+ this.currentStep = 0;
384
+ this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
385
+ this.indent = options.indent || 0;
386
+ this.startTime = Date.now();
387
+ this.steps = [];
388
+ }
389
+
390
+ _prefix() {
391
+ return ' '.repeat(this.indent);
392
+ }
393
+
394
+ /**
395
+ * Start the task (optional - for explicit start message)
396
+ * @param {string} [message] - Start message
397
+ * @returns {FeedbackTask}
398
+ */
399
+ start(message = null) {
400
+ const msg = message || this.name;
401
+ console.log(`${this._prefix()}${c.cyan}${SYMBOLS.active}${c.reset} ${msg}`);
402
+ return this;
403
+ }
404
+
405
+ /**
406
+ * Mark a step as complete
407
+ * @param {string} message - Step message
408
+ * @returns {FeedbackTask}
409
+ */
410
+ step(message) {
411
+ this.currentStep++;
412
+ this.steps.push(message);
413
+
414
+ let stepInfo = '';
415
+ if (this.totalSteps) {
416
+ stepInfo = ` ${c.dim}[${this.currentStep}/${this.totalSteps}]${c.reset}`;
417
+ }
418
+
419
+ console.log(`${this._prefix()} ${c.green}${SYMBOLS.success}${c.reset} ${message}${stepInfo}`);
420
+ return this;
421
+ }
422
+
423
+ /**
424
+ * Log a sub-item (dimmed, no step counting)
425
+ * @param {string} message - Item message
426
+ * @returns {FeedbackTask}
427
+ */
428
+ item(message) {
429
+ console.log(`${this._prefix()} ${c.dim}${SYMBOLS.bullet} ${message}${c.reset}`);
430
+ return this;
431
+ }
432
+
433
+ /**
434
+ * Complete the task successfully
435
+ * @param {string} [message] - Completion message
436
+ * @returns {FeedbackTask}
437
+ */
438
+ complete(message = null) {
439
+ const elapsed = Date.now() - this.startTime;
440
+ const suffix =
441
+ elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
442
+ const msg = message || `${this.name} complete`;
443
+
444
+ console.log(`${this._prefix()}${c.green}${SYMBOLS.success}${c.reset} ${msg}${suffix}`);
445
+ return this;
446
+ }
447
+
448
+ /**
449
+ * Fail the task
450
+ * @param {string} [message] - Failure message
451
+ * @returns {FeedbackTask}
452
+ */
453
+ fail(message = null) {
454
+ const msg = message || `${this.name} failed`;
455
+ console.log(`${this._prefix()}${c.red}${SYMBOLS.error}${c.reset} ${msg}`);
456
+ return this;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Progress bar for determinate progress
462
+ */
463
+ class FeedbackProgressBar {
464
+ constructor(label, total, options = {}) {
465
+ this.label = label;
466
+ this.total = total;
467
+ this.current = 0;
468
+ this.width = options.width || 30;
469
+ this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
470
+ this.indent = options.indent || 0;
471
+ this.startTime = Date.now();
472
+ this.lastPrintedPercent = -1;
473
+ }
474
+
475
+ _prefix() {
476
+ return ' '.repeat(this.indent);
477
+ }
478
+
479
+ /**
480
+ * Update progress
481
+ * @param {number} current - Current count
482
+ * @param {string} [item] - Current item name
483
+ * @returns {FeedbackProgressBar}
484
+ */
485
+ update(current, item = null) {
486
+ this.current = current;
487
+
488
+ if (!this.isTTY) {
489
+ // Non-TTY: print every 10% or on completion
490
+ const percent = Math.floor((current / this.total) * 100);
491
+ if (percent % 10 === 0 && percent !== this.lastPrintedPercent) {
492
+ this.lastPrintedPercent = percent;
493
+ console.log(`${this._prefix()}${this.label}: ${percent}% (${current}/${this.total})`);
494
+ }
495
+ return this;
496
+ }
497
+
498
+ const percent = this.total > 0 ? current / this.total : 0;
499
+ const filled = Math.round(this.width * percent);
500
+ const empty = this.width - filled;
501
+
502
+ const bar = `${c.green}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
503
+ const percentStr = `${Math.round(percent * 100)}%`.padStart(4);
504
+ const countStr = `${current}/${this.total}`;
505
+ const itemStr = item ? ` ${c.dim}${item}${c.reset}` : '';
506
+
507
+ process.stdout.clearLine(0);
508
+ process.stdout.cursorTo(0);
509
+ process.stdout.write(
510
+ `${this._prefix()}${this.label} ${bar} ${percentStr} (${countStr})${itemStr}`
511
+ );
512
+
513
+ return this;
514
+ }
515
+
516
+ /**
517
+ * Increment by 1
518
+ * @param {string} [item] - Current item name
519
+ * @returns {FeedbackProgressBar}
520
+ */
521
+ increment(item = null) {
522
+ return this.update(this.current + 1, item);
523
+ }
524
+
525
+ /**
526
+ * Complete the progress bar
527
+ * @param {string} [message] - Completion message
528
+ * @returns {FeedbackProgressBar}
529
+ */
530
+ complete(message = null) {
531
+ if (this.isTTY) {
532
+ process.stdout.clearLine(0);
533
+ process.stdout.cursorTo(0);
534
+ }
535
+
536
+ const elapsed = Date.now() - this.startTime;
537
+ const suffix =
538
+ elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
539
+ const msg = message || `${this.label} complete`;
540
+
541
+ console.log(
542
+ `${this._prefix()}${c.green}${SYMBOLS.success}${c.reset} ${msg} (${this.total} items)${suffix}`
543
+ );
544
+ return this;
545
+ }
546
+ }
547
+
548
+ // Singleton instance for convenience
549
+ const feedback = new Feedback();
550
+
551
+ module.exports = {
552
+ Feedback,
553
+ FeedbackSpinner,
554
+ FeedbackTask,
555
+ FeedbackProgressBar,
556
+ feedback,
557
+ SYMBOLS,
558
+ SPINNER_FRAMES,
559
+ DOHERTY_THRESHOLD_MS,
560
+ formatDuration,
561
+ };