@wundr.io/cli 1.0.10 → 1.0.12
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.
- package/bin/wundr.js +8 -4
- package/package.json +23 -23
- package/src/ai/ai-service.ts +16 -17
- package/src/ai/claude-client.ts +16 -16
- package/src/ai/conversation-manager.ts +29 -29
- package/src/cli.ts +4 -4
- package/src/commands/ai.ts +246 -78
- package/src/commands/alignment.ts +74 -74
- package/src/commands/analyze-optimized.ts +111 -78
- package/src/commands/analyze.ts +14 -14
- package/src/commands/batch.ts +179 -42
- package/src/commands/chat.ts +37 -30
- package/src/commands/claude-init.ts +41 -45
- package/src/commands/claude-setup.ts +204 -119
- package/src/commands/computer-setup.ts +85 -43
- package/src/commands/create-command.ts +4 -4
- package/src/commands/create.ts +27 -27
- package/src/commands/dashboard.ts +24 -24
- package/src/commands/govern.ts +25 -25
- package/src/commands/governance.ts +34 -34
- package/src/commands/guardian.ts +56 -56
- package/src/commands/init.ts +25 -22
- package/src/commands/orchestrator.ts +68 -41
- package/src/commands/performance-optimizer.ts +34 -35
- package/src/commands/plugins.ts +27 -27
- package/src/commands/project-update.ts +175 -72
- package/src/commands/rag.ts +185 -78
- package/src/commands/session.ts +35 -35
- package/src/commands/setup.ts +40 -344
- package/src/commands/test-init.ts +3 -3
- package/src/commands/test.ts +4 -4
- package/src/commands/watch.ts +28 -29
- package/src/commands/worktree.ts +49 -49
- package/src/context/context-manager.ts +10 -10
- package/src/context/session-manager.ts +41 -41
- package/src/framework/command-interface.ts +520 -0
- package/src/framework/command-registry.ts +942 -0
- package/src/framework/completion-exporter.ts +383 -0
- package/src/framework/debug-logger.ts +519 -0
- package/src/framework/error-handler.ts +867 -0
- package/src/framework/help-generator.ts +540 -0
- package/src/framework/index.ts +169 -0
- package/src/framework/interactive-repl.ts +703 -0
- package/src/framework/output-formatter.ts +834 -0
- package/src/framework/progress-manager.ts +539 -0
- package/src/index.ts +4 -4
- package/src/interactive/interactive-mode.ts +16 -16
- package/src/lib/conflict-resolution.ts +799 -9
- package/src/lib/merge-strategy.ts +529 -7
- package/src/lib/safety-mechanisms.ts +422 -18
- package/src/lib/state-detection.ts +1015 -13
- package/src/nlp/command-mapper.ts +29 -29
- package/src/nlp/command-parser.ts +17 -17
- package/src/nlp/intent-classifier.ts +7 -7
- package/src/nlp/intent-parser.ts +54 -52
- package/src/plugins/plugin-manager.ts +61 -39
- package/src/tests/computer-setup-integration.test.ts +46 -15
- package/src/types/modules.d.ts +424 -1
- package/src/utils/backup-rollback-manager.ts +11 -8
- package/src/utils/config-manager.ts +3 -3
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/logger.ts +22 -22
- package/templates/batch/ci-cd.yaml +7 -7
- package/test-suites/api/health.spec.ts +20 -23
- package/test-suites/helpers/test-config.ts +14 -13
- package/test-suites/ui/accessibility.spec.ts +27 -22
- package/test-suites/ui/smoke.spec.ts +26 -21
- package/LICENSE +0 -21
- package/dist/ai/ai-service.d.ts +0 -152
- package/dist/ai/ai-service.d.ts.map +0 -1
- package/dist/ai/ai-service.js +0 -430
- package/dist/ai/ai-service.js.map +0 -1
- package/dist/ai/claude-client.d.ts +0 -130
- package/dist/ai/claude-client.d.ts.map +0 -1
- package/dist/ai/claude-client.js +0 -340
- package/dist/ai/claude-client.js.map +0 -1
- package/dist/ai/conversation-manager.d.ts +0 -164
- package/dist/ai/conversation-manager.d.ts.map +0 -1
- package/dist/ai/conversation-manager.js +0 -614
- package/dist/ai/conversation-manager.js.map +0 -1
- package/dist/ai/index.d.ts +0 -5
- package/dist/ai/index.d.ts.map +0 -1
- package/dist/ai/index.js +0 -8
- package/dist/ai/index.js.map +0 -1
- package/dist/cli.d.ts +0 -36
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -192
- package/dist/cli.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -89
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -799
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/alignment.d.ts +0 -78
- package/dist/commands/alignment.d.ts.map +0 -1
- package/dist/commands/alignment.js +0 -817
- package/dist/commands/alignment.js.map +0 -1
- package/dist/commands/analyze-optimized.d.ts +0 -14
- package/dist/commands/analyze-optimized.d.ts.map +0 -1
- package/dist/commands/analyze-optimized.js +0 -600
- package/dist/commands/analyze-optimized.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -65
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -435
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -71
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -738
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -71
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -674
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/claude-init.d.ts +0 -28
- package/dist/commands/claude-init.d.ts.map +0 -1
- package/dist/commands/claude-init.js +0 -591
- package/dist/commands/claude-init.js.map +0 -1
- package/dist/commands/claude-setup.d.ts +0 -119
- package/dist/commands/claude-setup.d.ts.map +0 -1
- package/dist/commands/claude-setup.js +0 -1073
- package/dist/commands/claude-setup.js.map +0 -1
- package/dist/commands/computer-setup-commands.d.ts +0 -53
- package/dist/commands/computer-setup-commands.d.ts.map +0 -1
- package/dist/commands/computer-setup-commands.js +0 -705
- package/dist/commands/computer-setup-commands.js.map +0 -1
- package/dist/commands/computer-setup.d.ts +0 -7
- package/dist/commands/computer-setup.d.ts.map +0 -1
- package/dist/commands/computer-setup.js +0 -849
- package/dist/commands/computer-setup.js.map +0 -1
- package/dist/commands/create-command.d.ts +0 -7
- package/dist/commands/create-command.d.ts.map +0 -1
- package/dist/commands/create-command.js +0 -158
- package/dist/commands/create-command.js.map +0 -1
- package/dist/commands/create.d.ts +0 -74
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -556
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -91
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -538
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/govern.d.ts +0 -70
- package/dist/commands/govern.d.ts.map +0 -1
- package/dist/commands/govern.js +0 -481
- package/dist/commands/govern.js.map +0 -1
- package/dist/commands/governance.d.ts +0 -17
- package/dist/commands/governance.d.ts.map +0 -1
- package/dist/commands/governance.js +0 -703
- package/dist/commands/governance.js.map +0 -1
- package/dist/commands/guardian.d.ts +0 -20
- package/dist/commands/guardian.d.ts.map +0 -1
- package/dist/commands/guardian.js +0 -597
- package/dist/commands/guardian.js.map +0 -1
- package/dist/commands/init.d.ts +0 -59
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -650
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/orchestrator.d.ts +0 -7
- package/dist/commands/orchestrator.d.ts.map +0 -1
- package/dist/commands/orchestrator.js +0 -571
- package/dist/commands/orchestrator.js.map +0 -1
- package/dist/commands/performance-optimizer.d.ts +0 -30
- package/dist/commands/performance-optimizer.d.ts.map +0 -1
- package/dist/commands/performance-optimizer.js +0 -650
- package/dist/commands/performance-optimizer.js.map +0 -1
- package/dist/commands/plugins.d.ts +0 -87
- package/dist/commands/plugins.d.ts.map +0 -1
- package/dist/commands/plugins.js +0 -685
- package/dist/commands/plugins.js.map +0 -1
- package/dist/commands/rag.d.ts +0 -7
- package/dist/commands/rag.d.ts.map +0 -1
- package/dist/commands/rag.js +0 -748
- package/dist/commands/rag.js.map +0 -1
- package/dist/commands/session.d.ts +0 -41
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js +0 -441
- package/dist/commands/session.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -397
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/test-init.d.ts +0 -9
- package/dist/commands/test-init.d.ts.map +0 -1
- package/dist/commands/test-init.js +0 -222
- package/dist/commands/test-init.js.map +0 -1
- package/dist/commands/test.d.ts +0 -25
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js +0 -217
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/vp.d.ts +0 -7
- package/dist/commands/vp.d.ts.map +0 -1
- package/dist/commands/vp.js +0 -571
- package/dist/commands/vp.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -76
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -613
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/worktree.d.ts +0 -63
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/commands/worktree.js +0 -774
- package/dist/commands/worktree.js.map +0 -1
- package/dist/context/context-manager.d.ts +0 -155
- package/dist/context/context-manager.d.ts.map +0 -1
- package/dist/context/context-manager.js +0 -383
- package/dist/context/context-manager.js.map +0 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -6
- package/dist/context/index.js.map +0 -1
- package/dist/context/session-manager.d.ts +0 -207
- package/dist/context/session-manager.d.ts.map +0 -1
- package/dist/context/session-manager.js +0 -686
- package/dist/context/session-manager.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/interactive/interactive-mode.d.ts +0 -76
- package/dist/interactive/interactive-mode.d.ts.map +0 -1
- package/dist/interactive/interactive-mode.js +0 -732
- package/dist/interactive/interactive-mode.js.map +0 -1
- package/dist/nlp/command-mapper.d.ts +0 -174
- package/dist/nlp/command-mapper.d.ts.map +0 -1
- package/dist/nlp/command-mapper.js +0 -624
- package/dist/nlp/command-mapper.js.map +0 -1
- package/dist/nlp/command-parser.d.ts +0 -106
- package/dist/nlp/command-parser.d.ts.map +0 -1
- package/dist/nlp/command-parser.js +0 -417
- package/dist/nlp/command-parser.js.map +0 -1
- package/dist/nlp/index.d.ts +0 -5
- package/dist/nlp/index.d.ts.map +0 -1
- package/dist/nlp/index.js +0 -8
- package/dist/nlp/index.js.map +0 -1
- package/dist/nlp/intent-classifier.d.ts +0 -59
- package/dist/nlp/intent-classifier.d.ts.map +0 -1
- package/dist/nlp/intent-classifier.js +0 -384
- package/dist/nlp/intent-classifier.js.map +0 -1
- package/dist/nlp/intent-parser.d.ts +0 -152
- package/dist/nlp/intent-parser.d.ts.map +0 -1
- package/dist/nlp/intent-parser.js +0 -744
- package/dist/nlp/intent-parser.js.map +0 -1
- package/dist/plugins/plugin-manager.d.ts +0 -120
- package/dist/plugins/plugin-manager.d.ts.map +0 -1
- package/dist/plugins/plugin-manager.js +0 -595
- package/dist/plugins/plugin-manager.js.map +0 -1
- package/dist/types/index.d.ts +0 -224
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/backup-rollback-manager.d.ts +0 -72
- package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
- package/dist/utils/backup-rollback-manager.js +0 -289
- package/dist/utils/backup-rollback-manager.js.map +0 -1
- package/dist/utils/claude-config-installer.d.ts +0 -98
- package/dist/utils/claude-config-installer.d.ts.map +0 -1
- package/dist/utils/claude-config-installer.js +0 -678
- package/dist/utils/claude-config-installer.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -73
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -339
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/error-handler.d.ts +0 -46
- package/dist/utils/error-handler.d.ts.map +0 -1
- package/dist/utils/error-handler.js +0 -169
- package/dist/utils/error-handler.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -25
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -105
- package/dist/utils/logger.js.map +0 -1
- 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
|
+
}
|