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,519 @@
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
215
+ .map((cell, i) => padString(cell, colWidths[i], align[i] || 'left'))
216
+ .join(' ');
217
+ lines.push(indent + rowLine);
218
+ }
219
+ } else {
220
+ // Tab-separated for piping (still respect indent)
221
+ lines.push(indent + stringHeaders.join('\t'));
222
+ for (const row of stringRows) {
223
+ lines.push(indent + row.join('\t'));
224
+ }
225
+ }
226
+ } else {
227
+ // Bordered table
228
+ const padding = 1;
229
+ const paddedWidths = colWidths.map(w => w + padding * 2);
230
+
231
+ // Top border
232
+ const topBorder =
233
+ box.topLeft + paddedWidths.map(w => box.horizontal.repeat(w)).join(box.topT) + 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(
242
+ indent +
243
+ c.dim +
244
+ box.vertical +
245
+ c.reset +
246
+ headerCells.join(c.dim + box.vertical + c.reset) +
247
+ c.dim +
248
+ box.vertical +
249
+ c.reset
250
+ );
251
+
252
+ // Header separator
253
+ const headerSep =
254
+ box.leftT + paddedWidths.map(w => box.horizontal.repeat(w)).join(box.cross) + box.rightT;
255
+ lines.push(indent + c.dim + headerSep + c.reset);
256
+
257
+ // Data rows
258
+ for (const row of stringRows) {
259
+ const cells = row.map((cell, i) => {
260
+ const padded = padString(cell, colWidths[i], align[i] || 'left');
261
+ return ' ' + padded + ' ';
262
+ });
263
+ lines.push(
264
+ indent +
265
+ c.dim +
266
+ box.vertical +
267
+ c.reset +
268
+ cells.join(c.dim + box.vertical + c.reset) +
269
+ c.dim +
270
+ box.vertical +
271
+ c.reset
272
+ );
273
+ }
274
+
275
+ // Bottom border
276
+ const bottomBorder =
277
+ box.bottomLeft +
278
+ paddedWidths.map(w => box.horizontal.repeat(w)).join(box.bottomT) +
279
+ box.bottomRight;
280
+ lines.push(indent + c.dim + bottomBorder + c.reset);
281
+ }
282
+
283
+ return lines.join('\n');
284
+ }
285
+
286
+ /**
287
+ * Format key-value pairs
288
+ *
289
+ * @param {Object|Map|Array<[string,any]>} data - Key-value data
290
+ * @param {Object} [options={}] - Formatting options
291
+ * @param {string} [options.separator=':'] - Key-value separator
292
+ * @param {boolean} [options.alignValues=true] - Align all values to same column
293
+ * @param {string} [options.indent=''] - Prefix each line
294
+ * @param {string} [options.keyColor] - Color code for keys
295
+ * @param {string} [options.valueColor] - Color code for values
296
+ * @returns {string} Formatted key-value pairs
297
+ *
298
+ * @example
299
+ * formatKeyValue({
300
+ * Version: '2.0.0',
301
+ * Status: 'Active',
302
+ * 'Last Update': '2024-01-15'
303
+ * });
304
+ */
305
+ function formatKeyValue(data, options = {}) {
306
+ const {
307
+ separator = ':',
308
+ alignValues = true,
309
+ indent = '',
310
+ keyColor = c.bold,
311
+ valueColor = '',
312
+ } = options;
313
+
314
+ // Convert to array of [key, value] pairs
315
+ let pairs;
316
+ if (data instanceof Map) {
317
+ pairs = Array.from(data.entries());
318
+ } else if (Array.isArray(data)) {
319
+ pairs = data;
320
+ } else {
321
+ pairs = Object.entries(data);
322
+ }
323
+
324
+ // Convert values to strings
325
+ pairs = pairs.map(([k, v]) => [String(k), String(v ?? '')]);
326
+
327
+ if (!isTTY()) {
328
+ // Plain text for piping (still respect indent)
329
+ return pairs.map(([k, v]) => `${indent}${k}${separator} ${v}`).join('\n');
330
+ }
331
+
332
+ // Calculate key width for alignment
333
+ const maxKeyWidth = alignValues ? Math.max(...pairs.map(([k]) => visibleWidth(k))) : 0;
334
+
335
+ const lines = pairs.map(([key, value]) => {
336
+ const paddedKey = alignValues ? padString(key, maxKeyWidth, 'left') : key;
337
+ return `${indent}${keyColor}${paddedKey}${c.reset}${separator} ${valueColor}${value}${valueColor ? c.reset : ''}`;
338
+ });
339
+
340
+ return lines.join('\n');
341
+ }
342
+
343
+ /**
344
+ * Format a list of items
345
+ *
346
+ * @param {(string|{text:string,status?:string})[]} items - List items
347
+ * @param {Object} [options={}] - Formatting options
348
+ * @param {string} [options.bullet='disc'] - Bullet style ('disc', 'circle', 'square', 'dash', 'arrow', 'check', 'cross', 'star', 'number')
349
+ * @param {string} [options.indent=''] - Prefix each line
350
+ * @param {boolean} [options.numbered=false] - Use numbers instead of bullets
351
+ * @param {number} [options.startNumber=1] - Starting number for numbered lists
352
+ * @param {string} [options.itemColor] - Color code for items
353
+ * @returns {string} Formatted list
354
+ *
355
+ * @example
356
+ * formatList(['Item 1', 'Item 2', 'Item 3'], { bullet: 'check' });
357
+ *
358
+ * // With status indicators
359
+ * formatList([
360
+ * { text: 'Task 1', status: 'done' },
361
+ * { text: 'Task 2', status: 'pending' }
362
+ * ]);
363
+ */
364
+ function formatList(items, options = {}) {
365
+ const {
366
+ bullet = 'disc',
367
+ indent = '',
368
+ numbered = false,
369
+ startNumber = 1,
370
+ itemColor = '',
371
+ } = options;
372
+
373
+ if (!isTTY()) {
374
+ // Plain text for piping
375
+ return items
376
+ .map((item, i) => {
377
+ const text = typeof item === 'object' ? item.text : item;
378
+ const prefix = numbered ? `${startNumber + i}.` : '-';
379
+ return `${prefix} ${text}`;
380
+ })
381
+ .join('\n');
382
+ }
383
+
384
+ const bulletChar = BULLETS[bullet] || BULLETS.disc;
385
+ const numWidth = numbered ? String(startNumber + items.length - 1).length : 0;
386
+
387
+ const lines = items.map((item, i) => {
388
+ const text = typeof item === 'object' ? item.text : String(item);
389
+ const status = typeof item === 'object' ? item.status : null;
390
+
391
+ let prefix;
392
+ if (numbered) {
393
+ prefix = `${String(startNumber + i).padStart(numWidth)}.`;
394
+ } else if (status) {
395
+ // Status-based bullet
396
+ switch (status) {
397
+ case 'done':
398
+ case 'completed':
399
+ case 'success':
400
+ prefix = c.green + BULLETS.check + c.reset;
401
+ break;
402
+ case 'error':
403
+ case 'failed':
404
+ prefix = c.red + BULLETS.cross + c.reset;
405
+ break;
406
+ case 'pending':
407
+ case 'waiting':
408
+ prefix = c.yellow + BULLETS.circle + c.reset;
409
+ break;
410
+ case 'active':
411
+ case 'in_progress':
412
+ prefix = c.cyan + BULLETS.disc + c.reset;
413
+ break;
414
+ default:
415
+ prefix = c.dim + bulletChar + c.reset;
416
+ }
417
+ } else {
418
+ prefix = c.dim + bulletChar + c.reset;
419
+ }
420
+
421
+ const coloredText = itemColor ? `${itemColor}${text}${c.reset}` : text;
422
+ return `${indent}${prefix} ${coloredText}`;
423
+ });
424
+
425
+ return lines.join('\n');
426
+ }
427
+
428
+ /**
429
+ * Format a horizontal divider
430
+ *
431
+ * @param {Object} [options={}] - Formatting options
432
+ * @param {number} [options.width] - Divider width (default: terminal width)
433
+ * @param {string} [options.char='─'] - Character to use
434
+ * @param {string} [options.style] - Style ('single', 'double', 'dotted', 'bold')
435
+ * @returns {string} Formatted divider
436
+ */
437
+ function formatDivider(options = {}) {
438
+ const { width = process.stdout.columns || 80, char, style = 'single' } = options;
439
+
440
+ if (!isTTY()) {
441
+ return '-'.repeat(Math.min(width, 80));
442
+ }
443
+
444
+ const chars = {
445
+ single: '─',
446
+ double: '═',
447
+ dotted: '┄',
448
+ bold: '━',
449
+ };
450
+
451
+ const divChar = char || chars[style] || chars.single;
452
+ return c.dim + divChar.repeat(width) + c.reset;
453
+ }
454
+
455
+ /**
456
+ * Format a section header
457
+ *
458
+ * @param {string} title - Section title
459
+ * @param {Object} [options={}] - Formatting options
460
+ * @param {string} [options.subtitle] - Optional subtitle
461
+ * @param {string} [options.indent=''] - Prefix
462
+ * @returns {string} Formatted header
463
+ */
464
+ function formatHeader(title, options = {}) {
465
+ const { subtitle, indent = '' } = options;
466
+
467
+ if (!isTTY()) {
468
+ let header = `\n${title}`;
469
+ if (subtitle) header += ` - ${subtitle}`;
470
+ header +=
471
+ '\n' + '='.repeat(Math.min(visibleWidth(title) + (subtitle ? subtitle.length + 3 : 0), 40));
472
+ return header;
473
+ }
474
+
475
+ let header = `\n${indent}${c.bold}${title}${c.reset}`;
476
+ if (subtitle) {
477
+ header += ` ${c.dim}${subtitle}${c.reset}`;
478
+ }
479
+
480
+ return header;
481
+ }
482
+
483
+ /**
484
+ * Truncate a string to fit within a width
485
+ *
486
+ * @param {string} str - String to truncate
487
+ * @param {number} maxWidth - Maximum width
488
+ * @param {string} [suffix='…'] - Suffix to add when truncated
489
+ * @returns {string} Truncated string
490
+ */
491
+ function truncate(str, maxWidth, suffix = '…') {
492
+ if (typeof str !== 'string') str = String(str);
493
+ const width = visibleWidth(str);
494
+ if (width <= maxWidth) return str;
495
+
496
+ const suffixWidth = visibleWidth(suffix);
497
+ const targetWidth = maxWidth - suffixWidth;
498
+
499
+ // Need to handle ANSI codes carefully
500
+ const plain = stripAnsi(str);
501
+ if (plain.length <= maxWidth) return str;
502
+
503
+ return plain.slice(0, targetWidth) + suffix;
504
+ }
505
+
506
+ module.exports = {
507
+ formatTable,
508
+ formatKeyValue,
509
+ formatList,
510
+ formatDivider,
511
+ formatHeader,
512
+ truncate,
513
+ stripAnsi,
514
+ visibleWidth,
515
+ padString,
516
+ isTTY,
517
+ BOX_CHARS,
518
+ BULLETS,
519
+ };