@wundr.io/cli 1.0.11 → 1.0.13

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 (180) hide show
  1. package/bin/wundr.js +8 -4
  2. package/dist/ai/ai-service.d.ts.map +1 -1
  3. package/dist/ai/ai-service.js.map +1 -1
  4. package/dist/ai/claude-client.js.map +1 -1
  5. package/dist/ai/conversation-manager.js.map +1 -1
  6. package/dist/commands/ai.d.ts.map +1 -1
  7. package/dist/commands/ai.js +179 -24
  8. package/dist/commands/ai.js.map +1 -1
  9. package/dist/commands/analyze-optimized.d.ts.map +1 -1
  10. package/dist/commands/analyze-optimized.js +15 -6
  11. package/dist/commands/analyze-optimized.js.map +1 -1
  12. package/dist/commands/batch.d.ts +22 -0
  13. package/dist/commands/batch.d.ts.map +1 -1
  14. package/dist/commands/batch.js +130 -14
  15. package/dist/commands/batch.js.map +1 -1
  16. package/dist/commands/chat.d.ts +1 -0
  17. package/dist/commands/chat.d.ts.map +1 -1
  18. package/dist/commands/chat.js +7 -3
  19. package/dist/commands/chat.js.map +1 -1
  20. package/dist/commands/claude-init.d.ts +1 -1
  21. package/dist/commands/claude-init.d.ts.map +1 -1
  22. package/dist/commands/claude-init.js +16 -16
  23. package/dist/commands/claude-init.js.map +1 -1
  24. package/dist/commands/claude-setup.d.ts +5 -5
  25. package/dist/commands/claude-setup.d.ts.map +1 -1
  26. package/dist/commands/claude-setup.js +65 -59
  27. package/dist/commands/claude-setup.js.map +1 -1
  28. package/dist/commands/computer-setup.d.ts +1 -0
  29. package/dist/commands/computer-setup.d.ts.map +1 -1
  30. package/dist/commands/computer-setup.js +35 -7
  31. package/dist/commands/computer-setup.js.map +1 -1
  32. package/dist/commands/dashboard.js.map +1 -1
  33. package/dist/commands/govern.js.map +1 -1
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js +3 -3
  36. package/dist/commands/init.js.map +1 -1
  37. package/dist/commands/orchestrator.d.ts.map +1 -1
  38. package/dist/commands/orchestrator.js +11 -4
  39. package/dist/commands/orchestrator.js.map +1 -1
  40. package/dist/commands/performance-optimizer.d.ts.map +1 -1
  41. package/dist/commands/performance-optimizer.js.map +1 -1
  42. package/dist/commands/rag.d.ts.map +1 -1
  43. package/dist/commands/rag.js +9 -6
  44. package/dist/commands/rag.js.map +1 -1
  45. package/dist/commands/setup.d.ts +5 -10
  46. package/dist/commands/setup.d.ts.map +1 -1
  47. package/dist/commands/setup.js +35 -260
  48. package/dist/commands/setup.js.map +1 -1
  49. package/dist/commands/watch.d.ts.map +1 -1
  50. package/dist/commands/watch.js.map +1 -1
  51. package/dist/context/session-manager.js.map +1 -1
  52. package/dist/framework/command-interface.d.ts +349 -0
  53. package/dist/framework/command-interface.d.ts.map +1 -0
  54. package/dist/framework/command-interface.js +101 -0
  55. package/dist/framework/command-interface.js.map +1 -0
  56. package/dist/framework/command-registry.d.ts +173 -0
  57. package/dist/framework/command-registry.d.ts.map +1 -0
  58. package/dist/framework/command-registry.js +734 -0
  59. package/dist/framework/command-registry.js.map +1 -0
  60. package/dist/framework/completion-exporter.d.ts +79 -0
  61. package/dist/framework/completion-exporter.d.ts.map +1 -0
  62. package/dist/framework/completion-exporter.js +259 -0
  63. package/dist/framework/completion-exporter.js.map +1 -0
  64. package/dist/framework/debug-logger.d.ts +163 -0
  65. package/dist/framework/debug-logger.d.ts.map +1 -0
  66. package/dist/framework/debug-logger.js +373 -0
  67. package/dist/framework/debug-logger.js.map +1 -0
  68. package/dist/framework/error-handler.d.ts +196 -0
  69. package/dist/framework/error-handler.d.ts.map +1 -0
  70. package/dist/framework/error-handler.js +613 -0
  71. package/dist/framework/error-handler.js.map +1 -0
  72. package/dist/framework/help-generator.d.ts +78 -0
  73. package/dist/framework/help-generator.d.ts.map +1 -0
  74. package/dist/framework/help-generator.js +414 -0
  75. package/dist/framework/help-generator.js.map +1 -0
  76. package/dist/framework/index.d.ts +62 -0
  77. package/dist/framework/index.d.ts.map +1 -0
  78. package/dist/framework/index.js +95 -0
  79. package/dist/framework/index.js.map +1 -0
  80. package/dist/framework/interactive-repl.d.ts +138 -0
  81. package/dist/framework/interactive-repl.d.ts.map +1 -0
  82. package/dist/framework/interactive-repl.js +567 -0
  83. package/dist/framework/interactive-repl.js.map +1 -0
  84. package/dist/framework/output-formatter.d.ts +274 -0
  85. package/dist/framework/output-formatter.d.ts.map +1 -0
  86. package/dist/framework/output-formatter.js +545 -0
  87. package/dist/framework/output-formatter.js.map +1 -0
  88. package/dist/framework/progress-manager.d.ts +192 -0
  89. package/dist/framework/progress-manager.d.ts.map +1 -0
  90. package/dist/framework/progress-manager.js +408 -0
  91. package/dist/framework/progress-manager.js.map +1 -0
  92. package/dist/interactive/interactive-mode.js.map +1 -1
  93. package/dist/nlp/command-mapper.js.map +1 -1
  94. package/dist/nlp/command-parser.js.map +1 -1
  95. package/dist/nlp/intent-parser.d.ts.map +1 -1
  96. package/dist/nlp/intent-parser.js +4 -2
  97. package/dist/nlp/intent-parser.js.map +1 -1
  98. package/dist/plugins/plugin-manager.d.ts +2 -1
  99. package/dist/plugins/plugin-manager.d.ts.map +1 -1
  100. package/dist/plugins/plugin-manager.js +30 -19
  101. package/dist/plugins/plugin-manager.js.map +1 -1
  102. package/dist/utils/backup-rollback-manager.d.ts.map +1 -1
  103. package/dist/utils/backup-rollback-manager.js +1 -2
  104. package/dist/utils/backup-rollback-manager.js.map +1 -1
  105. package/dist/utils/logger.js.map +1 -1
  106. package/package.json +6 -6
  107. package/src/ai/ai-service.ts +16 -17
  108. package/src/ai/claude-client.ts +16 -16
  109. package/src/ai/conversation-manager.ts +29 -29
  110. package/src/cli.ts +4 -4
  111. package/src/commands/ai.ts +246 -78
  112. package/src/commands/alignment.ts +74 -74
  113. package/src/commands/analyze-optimized.ts +111 -78
  114. package/src/commands/analyze.ts +14 -14
  115. package/src/commands/batch.ts +179 -42
  116. package/src/commands/chat.ts +37 -30
  117. package/src/commands/claude-init.ts +41 -45
  118. package/src/commands/claude-setup.ts +204 -119
  119. package/src/commands/computer-setup.ts +85 -43
  120. package/src/commands/create-command.ts +4 -4
  121. package/src/commands/create.ts +27 -27
  122. package/src/commands/dashboard.ts +24 -24
  123. package/src/commands/govern.ts +25 -25
  124. package/src/commands/governance.ts +34 -34
  125. package/src/commands/guardian.ts +56 -56
  126. package/src/commands/init.ts +25 -22
  127. package/src/commands/orchestrator.ts +68 -41
  128. package/src/commands/performance-optimizer.ts +34 -35
  129. package/src/commands/plugins.ts +27 -27
  130. package/src/commands/project-update.ts +175 -72
  131. package/src/commands/rag.ts +185 -78
  132. package/src/commands/session.ts +35 -35
  133. package/src/commands/setup.ts +40 -344
  134. package/src/commands/test-init.ts +3 -3
  135. package/src/commands/test.ts +4 -4
  136. package/src/commands/watch.ts +28 -29
  137. package/src/commands/worktree.ts +49 -49
  138. package/src/context/context-manager.ts +10 -10
  139. package/src/context/session-manager.ts +41 -41
  140. package/src/framework/command-interface.ts +520 -0
  141. package/src/framework/command-registry.ts +942 -0
  142. package/src/framework/completion-exporter.ts +383 -0
  143. package/src/framework/debug-logger.ts +519 -0
  144. package/src/framework/error-handler.ts +867 -0
  145. package/src/framework/help-generator.ts +540 -0
  146. package/src/framework/index.ts +169 -0
  147. package/src/framework/interactive-repl.ts +703 -0
  148. package/src/framework/output-formatter.ts +834 -0
  149. package/src/framework/progress-manager.ts +539 -0
  150. package/src/index.ts +4 -4
  151. package/src/interactive/interactive-mode.ts +16 -16
  152. package/src/lib/conflict-resolution.ts +799 -9
  153. package/src/lib/merge-strategy.ts +529 -7
  154. package/src/lib/safety-mechanisms.ts +422 -18
  155. package/src/lib/state-detection.ts +1015 -13
  156. package/src/nlp/command-mapper.ts +29 -29
  157. package/src/nlp/command-parser.ts +17 -17
  158. package/src/nlp/intent-classifier.ts +7 -7
  159. package/src/nlp/intent-parser.ts +54 -52
  160. package/src/plugins/plugin-manager.ts +61 -39
  161. package/src/tests/computer-setup-integration.test.ts +46 -15
  162. package/src/types/modules.d.ts +424 -1
  163. package/src/utils/backup-rollback-manager.ts +11 -8
  164. package/src/utils/config-manager.ts +3 -3
  165. package/src/utils/error-handler.ts +2 -2
  166. package/src/utils/logger.ts +22 -22
  167. package/templates/batch/ci-cd.yaml +7 -7
  168. package/test-suites/api/health.spec.ts +20 -23
  169. package/test-suites/helpers/test-config.ts +14 -13
  170. package/test-suites/ui/accessibility.spec.ts +27 -22
  171. package/test-suites/ui/smoke.spec.ts +26 -21
  172. package/dist/commands/computer-setup-commands.d.ts +0 -53
  173. package/dist/commands/computer-setup-commands.d.ts.map +0 -1
  174. package/dist/commands/computer-setup-commands.js +0 -705
  175. package/dist/commands/computer-setup-commands.js.map +0 -1
  176. package/dist/commands/vp.d.ts +0 -7
  177. package/dist/commands/vp.d.ts.map +0 -1
  178. package/dist/commands/vp.js +0 -571
  179. package/dist/commands/vp.js.map +0 -1
  180. package/src/commands/computer-setup-commands.ts +0 -872
@@ -0,0 +1,834 @@
1
+ /**
2
+ * Output Formatter - Consistent output formatting for all CLI commands.
3
+ *
4
+ * Provides:
5
+ * - Table rendering with column alignment and truncation
6
+ * - JSON output (pretty or compact)
7
+ * - Key-value pair formatting
8
+ * - List formatting (ordered/unordered)
9
+ * - Tree rendering for hierarchical data
10
+ * - Progress bars
11
+ * - Status indicators with consistent color coding
12
+ * - Diff formatting
13
+ * - Smart output that respects --json, --quiet, --no-color flags
14
+ *
15
+ * @module framework/output-formatter
16
+ */
17
+
18
+ import chalk from 'chalk';
19
+
20
+ import type {
21
+ CommandContext,
22
+ OutputFormatterInterface,
23
+ } from './command-interface';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Options for table rendering.
31
+ */
32
+ export interface TableOptions {
33
+ /** Column headers. If not provided, inferred from data keys. */
34
+ columns?: ColumnDefinition[];
35
+
36
+ /** Maximum width for the entire table. Defaults to terminal width. */
37
+ maxWidth?: number;
38
+
39
+ /** Whether to show row numbers. */
40
+ rowNumbers?: boolean;
41
+
42
+ /** Whether to show borders. */
43
+ borders?: boolean;
44
+
45
+ /** Header style. */
46
+ headerStyle?: 'bold' | 'underline' | 'dim' | 'none';
47
+
48
+ /** Empty table message. */
49
+ emptyMessage?: string;
50
+
51
+ /** Maximum number of rows to display. */
52
+ maxRows?: number;
53
+ }
54
+
55
+ /**
56
+ * Column definition for table rendering.
57
+ */
58
+ export interface ColumnDefinition {
59
+ /** Column header label */
60
+ header: string;
61
+
62
+ /** Property key in the data object */
63
+ key: string;
64
+
65
+ /** Fixed width for the column. If not set, auto-calculated. */
66
+ width?: number;
67
+
68
+ /** Minimum width. */
69
+ minWidth?: number;
70
+
71
+ /** Maximum width. */
72
+ maxWidth?: number;
73
+
74
+ /** Text alignment. */
75
+ align?: 'left' | 'right' | 'center';
76
+
77
+ /** Custom formatter for cell values. */
78
+ format?: (value: unknown) => string;
79
+
80
+ /** Color function for cell values. */
81
+ color?: (value: unknown) => string;
82
+ }
83
+
84
+ /**
85
+ * Options for JSON output.
86
+ */
87
+ export interface JsonOptions {
88
+ /** Pretty print with indentation. Defaults to true. */
89
+ pretty?: boolean;
90
+
91
+ /** Indentation spaces. Defaults to 2. */
92
+ indent?: number;
93
+
94
+ /** Sort object keys. */
95
+ sortKeys?: boolean;
96
+ }
97
+
98
+ /**
99
+ * Options for key-value pair formatting.
100
+ */
101
+ export interface KeyValueOptions {
102
+ /** Separator between key and value. Defaults to ': '. */
103
+ separator?: string;
104
+
105
+ /** Padding for key column alignment. */
106
+ keyWidth?: number;
107
+
108
+ /** Color for keys. */
109
+ keyColor?: (s: string) => string;
110
+
111
+ /** Color for values. */
112
+ valueColor?: (s: string) => string;
113
+
114
+ /** Indentation level. */
115
+ indent?: number;
116
+ }
117
+
118
+ /**
119
+ * Options for list formatting.
120
+ */
121
+ export interface ListOptions {
122
+ /** Ordered (numbered) list. */
123
+ ordered?: boolean;
124
+
125
+ /** Bullet character for unordered lists. */
126
+ bullet?: string;
127
+
128
+ /** Indentation level. */
129
+ indent?: number;
130
+ }
131
+
132
+ /**
133
+ * Node in a tree structure.
134
+ */
135
+ export interface TreeNode {
136
+ /** Display label */
137
+ label: string;
138
+
139
+ /** Optional icon/prefix */
140
+ prefix?: string;
141
+
142
+ /** Child nodes */
143
+ children?: TreeNode[];
144
+ }
145
+
146
+ /**
147
+ * Options for tree rendering.
148
+ */
149
+ export interface TreeOptions {
150
+ /** Whether to use Unicode box-drawing characters. Defaults to true. */
151
+ unicode?: boolean;
152
+
153
+ /** Indentation per level. */
154
+ indent?: number;
155
+
156
+ /** Maximum depth to render. */
157
+ maxDepth?: number;
158
+ }
159
+
160
+ /**
161
+ * Options for progress bar.
162
+ */
163
+ export interface ProgressOptions {
164
+ /** Total width of the progress bar. Defaults to 30. */
165
+ width?: number;
166
+
167
+ /** Filled character. Defaults to '='. */
168
+ filled?: string;
169
+
170
+ /** Empty character. Defaults to '-'. */
171
+ empty?: string;
172
+
173
+ /** Show percentage. */
174
+ showPercentage?: boolean;
175
+
176
+ /** Show count (current/total). */
177
+ showCount?: boolean;
178
+
179
+ /** Label to show after the bar. */
180
+ label?: string;
181
+ }
182
+
183
+ /**
184
+ * Status states with associated colors and icons.
185
+ */
186
+ export type StatusState =
187
+ | 'running'
188
+ | 'stopped'
189
+ | 'error'
190
+ | 'pending'
191
+ | 'done'
192
+ | 'skipped'
193
+ | 'warning'
194
+ | 'healthy'
195
+ | 'degraded'
196
+ | 'unhealthy'
197
+ | 'active'
198
+ | 'paused'
199
+ | 'terminated';
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Constants
203
+ // ---------------------------------------------------------------------------
204
+
205
+ const STATUS_CONFIG: Record<
206
+ StatusState,
207
+ { icon: string; color: (s: string) => string }
208
+ > = {
209
+ running: { icon: '[RUNNING]', color: chalk.green },
210
+ active: { icon: '[ACTIVE]', color: chalk.green },
211
+ healthy: { icon: '[HEALTHY]', color: chalk.green },
212
+ done: { icon: '[DONE]', color: chalk.blue },
213
+ stopped: { icon: '[STOPPED]', color: chalk.yellow },
214
+ paused: { icon: '[PAUSED]', color: chalk.yellow },
215
+ warning: { icon: '[WARNING]', color: chalk.yellow },
216
+ degraded: { icon: '[DEGRADED]', color: chalk.yellow },
217
+ pending: { icon: '[PENDING]', color: chalk.cyan },
218
+ error: { icon: '[ERROR]', color: chalk.red },
219
+ unhealthy: { icon: '[UNHEALTHY]', color: chalk.red },
220
+ terminated: { icon: '[TERMINATED]', color: chalk.gray },
221
+ skipped: { icon: '[SKIPPED]', color: chalk.gray },
222
+ };
223
+
224
+ const TREE_CHARS = {
225
+ unicode: {
226
+ branch: '\u251c\u2500\u2500 ',
227
+ last: '\u2514\u2500\u2500 ',
228
+ pipe: '\u2502 ',
229
+ empty: ' ',
230
+ },
231
+ ascii: { branch: '|-- ', last: '`-- ', pipe: '| ', empty: ' ' },
232
+ };
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Output Formatter
236
+ // ---------------------------------------------------------------------------
237
+
238
+ export class OutputFormatter implements OutputFormatterInterface {
239
+ private noColor: boolean;
240
+
241
+ constructor(options: { noColor?: boolean } = {}) {
242
+ this.noColor = options.noColor ?? process.env['NO_COLOR'] === '1';
243
+ }
244
+
245
+ // -------------------------------------------------------------------------
246
+ // Table
247
+ // -------------------------------------------------------------------------
248
+
249
+ /**
250
+ * Render a data array as an aligned table.
251
+ *
252
+ * @param data - Array of row objects
253
+ * @param options - Table rendering options
254
+ * @returns Formatted table string
255
+ */
256
+ table(data: Record<string, unknown>[], options: TableOptions = {}): string {
257
+ if (data.length === 0) {
258
+ return options.emptyMessage ?? chalk.gray('No data to display.');
259
+ }
260
+
261
+ // Determine columns
262
+ const columns = options.columns ?? this.inferColumns(data);
263
+ const rows = options.maxRows ? data.slice(0, options.maxRows) : data;
264
+
265
+ // Calculate column widths
266
+ const widths = this.calculateColumnWidths(columns, rows, options.maxWidth);
267
+
268
+ // Render header
269
+ const lines: string[] = [];
270
+
271
+ const headerLine = columns
272
+ .map((col, i) => {
273
+ const text = this.padCell(
274
+ col.header,
275
+ widths[i] ?? 10,
276
+ col.align ?? 'left'
277
+ );
278
+ return this.applyHeaderStyle(text, options.headerStyle ?? 'bold');
279
+ })
280
+ .join(' ');
281
+
282
+ lines.push(headerLine);
283
+
284
+ // Separator
285
+ const separator = columns
286
+ .map((_, i) => chalk.gray('-'.repeat(widths[i] ?? 10)))
287
+ .join(' ');
288
+ lines.push(separator);
289
+
290
+ // Render rows
291
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
292
+ const row = rows[rowIndex];
293
+ if (!row) continue;
294
+
295
+ const prefix = options.rowNumbers ? chalk.gray(`${rowIndex + 1}. `) : '';
296
+ const cells = columns
297
+ .map((col, i) => {
298
+ const rawValue = row[col.key];
299
+ const formatted = col.format
300
+ ? col.format(rawValue)
301
+ : String(rawValue ?? '');
302
+ const truncated = this.truncate(formatted, widths[i] ?? 10);
303
+ const padded = this.padCell(
304
+ truncated,
305
+ widths[i] ?? 10,
306
+ col.align ?? 'left'
307
+ );
308
+ return col.color ? col.color(rawValue) : padded;
309
+ })
310
+ .join(' ');
311
+
312
+ lines.push(prefix + cells);
313
+ }
314
+
315
+ // Truncation notice
316
+ if (options.maxRows && data.length > options.maxRows) {
317
+ lines.push(
318
+ chalk.gray(`... and ${data.length - options.maxRows} more rows`)
319
+ );
320
+ }
321
+
322
+ return lines.join('\n');
323
+ }
324
+
325
+ // -------------------------------------------------------------------------
326
+ // JSON
327
+ // -------------------------------------------------------------------------
328
+
329
+ /**
330
+ * Format data as JSON.
331
+ *
332
+ * @param data - Any serializable data
333
+ * @param pretty - Pretty print. Defaults to true.
334
+ * @returns JSON string
335
+ */
336
+ json(data: unknown, pretty: boolean = true): string {
337
+ if (pretty) {
338
+ return JSON.stringify(data, null, 2);
339
+ }
340
+ return JSON.stringify(data);
341
+ }
342
+
343
+ // -------------------------------------------------------------------------
344
+ // Key-Value
345
+ // -------------------------------------------------------------------------
346
+
347
+ /**
348
+ * Format key-value pairs with aligned columns.
349
+ *
350
+ * @param data - Object with string keys
351
+ * @param options - Formatting options
352
+ * @returns Formatted key-value string
353
+ */
354
+ keyValue(
355
+ data: Record<string, unknown>,
356
+ options: KeyValueOptions = {}
357
+ ): string {
358
+ const separator = options.separator ?? ': ';
359
+ const indent = ' '.repeat(options.indent ?? 0);
360
+ const keyColor = options.keyColor ?? chalk.white;
361
+ const valueColor = options.valueColor ?? ((s: string) => s);
362
+
363
+ const entries = Object.entries(data);
364
+ if (entries.length === 0) {
365
+ return chalk.gray('No data.');
366
+ }
367
+
368
+ // Calculate key width for alignment
369
+ const keyWidth =
370
+ options.keyWidth ?? Math.max(...entries.map(([k]) => k.length));
371
+
372
+ return entries
373
+ .map(([key, value]) => {
374
+ const paddedKey = key.padEnd(keyWidth);
375
+ const formattedValue = this.formatValue(value);
376
+ return `${indent}${keyColor(paddedKey)}${separator}${valueColor(formattedValue)}`;
377
+ })
378
+ .join('\n');
379
+ }
380
+
381
+ // -------------------------------------------------------------------------
382
+ // List
383
+ // -------------------------------------------------------------------------
384
+
385
+ /**
386
+ * Format items as a list.
387
+ *
388
+ * @param items - List items
389
+ * @param ordered - Use numbers instead of bullets
390
+ * @returns Formatted list string
391
+ */
392
+ list(items: string[], ordered: boolean = false): string {
393
+ if (items.length === 0) {
394
+ return chalk.gray('Empty list.');
395
+ }
396
+
397
+ const indent = ' ';
398
+
399
+ return items
400
+ .map((item, index) => {
401
+ const prefix = ordered ? chalk.gray(`${index + 1}.`) : chalk.gray('-');
402
+ return `${indent}${prefix} ${item}`;
403
+ })
404
+ .join('\n');
405
+ }
406
+
407
+ // -------------------------------------------------------------------------
408
+ // Tree
409
+ // -------------------------------------------------------------------------
410
+
411
+ /**
412
+ * Render a tree structure.
413
+ *
414
+ * @param node - Root node
415
+ * @param options - Rendering options
416
+ * @returns Formatted tree string
417
+ */
418
+ tree(node: TreeNode, options: TreeOptions = {}): string {
419
+ const chars =
420
+ options.unicode !== false ? TREE_CHARS.unicode : TREE_CHARS.ascii;
421
+ const maxDepth = options.maxDepth ?? Infinity;
422
+
423
+ const lines: string[] = [];
424
+ lines.push(`${node.prefix ?? ''}${node.label}`);
425
+
426
+ if (node.children) {
427
+ this.renderTreeChildren(node.children, '', chars, lines, 0, maxDepth);
428
+ }
429
+
430
+ return lines.join('\n');
431
+ }
432
+
433
+ // -------------------------------------------------------------------------
434
+ // Progress Bar
435
+ // -------------------------------------------------------------------------
436
+
437
+ /**
438
+ * Render a progress bar.
439
+ *
440
+ * @param current - Current progress value
441
+ * @param total - Total/target value
442
+ * @param width - Bar width in characters
443
+ * @returns Formatted progress bar string
444
+ */
445
+ progressBar(current: number, total: number, width: number = 30): string {
446
+ const percentage = total > 0 ? Math.min(current / total, 1) : 0;
447
+ const filled = Math.round(percentage * width);
448
+ const empty = width - filled;
449
+
450
+ const bar =
451
+ chalk.cyan('[') +
452
+ chalk.green('='.repeat(filled)) +
453
+ chalk.gray('-'.repeat(empty)) +
454
+ chalk.cyan(']');
455
+
456
+ const pct = `${(percentage * 100).toFixed(1)}%`;
457
+ const count = `${current}/${total}`;
458
+
459
+ return `${bar} ${pct} (${count})`;
460
+ }
461
+
462
+ // -------------------------------------------------------------------------
463
+ // Status
464
+ // -------------------------------------------------------------------------
465
+
466
+ /**
467
+ * Format a status indicator with consistent color and icon.
468
+ *
469
+ * @param state - Status state
470
+ * @param label - Label to display next to the status
471
+ * @returns Formatted status string
472
+ */
473
+ status(state: string, label: string): string {
474
+ const config = STATUS_CONFIG[state as StatusState];
475
+
476
+ if (!config) {
477
+ return `[${state.toUpperCase()}] ${label}`;
478
+ }
479
+
480
+ return `${config.color(config.icon)} ${label}`;
481
+ }
482
+
483
+ // -------------------------------------------------------------------------
484
+ // Diff
485
+ // -------------------------------------------------------------------------
486
+
487
+ /**
488
+ * Format a simple diff between two values.
489
+ *
490
+ * @param before - Original value
491
+ * @param after - New value
492
+ * @returns Formatted diff string
493
+ */
494
+ diff(before: string, after: string): string {
495
+ return `${chalk.red('- ' + before)}\n${chalk.green('+ ' + after)}`;
496
+ }
497
+
498
+ // -------------------------------------------------------------------------
499
+ // Section Headers
500
+ // -------------------------------------------------------------------------
501
+
502
+ /**
503
+ * Format a section header with a separator line.
504
+ *
505
+ * @param title - Section title
506
+ * @param width - Separator width. Defaults to 60.
507
+ * @returns Formatted header string
508
+ */
509
+ header(title: string, width: number = 60): string {
510
+ return `\n${chalk.cyan(title)}\n${chalk.gray('='.repeat(width))}`;
511
+ }
512
+
513
+ /**
514
+ * Format a sub-section header.
515
+ *
516
+ * @param title - Sub-section title
517
+ * @param width - Separator width. Defaults to 40.
518
+ * @returns Formatted sub-header string
519
+ */
520
+ subHeader(title: string, width: number = 40): string {
521
+ return `\n${chalk.white(title)}\n${chalk.gray('-'.repeat(width))}`;
522
+ }
523
+
524
+ // -------------------------------------------------------------------------
525
+ // Duration & Size Formatting
526
+ // -------------------------------------------------------------------------
527
+
528
+ /**
529
+ * Format a duration in milliseconds to a human-readable string.
530
+ */
531
+ duration(ms: number): string {
532
+ const seconds = Math.floor(ms / 1000);
533
+ const minutes = Math.floor(seconds / 60);
534
+ const hours = Math.floor(minutes / 60);
535
+ const days = Math.floor(hours / 24);
536
+
537
+ if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`;
538
+ if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
539
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
540
+ return `${seconds}s`;
541
+ }
542
+
543
+ /**
544
+ * Format bytes to a human-readable string.
545
+ */
546
+ bytes(bytes: number): string {
547
+ if (bytes === 0) return '0 B';
548
+ const k = 1024;
549
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
550
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
551
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
552
+ }
553
+
554
+ // -------------------------------------------------------------------------
555
+ // YAML
556
+ // -------------------------------------------------------------------------
557
+
558
+ /**
559
+ * Format data as YAML.
560
+ * Uses a lightweight built-in serializer to avoid external dependencies.
561
+ *
562
+ * @param data - Any serializable data
563
+ * @param indent - Indentation level (used for recursion). Defaults to 0.
564
+ * @returns YAML-formatted string
565
+ */
566
+ yaml(data: unknown, indent: number = 0): string {
567
+ return this.toYaml(data, indent);
568
+ }
569
+
570
+ // -------------------------------------------------------------------------
571
+ // Multi-format output
572
+ // -------------------------------------------------------------------------
573
+
574
+ /**
575
+ * Format data in the specified output format.
576
+ *
577
+ * @param data - Data to format
578
+ * @param format - Output format
579
+ * @returns Formatted string
580
+ */
581
+ formatAs(data: unknown, format: 'json' | 'yaml' | 'table' | 'plain'): string {
582
+ switch (format) {
583
+ case 'json':
584
+ return this.json(data);
585
+ case 'yaml':
586
+ return this.yaml(data);
587
+ case 'table':
588
+ if (
589
+ Array.isArray(data) &&
590
+ data.length > 0 &&
591
+ typeof data[0] === 'object'
592
+ ) {
593
+ return this.table(data as Record<string, unknown>[]);
594
+ }
595
+ if (typeof data === 'object' && data !== null) {
596
+ return this.keyValue(data as Record<string, unknown>);
597
+ }
598
+ return String(data);
599
+ case 'plain':
600
+ if (typeof data === 'string') return data;
601
+ if (typeof data === 'object') return JSON.stringify(data, null, 2);
602
+ return String(data);
603
+ }
604
+ }
605
+
606
+ // -------------------------------------------------------------------------
607
+ // Smart Output
608
+ // -------------------------------------------------------------------------
609
+
610
+ /**
611
+ * Smart output respecting context flags (--json, --quiet, --no-color).
612
+ *
613
+ * In JSON mode: outputs data as JSON to stdout.
614
+ * In quiet mode: outputs nothing.
615
+ * Otherwise: outputs message to stdout.
616
+ *
617
+ * @param data - Structured data for JSON mode
618
+ * @param message - Human-readable message for normal mode
619
+ * @param context - Command context with global flags
620
+ */
621
+ output(data: unknown, message: string, context: CommandContext): void {
622
+ if (context.globalOptions.quiet) {
623
+ return;
624
+ }
625
+
626
+ if (context.globalOptions.json) {
627
+ console.log(this.json(data));
628
+ return;
629
+ }
630
+
631
+ console.log(message);
632
+ }
633
+
634
+ // -------------------------------------------------------------------------
635
+ // Private Helpers
636
+ // -------------------------------------------------------------------------
637
+
638
+ private inferColumns(data: Record<string, unknown>[]): ColumnDefinition[] {
639
+ const firstRow = data[0];
640
+ if (!firstRow) return [];
641
+
642
+ return Object.keys(firstRow).map(key => ({
643
+ header: this.humanize(key),
644
+ key,
645
+ align: 'left' as const,
646
+ }));
647
+ }
648
+
649
+ private calculateColumnWidths(
650
+ columns: ColumnDefinition[],
651
+ data: Record<string, unknown>[],
652
+ maxWidth?: number
653
+ ): number[] {
654
+ const termWidth = maxWidth ?? process.stdout.columns ?? 120;
655
+ const gap = 2; // gap between columns
656
+
657
+ return columns.map(col => {
658
+ // Header width
659
+ let width = col.header.length;
660
+
661
+ // Data widths
662
+ for (const row of data) {
663
+ const val = row[col.key];
664
+ const formatted = col.format ? col.format(val) : String(val ?? '');
665
+ width = Math.max(width, formatted.length);
666
+ }
667
+
668
+ // Apply constraints
669
+ if (col.minWidth) width = Math.max(width, col.minWidth);
670
+ if (col.maxWidth) width = Math.min(width, col.maxWidth);
671
+ if (col.width) width = col.width;
672
+
673
+ return width;
674
+ });
675
+ }
676
+
677
+ private padCell(
678
+ text: string,
679
+ width: number,
680
+ align: 'left' | 'right' | 'center'
681
+ ): string {
682
+ if (text.length >= width) return text.substring(0, width);
683
+
684
+ switch (align) {
685
+ case 'right':
686
+ return text.padStart(width);
687
+ case 'center': {
688
+ const leftPad = Math.floor((width - text.length) / 2);
689
+ const rightPad = width - text.length - leftPad;
690
+ return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
691
+ }
692
+ default:
693
+ return text.padEnd(width);
694
+ }
695
+ }
696
+
697
+ private applyHeaderStyle(text: string, style: string): string {
698
+ switch (style) {
699
+ case 'bold':
700
+ return chalk.bold(text);
701
+ case 'underline':
702
+ return chalk.underline(text);
703
+ case 'dim':
704
+ return chalk.dim(text);
705
+ default:
706
+ return text;
707
+ }
708
+ }
709
+
710
+ private truncate(text: string, maxLength: number): string {
711
+ if (text.length <= maxLength) return text;
712
+ return text.substring(0, maxLength - 3) + '...';
713
+ }
714
+
715
+ private formatValue(value: unknown): string {
716
+ if (value === null || value === undefined) return chalk.gray('(none)');
717
+ if (typeof value === 'boolean')
718
+ return value ? chalk.green('true') : chalk.red('false');
719
+ if (typeof value === 'number') return chalk.cyan(String(value));
720
+ if (Array.isArray(value)) return value.join(', ');
721
+ if (typeof value === 'object') return JSON.stringify(value);
722
+ return String(value);
723
+ }
724
+
725
+ private humanize(key: string): string {
726
+ return key
727
+ .replace(/([A-Z])/g, ' $1')
728
+ .replace(/[_-]/g, ' ')
729
+ .replace(/^\s/, '')
730
+ .split(' ')
731
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
732
+ .join(' ');
733
+ }
734
+
735
+ /**
736
+ * Lightweight YAML serializer.
737
+ */
738
+ private toYaml(data: unknown, indent: number): string {
739
+ const prefix = ' '.repeat(indent);
740
+
741
+ if (data === null || data === undefined) {
742
+ return 'null';
743
+ }
744
+
745
+ if (typeof data === 'string') {
746
+ // Quote strings that need it
747
+ if (
748
+ data === '' ||
749
+ data.includes('\n') ||
750
+ data.includes(':') ||
751
+ data.includes('#') ||
752
+ data === 'true' ||
753
+ data === 'false' ||
754
+ data === 'null' ||
755
+ /^\d+$/.test(data)
756
+ ) {
757
+ return `"${data.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
758
+ }
759
+ return data;
760
+ }
761
+
762
+ if (typeof data === 'number' || typeof data === 'boolean') {
763
+ return String(data);
764
+ }
765
+
766
+ if (Array.isArray(data)) {
767
+ if (data.length === 0) return '[]';
768
+ const items = data.map(item => {
769
+ const val = this.toYaml(item, indent + 1);
770
+ if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
771
+ // Object items: put first key on same line as dash
772
+ const firstNewline = val.indexOf('\n');
773
+ if (firstNewline === -1) {
774
+ return `${prefix}- ${val}`;
775
+ }
776
+ return `${prefix}- ${val}`;
777
+ }
778
+ return `${prefix}- ${val}`;
779
+ });
780
+ return '\n' + items.join('\n');
781
+ }
782
+
783
+ if (typeof data === 'object') {
784
+ const entries = Object.entries(data as Record<string, unknown>);
785
+ if (entries.length === 0) return '{}';
786
+
787
+ const lines = entries.map(([key, value]) => {
788
+ const serialized = this.toYaml(value, indent + 1);
789
+ if (typeof value === 'object' && value !== null) {
790
+ return `${prefix}${key}:${serialized.startsWith('\n') ? serialized : ' ' + serialized}`;
791
+ }
792
+ return `${prefix}${key}: ${serialized}`;
793
+ });
794
+
795
+ return (indent > 0 ? '\n' : '') + lines.join('\n');
796
+ }
797
+
798
+ return String(data);
799
+ }
800
+
801
+ private renderTreeChildren(
802
+ children: TreeNode[],
803
+ prefix: string,
804
+ chars: typeof TREE_CHARS.unicode,
805
+ lines: string[],
806
+ depth: number,
807
+ maxDepth: number
808
+ ): void {
809
+ if (depth >= maxDepth) return;
810
+
811
+ for (let i = 0; i < children.length; i++) {
812
+ const child = children[i];
813
+ if (!child) continue;
814
+
815
+ const isLast = i === children.length - 1;
816
+ const connector = isLast ? chars.last : chars.branch;
817
+ const nodePrefix = child.prefix ? `${child.prefix} ` : '';
818
+
819
+ lines.push(`${prefix}${connector}${nodePrefix}${child.label}`);
820
+
821
+ if (child.children && child.children.length > 0) {
822
+ const childPrefix = prefix + (isLast ? chars.empty : chars.pipe);
823
+ this.renderTreeChildren(
824
+ child.children,
825
+ childPrefix,
826
+ chars,
827
+ lines,
828
+ depth + 1,
829
+ maxDepth
830
+ );
831
+ }
832
+ }
833
+ }
834
+ }