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.
Files changed (37) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -3
  3. package/lib/placeholder-registry.js +617 -0
  4. package/lib/smart-json-file.js +205 -1
  5. package/lib/table-formatter.js +504 -0
  6. package/lib/transient-status.js +374 -0
  7. package/lib/ui-manager.js +612 -0
  8. package/lib/validate-args.js +213 -0
  9. package/lib/validate-names.js +143 -0
  10. package/lib/validate-paths.js +434 -0
  11. package/lib/validate.js +37 -737
  12. package/package.json +4 -1
  13. package/scripts/check-update.js +16 -3
  14. package/scripts/lib/sessionRegistry.js +682 -0
  15. package/scripts/session-manager.js +77 -10
  16. package/scripts/tui/App.js +176 -0
  17. package/scripts/tui/index.js +75 -0
  18. package/scripts/tui/lib/crashRecovery.js +302 -0
  19. package/scripts/tui/lib/eventStream.js +316 -0
  20. package/scripts/tui/lib/keyboard.js +252 -0
  21. package/scripts/tui/lib/loopControl.js +371 -0
  22. package/scripts/tui/panels/OutputPanel.js +278 -0
  23. package/scripts/tui/panels/SessionPanel.js +178 -0
  24. package/scripts/tui/panels/TracePanel.js +333 -0
  25. package/src/core/commands/tui.md +91 -0
  26. package/tools/cli/commands/config.js +7 -30
  27. package/tools/cli/commands/doctor.js +18 -38
  28. package/tools/cli/commands/list.js +47 -35
  29. package/tools/cli/commands/status.js +13 -37
  30. package/tools/cli/commands/uninstall.js +9 -38
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +374 -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 +16 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -0,0 +1,504 @@
1
+ /**
2
+ * table-formatter.js - Unified Table and List Formatting
3
+ *
4
+ * Provides consistent, TTY-aware formatting for tables, key-value pairs,
5
+ * and lists across all CLI commands.
6
+ *
7
+ * Features:
8
+ * - TTY detection for smart formatting
9
+ * - Unicode box-drawing for borders
10
+ * - Auto-width calculation
11
+ * - Column alignment
12
+ * - Safe for piping (plain text when not TTY)
13
+ *
14
+ * Usage:
15
+ * const { formatTable, formatKeyValue, formatList } = require('./table-formatter');
16
+ *
17
+ * // Table with headers
18
+ * console.log(formatTable(
19
+ * ['Name', 'Status', 'Count'],
20
+ * [['Story 1', 'Done', '5'], ['Story 2', 'In Progress', '3']]
21
+ * ));
22
+ *
23
+ * // Key-value pairs
24
+ * console.log(formatKeyValue({ Version: '1.0.0', Status: 'Active' }));
25
+ *
26
+ * // Simple list
27
+ * console.log(formatList(['Item 1', 'Item 2', 'Item 3']));
28
+ */
29
+
30
+ const { c } = require('./colors');
31
+
32
+ // Box-drawing characters for different styles
33
+ const BOX_CHARS = {
34
+ // Single-line box
35
+ single: {
36
+ topLeft: '┌',
37
+ topRight: '┐',
38
+ bottomLeft: '└',
39
+ bottomRight: '┘',
40
+ horizontal: '─',
41
+ vertical: '│',
42
+ cross: '┼',
43
+ topT: '┬',
44
+ bottomT: '┴',
45
+ leftT: '├',
46
+ rightT: '┤',
47
+ },
48
+ // Double-line box
49
+ double: {
50
+ topLeft: '╔',
51
+ topRight: '╗',
52
+ bottomLeft: '╚',
53
+ bottomRight: '╝',
54
+ horizontal: '═',
55
+ vertical: '║',
56
+ cross: '╬',
57
+ topT: '╦',
58
+ bottomT: '╩',
59
+ leftT: '╠',
60
+ rightT: '╣',
61
+ },
62
+ // Rounded corners
63
+ rounded: {
64
+ topLeft: '╭',
65
+ topRight: '╮',
66
+ bottomLeft: '╰',
67
+ bottomRight: '╯',
68
+ horizontal: '─',
69
+ vertical: '│',
70
+ cross: '┼',
71
+ topT: '┬',
72
+ bottomT: '┴',
73
+ leftT: '├',
74
+ rightT: '┤',
75
+ },
76
+ // ASCII fallback
77
+ ascii: {
78
+ topLeft: '+',
79
+ topRight: '+',
80
+ bottomLeft: '+',
81
+ bottomRight: '+',
82
+ horizontal: '-',
83
+ vertical: '|',
84
+ cross: '+',
85
+ topT: '+',
86
+ bottomT: '+',
87
+ leftT: '+',
88
+ rightT: '+',
89
+ },
90
+ };
91
+
92
+ // List bullet styles
93
+ const BULLETS = {
94
+ disc: '•',
95
+ circle: '○',
96
+ square: '■',
97
+ dash: '-',
98
+ arrow: '→',
99
+ check: '✓',
100
+ cross: '✗',
101
+ star: '★',
102
+ };
103
+
104
+ /**
105
+ * Strip ANSI escape codes from a string
106
+ * @param {string} str - String potentially containing ANSI codes
107
+ * @returns {string} Plain text without ANSI codes
108
+ */
109
+ function stripAnsi(str) {
110
+ if (typeof str !== 'string') return String(str);
111
+ // eslint-disable-next-line no-control-regex
112
+ return str.replace(/\x1B\[[0-9;]*m/g, '');
113
+ }
114
+
115
+ /**
116
+ * Get visible width of a string (excluding ANSI codes)
117
+ * @param {string} str - String to measure
118
+ * @returns {number} Visible character count
119
+ */
120
+ function visibleWidth(str) {
121
+ return stripAnsi(str).length;
122
+ }
123
+
124
+ /**
125
+ * Pad a string to a specified width (accounting for ANSI codes)
126
+ * @param {string} str - String to pad
127
+ * @param {number} width - Target width
128
+ * @param {string} align - Alignment ('left', 'right', 'center')
129
+ * @returns {string} Padded string
130
+ */
131
+ function padString(str, width, align = 'left') {
132
+ const visible = visibleWidth(str);
133
+ const padding = Math.max(0, width - visible);
134
+
135
+ if (align === 'right') {
136
+ return ' '.repeat(padding) + str;
137
+ }
138
+ if (align === 'center') {
139
+ const leftPad = Math.floor(padding / 2);
140
+ const rightPad = padding - leftPad;
141
+ return ' '.repeat(leftPad) + str + ' '.repeat(rightPad);
142
+ }
143
+ // Default: left
144
+ return str + ' '.repeat(padding);
145
+ }
146
+
147
+ /**
148
+ * Check if output is going to a TTY
149
+ * @returns {boolean}
150
+ */
151
+ function isTTY() {
152
+ return process.stdout.isTTY === true;
153
+ }
154
+
155
+ /**
156
+ * Format data as a table
157
+ *
158
+ * @param {string[]} headers - Column headers
159
+ * @param {(string|number)[][]} rows - Table rows (array of arrays)
160
+ * @param {Object} [options={}] - Formatting options
161
+ * @param {string} [options.style='rounded'] - Box style ('single', 'double', 'rounded', 'ascii', 'none')
162
+ * @param {string[]} [options.align] - Column alignments ('left', 'right', 'center')
163
+ * @param {number} [options.maxWidth] - Maximum table width
164
+ * @param {boolean} [options.compact=false] - Compact mode (no borders)
165
+ * @param {string} [options.indent=''] - Prefix each line with this string
166
+ * @returns {string} Formatted table
167
+ *
168
+ * @example
169
+ * formatTable(
170
+ * ['Name', 'Status'],
171
+ * [['US-0001', 'Done'], ['US-0002', 'In Progress']],
172
+ * { style: 'rounded' }
173
+ * );
174
+ */
175
+ function formatTable(headers, rows, options = {}) {
176
+ const {
177
+ style = 'rounded',
178
+ align = [],
179
+ maxWidth = process.stdout.columns || 80,
180
+ compact = false,
181
+ indent = '',
182
+ } = options;
183
+
184
+ // Convert all values to strings
185
+ const stringHeaders = headers.map(h => String(h ?? ''));
186
+ const stringRows = rows.map(row => row.map(cell => String(cell ?? '')));
187
+
188
+ // Calculate column widths
189
+ const colWidths = stringHeaders.map((h, i) => {
190
+ const headerWidth = visibleWidth(h);
191
+ const maxCellWidth = stringRows.reduce((max, row) => {
192
+ const cellWidth = visibleWidth(row[i] || '');
193
+ return Math.max(max, cellWidth);
194
+ }, 0);
195
+ return Math.max(headerWidth, maxCellWidth);
196
+ });
197
+
198
+ // Use compact/no-border for non-TTY
199
+ const useCompact = compact || !isTTY();
200
+ const box = useCompact ? null : BOX_CHARS[style] || BOX_CHARS.ascii;
201
+
202
+ const lines = [];
203
+
204
+ if (useCompact) {
205
+ // Compact mode: tab-separated or space-padded
206
+ if (isTTY()) {
207
+ // Padded columns
208
+ const headerLine = stringHeaders
209
+ .map((h, i) => padString(h, colWidths[i], align[i] || 'left'))
210
+ .join(' ');
211
+ lines.push(indent + c.bold + headerLine + c.reset);
212
+
213
+ for (const row of stringRows) {
214
+ const rowLine = row.map((cell, i) => padString(cell, colWidths[i], align[i] || 'left')).join(' ');
215
+ lines.push(indent + rowLine);
216
+ }
217
+ } else {
218
+ // Tab-separated for piping (still respect indent)
219
+ lines.push(indent + stringHeaders.join('\t'));
220
+ for (const row of stringRows) {
221
+ lines.push(indent + row.join('\t'));
222
+ }
223
+ }
224
+ } else {
225
+ // Bordered table
226
+ const padding = 1;
227
+ const paddedWidths = colWidths.map(w => w + padding * 2);
228
+
229
+ // Top border
230
+ const topBorder =
231
+ box.topLeft +
232
+ paddedWidths.map(w => box.horizontal.repeat(w)).join(box.topT) +
233
+ box.topRight;
234
+ lines.push(indent + c.dim + topBorder + c.reset);
235
+
236
+ // Header row
237
+ const headerCells = stringHeaders.map((h, i) => {
238
+ const padded = padString(h, colWidths[i], align[i] || 'left');
239
+ return ' ' + c.bold + padded + c.reset + ' ';
240
+ });
241
+ lines.push(indent + c.dim + box.vertical + c.reset + headerCells.join(c.dim + box.vertical + c.reset) + c.dim + box.vertical + c.reset);
242
+
243
+ // Header separator
244
+ const headerSep =
245
+ box.leftT +
246
+ paddedWidths.map(w => box.horizontal.repeat(w)).join(box.cross) +
247
+ box.rightT;
248
+ lines.push(indent + c.dim + headerSep + c.reset);
249
+
250
+ // Data rows
251
+ for (const row of stringRows) {
252
+ const cells = row.map((cell, i) => {
253
+ const padded = padString(cell, colWidths[i], align[i] || 'left');
254
+ return ' ' + padded + ' ';
255
+ });
256
+ lines.push(indent + c.dim + box.vertical + c.reset + cells.join(c.dim + box.vertical + c.reset) + c.dim + box.vertical + c.reset);
257
+ }
258
+
259
+ // Bottom border
260
+ const bottomBorder =
261
+ box.bottomLeft +
262
+ paddedWidths.map(w => box.horizontal.repeat(w)).join(box.bottomT) +
263
+ box.bottomRight;
264
+ lines.push(indent + c.dim + bottomBorder + c.reset);
265
+ }
266
+
267
+ return lines.join('\n');
268
+ }
269
+
270
+ /**
271
+ * Format key-value pairs
272
+ *
273
+ * @param {Object|Map|Array<[string,any]>} data - Key-value data
274
+ * @param {Object} [options={}] - Formatting options
275
+ * @param {string} [options.separator=':'] - Key-value separator
276
+ * @param {boolean} [options.alignValues=true] - Align all values to same column
277
+ * @param {string} [options.indent=''] - Prefix each line
278
+ * @param {string} [options.keyColor] - Color code for keys
279
+ * @param {string} [options.valueColor] - Color code for values
280
+ * @returns {string} Formatted key-value pairs
281
+ *
282
+ * @example
283
+ * formatKeyValue({
284
+ * Version: '2.0.0',
285
+ * Status: 'Active',
286
+ * 'Last Update': '2024-01-15'
287
+ * });
288
+ */
289
+ function formatKeyValue(data, options = {}) {
290
+ const {
291
+ separator = ':',
292
+ alignValues = true,
293
+ indent = '',
294
+ keyColor = c.bold,
295
+ valueColor = '',
296
+ } = options;
297
+
298
+ // Convert to array of [key, value] pairs
299
+ let pairs;
300
+ if (data instanceof Map) {
301
+ pairs = Array.from(data.entries());
302
+ } else if (Array.isArray(data)) {
303
+ pairs = data;
304
+ } else {
305
+ pairs = Object.entries(data);
306
+ }
307
+
308
+ // Convert values to strings
309
+ pairs = pairs.map(([k, v]) => [String(k), String(v ?? '')]);
310
+
311
+ if (!isTTY()) {
312
+ // Plain text for piping (still respect indent)
313
+ return pairs.map(([k, v]) => `${indent}${k}${separator} ${v}`).join('\n');
314
+ }
315
+
316
+ // Calculate key width for alignment
317
+ const maxKeyWidth = alignValues
318
+ ? Math.max(...pairs.map(([k]) => visibleWidth(k)))
319
+ : 0;
320
+
321
+ const lines = pairs.map(([key, value]) => {
322
+ const paddedKey = alignValues ? padString(key, maxKeyWidth, 'left') : key;
323
+ return `${indent}${keyColor}${paddedKey}${c.reset}${separator} ${valueColor}${value}${valueColor ? c.reset : ''}`;
324
+ });
325
+
326
+ return lines.join('\n');
327
+ }
328
+
329
+ /**
330
+ * Format a list of items
331
+ *
332
+ * @param {(string|{text:string,status?:string})[]} items - List items
333
+ * @param {Object} [options={}] - Formatting options
334
+ * @param {string} [options.bullet='disc'] - Bullet style ('disc', 'circle', 'square', 'dash', 'arrow', 'check', 'cross', 'star', 'number')
335
+ * @param {string} [options.indent=''] - Prefix each line
336
+ * @param {boolean} [options.numbered=false] - Use numbers instead of bullets
337
+ * @param {number} [options.startNumber=1] - Starting number for numbered lists
338
+ * @param {string} [options.itemColor] - Color code for items
339
+ * @returns {string} Formatted list
340
+ *
341
+ * @example
342
+ * formatList(['Item 1', 'Item 2', 'Item 3'], { bullet: 'check' });
343
+ *
344
+ * // With status indicators
345
+ * formatList([
346
+ * { text: 'Task 1', status: 'done' },
347
+ * { text: 'Task 2', status: 'pending' }
348
+ * ]);
349
+ */
350
+ function formatList(items, options = {}) {
351
+ const {
352
+ bullet = 'disc',
353
+ indent = '',
354
+ numbered = false,
355
+ startNumber = 1,
356
+ itemColor = '',
357
+ } = options;
358
+
359
+ if (!isTTY()) {
360
+ // Plain text for piping
361
+ return items
362
+ .map((item, i) => {
363
+ const text = typeof item === 'object' ? item.text : item;
364
+ const prefix = numbered ? `${startNumber + i}.` : '-';
365
+ return `${prefix} ${text}`;
366
+ })
367
+ .join('\n');
368
+ }
369
+
370
+ const bulletChar = BULLETS[bullet] || BULLETS.disc;
371
+ const numWidth = numbered ? String(startNumber + items.length - 1).length : 0;
372
+
373
+ const lines = items.map((item, i) => {
374
+ const text = typeof item === 'object' ? item.text : String(item);
375
+ const status = typeof item === 'object' ? item.status : null;
376
+
377
+ let prefix;
378
+ if (numbered) {
379
+ prefix = `${String(startNumber + i).padStart(numWidth)}.`;
380
+ } else if (status) {
381
+ // Status-based bullet
382
+ switch (status) {
383
+ case 'done':
384
+ case 'completed':
385
+ case 'success':
386
+ prefix = c.green + BULLETS.check + c.reset;
387
+ break;
388
+ case 'error':
389
+ case 'failed':
390
+ prefix = c.red + BULLETS.cross + c.reset;
391
+ break;
392
+ case 'pending':
393
+ case 'waiting':
394
+ prefix = c.yellow + BULLETS.circle + c.reset;
395
+ break;
396
+ case 'active':
397
+ case 'in_progress':
398
+ prefix = c.cyan + BULLETS.disc + c.reset;
399
+ break;
400
+ default:
401
+ prefix = c.dim + bulletChar + c.reset;
402
+ }
403
+ } else {
404
+ prefix = c.dim + bulletChar + c.reset;
405
+ }
406
+
407
+ const coloredText = itemColor ? `${itemColor}${text}${c.reset}` : text;
408
+ return `${indent}${prefix} ${coloredText}`;
409
+ });
410
+
411
+ return lines.join('\n');
412
+ }
413
+
414
+ /**
415
+ * Format a horizontal divider
416
+ *
417
+ * @param {Object} [options={}] - Formatting options
418
+ * @param {number} [options.width] - Divider width (default: terminal width)
419
+ * @param {string} [options.char='─'] - Character to use
420
+ * @param {string} [options.style] - Style ('single', 'double', 'dotted', 'bold')
421
+ * @returns {string} Formatted divider
422
+ */
423
+ function formatDivider(options = {}) {
424
+ const { width = process.stdout.columns || 80, char, style = 'single' } = options;
425
+
426
+ if (!isTTY()) {
427
+ return '-'.repeat(Math.min(width, 80));
428
+ }
429
+
430
+ const chars = {
431
+ single: '─',
432
+ double: '═',
433
+ dotted: '┄',
434
+ bold: '━',
435
+ };
436
+
437
+ const divChar = char || chars[style] || chars.single;
438
+ return c.dim + divChar.repeat(width) + c.reset;
439
+ }
440
+
441
+ /**
442
+ * Format a section header
443
+ *
444
+ * @param {string} title - Section title
445
+ * @param {Object} [options={}] - Formatting options
446
+ * @param {string} [options.subtitle] - Optional subtitle
447
+ * @param {string} [options.indent=''] - Prefix
448
+ * @returns {string} Formatted header
449
+ */
450
+ function formatHeader(title, options = {}) {
451
+ const { subtitle, indent = '' } = options;
452
+
453
+ if (!isTTY()) {
454
+ let header = `\n${title}`;
455
+ if (subtitle) header += ` - ${subtitle}`;
456
+ header += '\n' + '='.repeat(Math.min(visibleWidth(title) + (subtitle ? subtitle.length + 3 : 0), 40));
457
+ return header;
458
+ }
459
+
460
+ let header = `\n${indent}${c.bold}${title}${c.reset}`;
461
+ if (subtitle) {
462
+ header += ` ${c.dim}${subtitle}${c.reset}`;
463
+ }
464
+
465
+ return header;
466
+ }
467
+
468
+ /**
469
+ * Truncate a string to fit within a width
470
+ *
471
+ * @param {string} str - String to truncate
472
+ * @param {number} maxWidth - Maximum width
473
+ * @param {string} [suffix='…'] - Suffix to add when truncated
474
+ * @returns {string} Truncated string
475
+ */
476
+ function truncate(str, maxWidth, suffix = '…') {
477
+ if (typeof str !== 'string') str = String(str);
478
+ const width = visibleWidth(str);
479
+ if (width <= maxWidth) return str;
480
+
481
+ const suffixWidth = visibleWidth(suffix);
482
+ const targetWidth = maxWidth - suffixWidth;
483
+
484
+ // Need to handle ANSI codes carefully
485
+ const plain = stripAnsi(str);
486
+ if (plain.length <= maxWidth) return str;
487
+
488
+ return plain.slice(0, targetWidth) + suffix;
489
+ }
490
+
491
+ module.exports = {
492
+ formatTable,
493
+ formatKeyValue,
494
+ formatList,
495
+ formatDivider,
496
+ formatHeader,
497
+ truncate,
498
+ stripAnsi,
499
+ visibleWidth,
500
+ padString,
501
+ isTTY,
502
+ BOX_CHARS,
503
+ BULLETS,
504
+ };