@vibecheckai/cli 3.2.1 → 3.2.2
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/runners/lib/cli-output.js +236 -210
- package/bin/runners/lib/scan-output.js +144 -822
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/terminal-ui.js +188 -770
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runShip.js +7 -8
- package/package.json +1 -1
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Terminal UI -
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* - Tables and cards
|
|
9
|
-
* - Color utilities
|
|
10
|
-
*
|
|
11
|
-
* Zero dependencies - pure ANSI escape codes
|
|
2
|
+
* Terminal UI - The Vibecheck Design System
|
|
3
|
+
* * Centralizes all visual primitives to ensure "World Class" consistency.
|
|
4
|
+
* * features:
|
|
5
|
+
* - Global Grid: 76 chars width
|
|
6
|
+
* - Border Style: Double Outer / Single Inner
|
|
7
|
+
* - Components: Spinners, Progress Bars, Tables, Headers
|
|
12
8
|
*/
|
|
13
9
|
|
|
10
|
+
import process from 'process';
|
|
11
|
+
|
|
14
12
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
-
//
|
|
13
|
+
// 1. CORE CONSTANTS & ANSI
|
|
16
14
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
17
15
|
|
|
18
|
-
const
|
|
16
|
+
export const WIDTH = 76;
|
|
19
17
|
|
|
20
|
-
const
|
|
21
|
-
|
|
18
|
+
const ESC = '\x1b';
|
|
19
|
+
export const ansi = {
|
|
22
20
|
reset: `${ESC}[0m`,
|
|
23
21
|
bold: `${ESC}[1m`,
|
|
24
22
|
dim: `${ESC}[2m`,
|
|
25
23
|
italic: `${ESC}[3m`,
|
|
26
24
|
underline: `${ESC}[4m`,
|
|
27
|
-
|
|
28
|
-
strikethrough: `${ESC}[9m`,
|
|
29
|
-
|
|
30
|
-
// Standard colors
|
|
31
|
-
black: `${ESC}[30m`,
|
|
25
|
+
// Colors
|
|
32
26
|
red: `${ESC}[31m`,
|
|
33
27
|
green: `${ESC}[32m`,
|
|
34
28
|
yellow: `${ESC}[33m`,
|
|
@@ -36,818 +30,242 @@ const ansi = {
|
|
|
36
30
|
magenta: `${ESC}[35m`,
|
|
37
31
|
cyan: `${ESC}[36m`,
|
|
38
32
|
white: `${ESC}[37m`,
|
|
39
|
-
|
|
40
|
-
// Bright colors
|
|
41
33
|
gray: `${ESC}[90m`,
|
|
42
|
-
|
|
43
|
-
brightGreen: `${ESC}[92m`,
|
|
44
|
-
brightYellow: `${ESC}[93m`,
|
|
45
|
-
brightBlue: `${ESC}[94m`,
|
|
46
|
-
brightMagenta: `${ESC}[95m`,
|
|
47
|
-
brightCyan: `${ESC}[96m`,
|
|
48
|
-
brightWhite: `${ESC}[97m`,
|
|
49
|
-
|
|
50
|
-
// Background colors
|
|
51
|
-
bgBlack: `${ESC}[40m`,
|
|
34
|
+
// Backgrounds
|
|
52
35
|
bgRed: `${ESC}[41m`,
|
|
53
36
|
bgGreen: `${ESC}[42m`,
|
|
54
37
|
bgYellow: `${ESC}[43m`,
|
|
55
38
|
bgBlue: `${ESC}[44m`,
|
|
56
39
|
bgMagenta: `${ESC}[45m`,
|
|
57
40
|
bgCyan: `${ESC}[46m`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Cursor control
|
|
41
|
+
// Cursor
|
|
61
42
|
hideCursor: `${ESC}[?25l`,
|
|
62
43
|
showCursor: `${ESC}[?25h`,
|
|
63
|
-
saveCursor: `${ESC}[s`,
|
|
64
|
-
restoreCursor: `${ESC}[u`,
|
|
65
44
|
clearLine: `${ESC}[2K`,
|
|
66
|
-
clearScreen: `${ESC}[2J`,
|
|
67
45
|
cursorUp: (n = 1) => `${ESC}[${n}A`,
|
|
68
|
-
cursorDown: (n = 1) => `${ESC}[${n}B`,
|
|
69
|
-
cursorRight: (n = 1) => `${ESC}[${n}C`,
|
|
70
|
-
cursorLeft: (n = 1) => `${ESC}[${n}D`,
|
|
71
|
-
cursorTo: (x, y) => `${ESC}[${y};${x}H`,
|
|
72
|
-
|
|
73
|
-
// 24-bit color (truecolor)
|
|
74
|
-
rgb: (r, g, b) => `${ESC}[38;2;${r};${g};${b}m`,
|
|
75
|
-
bgRgb: (r, g, b) => `${ESC}[48;2;${r};${g};${b}m`,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
79
|
-
// COLOR PALETTE - Electric Blue Theme
|
|
80
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
81
|
-
|
|
82
|
-
const colors = {
|
|
83
|
-
// Primary brand colors
|
|
84
|
-
primary: ansi.rgb(99, 102, 241), // Indigo
|
|
85
|
-
secondary: ansi.rgb(139, 92, 246), // Purple
|
|
86
|
-
accent: ansi.rgb(6, 182, 212), // Cyan
|
|
87
|
-
|
|
88
|
-
// Semantic colors
|
|
89
|
-
success: ansi.rgb(16, 185, 129), // Emerald
|
|
90
|
-
warning: ansi.rgb(245, 158, 11), // Amber
|
|
91
|
-
error: ansi.rgb(239, 68, 68), // Red
|
|
92
|
-
info: ansi.rgb(59, 130, 246), // Blue
|
|
93
|
-
|
|
94
|
-
// Severity colors
|
|
95
|
-
critical: ansi.rgb(220, 38, 38), // Red-600
|
|
96
|
-
high: ansi.rgb(234, 88, 12), // Orange-600
|
|
97
|
-
medium: ansi.rgb(202, 138, 4), // Yellow-600
|
|
98
|
-
low: ansi.rgb(37, 99, 235), // Blue-600
|
|
99
|
-
|
|
100
|
-
// Gradient stops
|
|
101
|
-
gradient: {
|
|
102
|
-
cyan: ansi.rgb(0, 255, 255),
|
|
103
|
-
blue: ansi.rgb(100, 149, 237),
|
|
104
|
-
purple: ansi.rgb(138, 43, 226),
|
|
105
|
-
pink: ansi.rgb(236, 72, 153),
|
|
106
|
-
orange: ansi.rgb(251, 146, 60),
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
// Background variants
|
|
110
|
-
bg: {
|
|
111
|
-
success: ansi.bgRgb(16, 185, 129),
|
|
112
|
-
warning: ansi.bgRgb(245, 158, 11),
|
|
113
|
-
error: ansi.bgRgb(220, 38, 38),
|
|
114
|
-
info: ansi.bgRgb(59, 130, 246),
|
|
115
|
-
muted: ansi.bgRgb(39, 39, 42),
|
|
116
|
-
},
|
|
117
46
|
};
|
|
118
47
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Sharp corners
|
|
133
|
-
sharpTopLeft: '┌',
|
|
134
|
-
sharpTopRight: '┐',
|
|
135
|
-
sharpBottomLeft: '└',
|
|
136
|
-
sharpBottomRight: '┘',
|
|
137
|
-
|
|
138
|
-
// Double lines
|
|
139
|
-
doubleHorizontal: '═',
|
|
140
|
-
doubleVertical: '║',
|
|
141
|
-
doubleTopLeft: '╔',
|
|
142
|
-
doubleTopRight: '╗',
|
|
143
|
-
doubleBottomLeft: '╚',
|
|
144
|
-
doubleBottomRight: '╝',
|
|
145
|
-
|
|
146
|
-
// Connectors
|
|
147
|
-
teeRight: '├',
|
|
148
|
-
teeLeft: '┤',
|
|
149
|
-
teeDown: '┬',
|
|
150
|
-
teeUp: '┴',
|
|
151
|
-
cross: '┼',
|
|
152
|
-
|
|
153
|
-
// Block elements
|
|
154
|
-
fullBlock: '█',
|
|
155
|
-
lightShade: '░',
|
|
156
|
-
mediumShade: '▒',
|
|
157
|
-
darkShade: '▓',
|
|
158
|
-
upperHalf: '▀',
|
|
159
|
-
lowerHalf: '▄',
|
|
160
|
-
leftHalf: '▌',
|
|
161
|
-
rightHalf: '▐',
|
|
48
|
+
// Semantic Palette
|
|
49
|
+
export const style = {
|
|
50
|
+
success: (t: string) => `${ansi.green}${t}${ansi.reset}`,
|
|
51
|
+
error: (t: string) => `${ansi.red}${t}${ansi.reset}`,
|
|
52
|
+
warning: (t: string) => `${ansi.yellow}${t}${ansi.reset}`,
|
|
53
|
+
info: (t: string) => `${ansi.cyan}${t}${ansi.reset}`,
|
|
54
|
+
subtle: (t: string) => `${ansi.gray}${t}${ansi.reset}`,
|
|
55
|
+
highlight: (t: string) => `${ansi.bold}${ansi.white}${t}${ansi.reset}`,
|
|
56
|
+
label: (t: string) => `${ansi.blue}${t}${ansi.reset}`,
|
|
57
|
+
// Backgrounds for badges
|
|
58
|
+
bgSuccess: (t: string) => `${ansi.bgGreen}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
|
|
59
|
+
bgError: (t: string) => `${ansi.bgRed}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
|
|
60
|
+
bgWarn: (t: string) => `${ansi.bgYellow}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
|
|
162
61
|
};
|
|
163
62
|
|
|
164
|
-
|
|
165
|
-
// ICONS & SYMBOLS
|
|
166
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
167
|
-
|
|
168
|
-
const icons = {
|
|
169
|
-
// Status
|
|
63
|
+
export const icons = {
|
|
170
64
|
success: '✓',
|
|
171
65
|
error: '✗',
|
|
172
66
|
warning: '⚠',
|
|
173
67
|
info: 'ℹ',
|
|
174
|
-
question: '?',
|
|
175
|
-
|
|
176
|
-
// Arrows
|
|
177
|
-
arrowRight: '→',
|
|
178
|
-
arrowLeft: '←',
|
|
179
|
-
arrowUp: '↑',
|
|
180
|
-
arrowDown: '↓',
|
|
181
|
-
arrowBoth: '↔',
|
|
182
|
-
|
|
183
|
-
// Pointers
|
|
184
|
-
pointer: '❯',
|
|
185
|
-
pointerSmall: '›',
|
|
186
68
|
bullet: '•',
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
star: '★',
|
|
191
|
-
heart: '♥',
|
|
192
|
-
lightning: '⚡',
|
|
193
|
-
fire: '🔥',
|
|
194
|
-
rocket: '🚀',
|
|
195
|
-
check: '☑',
|
|
69
|
+
pointer: '❯',
|
|
70
|
+
arrowRight: '→',
|
|
71
|
+
line: '─',
|
|
196
72
|
radioOn: '◉',
|
|
197
73
|
radioOff: '○',
|
|
198
|
-
|
|
199
|
-
// Severity badges
|
|
200
|
-
critical: '🚨',
|
|
201
|
-
high: '🔴',
|
|
202
|
-
medium: '🟡',
|
|
203
|
-
low: '🔵',
|
|
74
|
+
lock: '🔒',
|
|
204
75
|
};
|
|
205
76
|
|
|
206
77
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
207
|
-
//
|
|
78
|
+
// 2. LAYOUT PRIMITIVES (The "Grid")
|
|
208
79
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
209
80
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
81
|
+
export const BOX = {
|
|
82
|
+
// Outer Frame (Double)
|
|
83
|
+
tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║',
|
|
84
|
+
trT: '╠', tlT: '╣',
|
|
85
|
+
// Inner Dividers (Single)
|
|
86
|
+
ltl: '┌', ltr: '┐', lbl: '└', lbr: '┘', lh: '─', lv: '│',
|
|
87
|
+
lt: '┬', lb: '┴', lx: '┼', ltrT: '├', ltlT: '┤'
|
|
217
88
|
};
|
|
218
89
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this.text = '';
|
|
228
|
-
this.phases = [];
|
|
229
|
-
this.currentPhase = 0;
|
|
230
|
-
this.lines = 1;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
start(text) {
|
|
234
|
-
this.text = text;
|
|
235
|
-
this.stream.write(ansi.hideCursor);
|
|
236
|
-
this._render();
|
|
237
|
-
this.timer = setInterval(() => this._render(), this.interval);
|
|
238
|
-
return this;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
update(text) {
|
|
242
|
-
this.text = text;
|
|
243
|
-
return this;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
setPhases(phases) {
|
|
247
|
-
this.phases = phases;
|
|
248
|
-
this.currentPhase = 0;
|
|
249
|
-
return this;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
nextPhase() {
|
|
253
|
-
if (this.currentPhase < this.phases.length - 1) {
|
|
254
|
-
this.currentPhase++;
|
|
255
|
-
this.text = this.phases[this.currentPhase];
|
|
256
|
-
}
|
|
257
|
-
return this;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
succeed(text) {
|
|
261
|
-
this._stop();
|
|
262
|
-
this._clear();
|
|
263
|
-
const msg = text || this.text;
|
|
264
|
-
this.stream.write(` ${colors.success}${icons.success}${ansi.reset} ${msg}\n`);
|
|
265
|
-
this.stream.write(ansi.showCursor);
|
|
266
|
-
return this;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
fail(text) {
|
|
270
|
-
this._stop();
|
|
271
|
-
this._clear();
|
|
272
|
-
const msg = text || this.text;
|
|
273
|
-
this.stream.write(` ${colors.error}${icons.error}${ansi.reset} ${msg}\n`);
|
|
274
|
-
this.stream.write(ansi.showCursor);
|
|
275
|
-
return this;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
warn(text) {
|
|
279
|
-
this._stop();
|
|
280
|
-
this._clear();
|
|
281
|
-
const msg = text || this.text;
|
|
282
|
-
this.stream.write(` ${colors.warning}${icons.warning}${ansi.reset} ${msg}\n`);
|
|
283
|
-
this.stream.write(ansi.showCursor);
|
|
284
|
-
return this;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
info(text) {
|
|
288
|
-
this._stop();
|
|
289
|
-
this._clear();
|
|
290
|
-
const msg = text || this.text;
|
|
291
|
-
this.stream.write(` ${colors.info}${icons.info}${ansi.reset} ${msg}\n`);
|
|
292
|
-
this.stream.write(ansi.showCursor);
|
|
293
|
-
return this;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
stop() {
|
|
297
|
-
this._stop();
|
|
298
|
-
this._clear();
|
|
299
|
-
this.stream.write(ansi.showCursor);
|
|
300
|
-
return this;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
_render() {
|
|
304
|
-
const frame = this.frames[this.frameIndex];
|
|
305
|
-
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
306
|
-
|
|
307
|
-
this._clear();
|
|
308
|
-
|
|
309
|
-
let output = ` ${this.color}${frame}${ansi.reset} ${this.text}`;
|
|
310
|
-
|
|
311
|
-
// Add phase indicator if phases are set
|
|
312
|
-
if (this.phases.length > 0) {
|
|
313
|
-
const phaseIndicator = this.phases.map((_, i) =>
|
|
314
|
-
i < this.currentPhase ? `${colors.success}●${ansi.reset}` :
|
|
315
|
-
i === this.currentPhase ? `${this.color}●${ansi.reset}` :
|
|
316
|
-
`${ansi.dim}○${ansi.reset}`
|
|
317
|
-
).join(' ');
|
|
318
|
-
output += ` ${ansi.dim}[${ansi.reset}${phaseIndicator}${ansi.dim}]${ansi.reset}`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
this.stream.write(`\r${output}`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
_stop() {
|
|
325
|
-
if (this.timer) {
|
|
326
|
-
clearInterval(this.timer);
|
|
327
|
-
this.timer = null;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
_clear() {
|
|
332
|
-
this.stream.write(`\r${ansi.clearLine}`);
|
|
333
|
-
}
|
|
90
|
+
/**
|
|
91
|
+
* Centers text within the standard width, accounting for ANSI codes
|
|
92
|
+
*/
|
|
93
|
+
export function padCenter(str: string, width: number = WIDTH - 2): string {
|
|
94
|
+
const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
|
|
95
|
+
const padding = Math.max(0, width - visibleLen);
|
|
96
|
+
const left = Math.floor(padding / 2);
|
|
97
|
+
return ' '.repeat(left) + str + ' '.repeat(padding - left);
|
|
334
98
|
}
|
|
335
99
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
this.complete = options.complete || '█';
|
|
345
|
-
this.incomplete = options.incomplete || '░';
|
|
346
|
-
this.stream = options.stream || process.stdout;
|
|
347
|
-
this.current = 0;
|
|
348
|
-
this.startTime = Date.now();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
update(current, label = '') {
|
|
352
|
-
this.current = current;
|
|
353
|
-
const percent = Math.min(100, Math.round((current / this.total) * 100));
|
|
354
|
-
const filled = Math.round((percent / 100) * this.width);
|
|
355
|
-
const empty = this.width - filled;
|
|
356
|
-
|
|
357
|
-
const color = percent >= 80 ? colors.success : percent >= 50 ? colors.warning : colors.error;
|
|
358
|
-
const bar = `${color}${this.complete.repeat(filled)}${ansi.dim}${this.incomplete.repeat(empty)}${ansi.reset}`;
|
|
359
|
-
|
|
360
|
-
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
361
|
-
const eta = current > 0 ? (((this.total - current) / current) * (Date.now() - this.startTime) / 1000).toFixed(1) : '?';
|
|
362
|
-
|
|
363
|
-
this.stream.write(`\r${ansi.clearLine} ${bar} ${ansi.bold}${percent}%${ansi.reset} ${ansi.dim}${label} (${elapsed}s / ~${eta}s)${ansi.reset}`);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
complete(label = 'Complete') {
|
|
367
|
-
this.update(this.total, label);
|
|
368
|
-
this.stream.write('\n');
|
|
369
|
-
}
|
|
100
|
+
/**
|
|
101
|
+
* Pads text to the right, accounting for ANSI codes
|
|
102
|
+
*/
|
|
103
|
+
export function padRight(str: string, len: number): string {
|
|
104
|
+
const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
|
|
105
|
+
const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
|
|
106
|
+
const finalLen = truncated.replace(/\u001b\[\d+m/g, '').length;
|
|
107
|
+
return truncated + ' '.repeat(Math.max(0, len - finalLen));
|
|
370
108
|
}
|
|
371
109
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
status: 'pending', // pending, running, success, error, skipped
|
|
381
|
-
message: '',
|
|
382
|
-
duration: null,
|
|
383
|
-
}));
|
|
384
|
-
this.currentPhase = -1;
|
|
385
|
-
this.stream = options.stream || process.stdout;
|
|
386
|
-
this.startTime = null;
|
|
387
|
-
this.phaseStartTime = null;
|
|
388
|
-
this.spinner = new Spinner({ color: colors.primary });
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
start() {
|
|
392
|
-
this.startTime = Date.now();
|
|
393
|
-
this.stream.write(ansi.hideCursor);
|
|
394
|
-
this._render();
|
|
395
|
-
return this;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
startPhase(index, message = '') {
|
|
399
|
-
if (this.currentPhase >= 0 && this.phases[this.currentPhase].status === 'running') {
|
|
400
|
-
this.phases[this.currentPhase].status = 'success';
|
|
401
|
-
this.phases[this.currentPhase].duration = Date.now() - this.phaseStartTime;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
this.currentPhase = index;
|
|
405
|
-
this.phaseStartTime = Date.now();
|
|
406
|
-
this.phases[index].status = 'running';
|
|
407
|
-
this.phases[index].message = message;
|
|
408
|
-
this._render();
|
|
409
|
-
return this;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
updatePhase(message) {
|
|
413
|
-
if (this.currentPhase >= 0) {
|
|
414
|
-
this.phases[this.currentPhase].message = message;
|
|
415
|
-
this._render();
|
|
416
|
-
}
|
|
417
|
-
return this;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
succeedPhase(message = '') {
|
|
421
|
-
if (this.currentPhase >= 0) {
|
|
422
|
-
this.phases[this.currentPhase].status = 'success';
|
|
423
|
-
this.phases[this.currentPhase].duration = Date.now() - this.phaseStartTime;
|
|
424
|
-
if (message) this.phases[this.currentPhase].message = message;
|
|
425
|
-
this._render();
|
|
426
|
-
}
|
|
427
|
-
return this;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
failPhase(message = '') {
|
|
431
|
-
if (this.currentPhase >= 0) {
|
|
432
|
-
this.phases[this.currentPhase].status = 'error';
|
|
433
|
-
this.phases[this.currentPhase].duration = Date.now() - this.phaseStartTime;
|
|
434
|
-
if (message) this.phases[this.currentPhase].message = message;
|
|
435
|
-
this._render();
|
|
436
|
-
}
|
|
437
|
-
return this;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
skipPhase(message = 'Skipped') {
|
|
441
|
-
if (this.currentPhase >= 0) {
|
|
442
|
-
this.phases[this.currentPhase].status = 'skipped';
|
|
443
|
-
this.phases[this.currentPhase].message = message;
|
|
444
|
-
this._render();
|
|
445
|
-
}
|
|
446
|
-
return this;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
finish() {
|
|
450
|
-
if (this.currentPhase >= 0 && this.phases[this.currentPhase].status === 'running') {
|
|
451
|
-
this.phases[this.currentPhase].status = 'success';
|
|
452
|
-
this.phases[this.currentPhase].duration = Date.now() - this.phaseStartTime;
|
|
453
|
-
}
|
|
454
|
-
this._render();
|
|
455
|
-
this.stream.write(ansi.showCursor);
|
|
456
|
-
return this;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
_render() {
|
|
460
|
-
// Move cursor up to clear previous render
|
|
461
|
-
if (this._rendered) {
|
|
462
|
-
this.stream.write(ansi.cursorUp(this.phases.length + 1));
|
|
463
|
-
}
|
|
464
|
-
this._rendered = true;
|
|
465
|
-
|
|
466
|
-
const totalDuration = Date.now() - this.startTime;
|
|
467
|
-
|
|
468
|
-
for (const phase of this.phases) {
|
|
469
|
-
this.stream.write(ansi.clearLine);
|
|
470
|
-
|
|
471
|
-
let statusIcon, statusColor;
|
|
472
|
-
switch (phase.status) {
|
|
473
|
-
case 'success':
|
|
474
|
-
statusIcon = icons.success;
|
|
475
|
-
statusColor = colors.success;
|
|
476
|
-
break;
|
|
477
|
-
case 'error':
|
|
478
|
-
statusIcon = icons.error;
|
|
479
|
-
statusColor = colors.error;
|
|
480
|
-
break;
|
|
481
|
-
case 'running':
|
|
482
|
-
statusIcon = this.spinner.frames[this.spinner.frameIndex];
|
|
483
|
-
statusColor = colors.primary;
|
|
484
|
-
this.spinner.frameIndex = (this.spinner.frameIndex + 1) % this.spinner.frames.length;
|
|
485
|
-
break;
|
|
486
|
-
case 'skipped':
|
|
487
|
-
statusIcon = '○';
|
|
488
|
-
statusColor = ansi.dim;
|
|
489
|
-
break;
|
|
490
|
-
default:
|
|
491
|
-
statusIcon = '○';
|
|
492
|
-
statusColor = ansi.dim;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const duration = phase.duration ? `${ansi.dim}${phase.duration}ms${ansi.reset}` : '';
|
|
496
|
-
const message = phase.message ? `${ansi.dim}${phase.message}${ansi.reset}` : '';
|
|
497
|
-
|
|
498
|
-
this.stream.write(` ${statusColor}${statusIcon}${ansi.reset} ${phase.name.padEnd(25)} ${duration.padEnd(15)} ${message}\n`);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Total line
|
|
502
|
-
this.stream.write(ansi.clearLine);
|
|
503
|
-
this.stream.write(` ${ansi.dim}${'─'.repeat(60)}${ansi.reset}\n`);
|
|
504
|
-
|
|
505
|
-
// Keep spinner running for active phase
|
|
506
|
-
if (this.phases.some(p => p.status === 'running')) {
|
|
507
|
-
setTimeout(() => this._render(), 80);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
110
|
+
/**
|
|
111
|
+
* Truncates text with ellipsis
|
|
112
|
+
*/
|
|
113
|
+
export function truncate(str: string, len: number): string {
|
|
114
|
+
if (!str) return '';
|
|
115
|
+
const clean = str.replace(/\u001b\[\d+m/g, '');
|
|
116
|
+
if (clean.length <= len) return str;
|
|
117
|
+
return clean.substring(0, len - 3) + '...';
|
|
510
118
|
}
|
|
511
119
|
|
|
512
120
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
513
|
-
//
|
|
121
|
+
// 3. COMPONENT: SPINNER
|
|
514
122
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
515
123
|
|
|
516
|
-
|
|
517
|
-
if (score >= 90) return colors.success;
|
|
518
|
-
if (score >= 70) return colors.warning;
|
|
519
|
-
if (score >= 50) return ansi.rgb(251, 146, 60);
|
|
520
|
-
return colors.error;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function getGrade(score) {
|
|
524
|
-
if (score >= 90) return 'A';
|
|
525
|
-
if (score >= 80) return 'B';
|
|
526
|
-
if (score >= 70) return 'C';
|
|
527
|
-
if (score >= 60) return 'D';
|
|
528
|
-
return 'F';
|
|
529
|
-
}
|
|
124
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
530
125
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
findings = { critical: 0, high: 0, medium: 0, low: 0 },
|
|
535
|
-
duration = null,
|
|
536
|
-
cached = false,
|
|
537
|
-
} = options;
|
|
538
|
-
|
|
539
|
-
const scoreColor = getScoreColor(score);
|
|
540
|
-
const grade = getGrade(score);
|
|
541
|
-
const gradeColor = scoreColor;
|
|
542
|
-
|
|
543
|
-
const verdictConfig = {
|
|
544
|
-
SHIP: { bg: colors.bg.success, text: ' ✓ SHIP ', desc: 'Ready to ship' },
|
|
545
|
-
WARN: { bg: colors.bg.warning, text: ' ⚠ WARN ', desc: 'Review before shipping' },
|
|
546
|
-
BLOCK: { bg: colors.bg.error, text: ' ✗ BLOCK ', desc: 'Fix issues before shipping' },
|
|
547
|
-
PASS: { bg: colors.bg.success, text: ' ✓ PASS ', desc: 'All checks passed' },
|
|
548
|
-
FAIL: { bg: colors.bg.error, text: ' ✗ FAIL ', desc: 'Checks failed' },
|
|
549
|
-
};
|
|
550
|
-
const v = verdictConfig[verdict] || verdictConfig.WARN;
|
|
551
|
-
|
|
552
|
-
// Build progress bar
|
|
553
|
-
const barWidth = 40;
|
|
554
|
-
const filled = Math.round((score / 100) * barWidth);
|
|
555
|
-
const bar = `${scoreColor}${'█'.repeat(filled)}${ansi.dim}${'░'.repeat(barWidth - filled)}${ansi.reset}`;
|
|
556
|
-
|
|
557
|
-
const lines = [];
|
|
558
|
-
lines.push('');
|
|
559
|
-
lines.push(` ${ansi.dim}${box.topLeft}${'─'.repeat(66)}${box.topRight}${ansi.reset}`);
|
|
560
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${' '.repeat(66)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
561
|
-
|
|
562
|
-
// Score + Grade row
|
|
563
|
-
const scoreStr = String(score).padStart(3);
|
|
564
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset} ${ansi.dim}SCORE${ansi.reset} ${scoreColor}${ansi.bold}${scoreStr}${ansi.reset}${ansi.dim}/100${ansi.reset} ${ansi.dim}GRADE${ansi.reset} ${gradeColor}${ansi.bold}${grade}${ansi.reset} ${cached ? `${ansi.dim}(cached)${ansi.reset}` : ''} ${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
565
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${' '.repeat(66)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
566
|
-
|
|
567
|
-
// Progress bar
|
|
568
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset} ${bar} ${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
569
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${' '.repeat(66)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
570
|
-
|
|
571
|
-
// Verdict badge
|
|
572
|
-
const verdictPad = ' '.repeat(Math.max(0, 23 - v.text.length));
|
|
573
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${' '.repeat(20)}${v.bg}${ansi.bold}${v.text}${ansi.reset}${verdictPad}${ansi.dim}${v.desc}${ansi.reset} ${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
574
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${' '.repeat(66)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
126
|
+
export class Spinner {
|
|
127
|
+
private timer: NodeJS.Timeout | null = null;
|
|
128
|
+
private frameIndex = 0;
|
|
575
129
|
|
|
576
|
-
|
|
577
|
-
const criticalStr = `${colors.critical}${findings.critical || 0}${ansi.reset} critical`;
|
|
578
|
-
const highStr = `${colors.high}${findings.high || 0}${ansi.reset} high`;
|
|
579
|
-
const mediumStr = `${colors.medium}${findings.medium || 0}${ansi.reset} medium`;
|
|
580
|
-
const lowStr = `${colors.low}${findings.low || 0}${ansi.reset} low`;
|
|
581
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset} ${criticalStr} ${ansi.dim}│${ansi.reset} ${highStr} ${ansi.dim}│${ansi.reset} ${mediumStr} ${ansi.dim}│${ansi.reset} ${lowStr} ${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
582
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${' '.repeat(66)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
583
|
-
|
|
584
|
-
// Duration if provided
|
|
585
|
-
if (duration) {
|
|
586
|
-
const durationStr = typeof duration === 'number' ? `${duration}ms` : duration;
|
|
587
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset} ${ansi.dim}Completed in ${durationStr}${ansi.reset}${' '.repeat(Math.max(0, 46 - durationStr.length))}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
588
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${' '.repeat(66)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
lines.push(` ${ansi.dim}${box.bottomLeft}${'─'.repeat(66)}${box.bottomRight}${ansi.reset}`);
|
|
592
|
-
lines.push('');
|
|
593
|
-
|
|
594
|
-
return lines.join('\n');
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
598
|
-
// FINDINGS LIST - Premium Findings Display
|
|
599
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
130
|
+
constructor(public text: string = '', private colorStr = ansi.cyan) {}
|
|
600
131
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (groupBySeverity) {
|
|
611
|
-
const groups = {
|
|
612
|
-
critical: findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK'),
|
|
613
|
-
high: findings.filter(f => f.severity === 'high'),
|
|
614
|
-
medium: findings.filter(f => f.severity === 'medium' || f.severity === 'WARN' || f.severity === 'warning'),
|
|
615
|
-
low: findings.filter(f => f.severity === 'low' || f.severity === 'INFO' || f.severity === 'info'),
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
for (const [severity, items] of Object.entries(groups)) {
|
|
619
|
-
if (items.length === 0) continue;
|
|
620
|
-
|
|
621
|
-
const color = colors[severity] || ansi.dim;
|
|
622
|
-
const icon = icons[severity] || icons.bullet;
|
|
623
|
-
|
|
624
|
-
lines.push('');
|
|
625
|
-
lines.push(` ${color}${ansi.bold}${severity.toUpperCase()} (${items.length})${ansi.reset}`);
|
|
626
|
-
lines.push(` ${ansi.dim}${'─'.repeat(40)}${ansi.reset}`);
|
|
627
|
-
|
|
628
|
-
for (const finding of items.slice(0, Math.ceil(maxItems / 4))) {
|
|
629
|
-
lines.push(...renderFinding(finding, { showCode, color }));
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (items.length > Math.ceil(maxItems / 4)) {
|
|
633
|
-
lines.push(` ${ansi.dim} ... and ${items.length - Math.ceil(maxItems / 4)} more ${severity} findings${ansi.reset}`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
} else {
|
|
637
|
-
for (const finding of findings.slice(0, maxItems)) {
|
|
638
|
-
lines.push(...renderFinding(finding, { showCode }));
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (findings.length > maxItems) {
|
|
642
|
-
lines.push('');
|
|
643
|
-
lines.push(` ${ansi.dim}... and ${findings.length - maxItems} more findings${ansi.reset}`);
|
|
644
|
-
}
|
|
132
|
+
start(text?: string) {
|
|
133
|
+
if (text) this.text = text;
|
|
134
|
+
process.stdout.write(ansi.hideCursor);
|
|
135
|
+
this.timer = setInterval(() => {
|
|
136
|
+
const frame = SPINNER_FRAMES[this.frameIndex];
|
|
137
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
138
|
+
process.stdout.write(`\r ${this.colorStr}${frame}${ansi.reset} ${this.text}`);
|
|
139
|
+
}, 80);
|
|
140
|
+
return this;
|
|
645
141
|
}
|
|
646
|
-
|
|
647
|
-
return lines.join('\n');
|
|
648
|
-
}
|
|
649
142
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
BLOCK: colors.critical,
|
|
657
|
-
high: colors.high,
|
|
658
|
-
medium: colors.medium,
|
|
659
|
-
WARN: colors.medium,
|
|
660
|
-
warning: colors.medium,
|
|
661
|
-
low: colors.low,
|
|
662
|
-
INFO: colors.low,
|
|
663
|
-
info: colors.low,
|
|
664
|
-
}[finding.severity] || ansi.dim;
|
|
665
|
-
|
|
666
|
-
const title = finding.title || finding.message || 'Unknown issue';
|
|
667
|
-
lines.push(` ${severityColor}${icons.pointer}${ansi.reset} ${ansi.bold}${truncate(title, 60)}${ansi.reset}`);
|
|
668
|
-
|
|
669
|
-
if (finding.file) {
|
|
670
|
-
const fileStr = finding.file + (finding.line ? `:${finding.line}` : '');
|
|
671
|
-
lines.push(` ${ansi.dim}${truncate(fileStr, 55)}${ansi.reset}`);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (finding.fix || finding.fixSuggestion) {
|
|
675
|
-
lines.push(` ${colors.success}${icons.arrowRight}${ansi.reset} ${ansi.dim}${truncate(finding.fix || finding.fixSuggestion, 50)}${ansi.reset}`);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (showCode && finding.codeSnippet) {
|
|
679
|
-
lines.push(` ${ansi.dim}┌──────────────────────────────────────${ansi.reset}`);
|
|
680
|
-
for (const line of finding.codeSnippet.split('\n').slice(0, 3)) {
|
|
681
|
-
lines.push(` ${ansi.dim}│${ansi.reset} ${truncate(line, 50)}`);
|
|
143
|
+
stop(symbol = icons.success, color = ansi.green, finalMsg?: string) {
|
|
144
|
+
if (this.timer) clearInterval(this.timer);
|
|
145
|
+
process.stdout.write(`\r${ansi.clearLine}`);
|
|
146
|
+
if (finalMsg !== null) { // Pass null to clear completely
|
|
147
|
+
const msg = finalMsg || this.text;
|
|
148
|
+
console.log(` ${color}${symbol}${ansi.reset} ${msg}`);
|
|
682
149
|
}
|
|
683
|
-
|
|
150
|
+
process.stdout.write(ansi.showCursor);
|
|
151
|
+
return this;
|
|
684
152
|
}
|
|
685
|
-
|
|
686
|
-
lines.push('');
|
|
687
|
-
return lines;
|
|
688
|
-
}
|
|
689
153
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
function renderTable(headers, rows, options = {}) {
|
|
695
|
-
const { padding = 2, headerColor = colors.primary } = options;
|
|
696
|
-
|
|
697
|
-
// Calculate column widths
|
|
698
|
-
const colWidths = headers.map((h, i) => {
|
|
699
|
-
const maxData = Math.max(...rows.map(r => stripAnsi(String(r[i] || '')).length));
|
|
700
|
-
return Math.max(stripAnsi(h).length, maxData) + padding;
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
const lines = [];
|
|
704
|
-
const totalWidth = colWidths.reduce((a, b) => a + b, 0) + colWidths.length + 1;
|
|
705
|
-
|
|
706
|
-
// Header
|
|
707
|
-
lines.push(` ${ansi.dim}${box.topLeft}${'─'.repeat(totalWidth - 2)}${box.topRight}${ansi.reset}`);
|
|
708
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${headers.map((h, i) => `${headerColor}${ansi.bold}${h.padEnd(colWidths[i])}${ansi.reset}`).join(`${ansi.dim}│${ansi.reset}`)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
709
|
-
lines.push(` ${ansi.dim}${box.teeRight}${'─'.repeat(totalWidth - 2)}${box.teeLeft}${ansi.reset}`);
|
|
710
|
-
|
|
711
|
-
// Rows
|
|
712
|
-
for (const row of rows) {
|
|
713
|
-
const cells = row.map((cell, i) => {
|
|
714
|
-
const str = String(cell || '');
|
|
715
|
-
const visibleLen = stripAnsi(str).length;
|
|
716
|
-
return str + ' '.repeat(Math.max(0, colWidths[i] - visibleLen));
|
|
717
|
-
});
|
|
718
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${cells.join(`${ansi.dim}│${ansi.reset}`)}${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
lines.push(` ${ansi.dim}${box.bottomLeft}${'─'.repeat(totalWidth - 2)}${box.bottomRight}${ansi.reset}`);
|
|
722
|
-
|
|
723
|
-
return lines.join('\n');
|
|
154
|
+
succeed(text?: string) { return this.stop(icons.success, ansi.green, text); }
|
|
155
|
+
fail(text?: string) { return this.stop(icons.error, ansi.red, text); }
|
|
156
|
+
warn(text?: string) { return this.stop(icons.warning, ansi.yellow, text); }
|
|
724
157
|
}
|
|
725
158
|
|
|
726
159
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
727
|
-
//
|
|
160
|
+
// 4. COMPONENT: PROGRESS BAR
|
|
728
161
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
729
162
|
|
|
730
|
-
function
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function renderDivider(char = '─', width = 60) {
|
|
735
|
-
return ` ${ansi.dim}${char.repeat(width)}${ansi.reset}`;
|
|
163
|
+
export function renderProgressBar(percentage: number, width = 20, color = ansi.green): string {
|
|
164
|
+
const filled = Math.round((percentage / 100) * width);
|
|
165
|
+
return `${color}${'█'.repeat(filled)}${ansi.gray}${'░'.repeat(width - filled)}${ansi.reset}`;
|
|
736
166
|
}
|
|
737
167
|
|
|
738
168
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
739
|
-
//
|
|
169
|
+
// 5. STANDARD RENDERERS (The "Look")
|
|
740
170
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
741
171
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Renders the Standard Double-Border Header with Logo
|
|
174
|
+
*/
|
|
175
|
+
export function renderScreenHeader(
|
|
176
|
+
logoAscii: string,
|
|
177
|
+
title: string,
|
|
178
|
+
logoColor: (t: string) => string = (t) => t,
|
|
179
|
+
context?: { label: string, value: string }
|
|
180
|
+
) {
|
|
181
|
+
const lines: string[] = [];
|
|
182
|
+
|
|
183
|
+
// Top Frame
|
|
184
|
+
lines.push(ansi.gray + BOX.tl + BOX.h.repeat(WIDTH - 2) + BOX.tr + ansi.reset);
|
|
185
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
186
|
+
|
|
187
|
+
// Logo Processing
|
|
188
|
+
logoAscii.trim().split('\n').forEach(line => {
|
|
189
|
+
const cleanLine = line.replace(/\r/g, '');
|
|
190
|
+
const maxLen = Math.max(...logoAscii.split('\n').map(l => l.length));
|
|
191
|
+
const padding = ' '.repeat(Math.floor((WIDTH - 2 - maxLen) / 2));
|
|
192
|
+
const rightPad = ' '.repeat(WIDTH - 2 - padding.length - cleanLine.length);
|
|
193
|
+
|
|
194
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padding + logoColor(cleanLine) + rightPad + ansi.gray + BOX.v + ansi.reset);
|
|
195
|
+
});
|
|
753
196
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
197
|
+
// Title
|
|
198
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
199
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(ansi.bold + ansi.white + title + ansi.reset, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
200
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
201
|
+
|
|
202
|
+
// Context Bar (Optional)
|
|
203
|
+
if (context) {
|
|
204
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
205
|
+
const ctxStr = `${ansi.gray}${context.label}: ${ansi.reset}${ansi.cyan}${context.value}${ansi.reset}`;
|
|
206
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(ctxStr, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
207
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
208
|
+
} else {
|
|
209
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
210
|
+
}
|
|
757
211
|
|
|
758
|
-
|
|
759
|
-
if (ms < 1000) return `${ms}ms`;
|
|
760
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
761
|
-
return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
|
|
212
|
+
console.log(lines.join('\n'));
|
|
762
213
|
}
|
|
763
214
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
215
|
+
/**
|
|
216
|
+
* Renders the Standard Footer
|
|
217
|
+
*/
|
|
218
|
+
export function renderScreenFooter() {
|
|
219
|
+
console.log(ansi.gray + BOX.bl + BOX.h.repeat(WIDTH - 2) + BOX.br + ansi.reset);
|
|
770
220
|
}
|
|
771
221
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Renders a Standard Verdict/Telemetry Table
|
|
224
|
+
*/
|
|
225
|
+
export function renderVerdictTable(
|
|
226
|
+
stats: { duration: number, heap: number, files?: number },
|
|
227
|
+
score: number,
|
|
228
|
+
findings: any[]
|
|
229
|
+
) {
|
|
230
|
+
const lines: string[] = [];
|
|
231
|
+
|
|
232
|
+
// Telemetry Row
|
|
233
|
+
const heapMB = Math.round(stats.heap / 1024 / 1024);
|
|
234
|
+
const statsStr = `📡 TELEMETRY │ ⏱ ${stats.duration}ms │ 📂 ${stats.files || '?'} Files │ 📦 ${heapMB}MB`;
|
|
235
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(statsStr, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
236
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
237
|
+
|
|
238
|
+
// Score Row
|
|
239
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
240
|
+
const bar = renderProgressBar(score, 20);
|
|
241
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(`HEALTH SCORE [${bar}] ${score} / 100`, WIDTH + 18) + ansi.gray + BOX.v + ansi.reset);
|
|
242
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
243
|
+
|
|
244
|
+
if (findings.length > 0) {
|
|
245
|
+
// Table Header
|
|
246
|
+
const C1=10, C2=13, C3=41;
|
|
247
|
+
const tTop = ` ${BOX.ltl}${BOX.lh.repeat(C1)}${BOX.lt}${BOX.lh.repeat(C2)}${BOX.lt}${BOX.lh.repeat(C3)}${BOX.ltr} `;
|
|
248
|
+
const header = ` ${BOX.lv}${padRight(' SEVERITY', C1)}${BOX.lv}${padRight(' TYPE', C2)}${BOX.lv}${padRight(' FINDING', C3)}${BOX.lv} `;
|
|
249
|
+
const tBot = ` ${BOX.lbl}${BOX.lh.repeat(C1)}${BOX.lb}${BOX.lh.repeat(C2)}${BOX.lb}${BOX.lh.repeat(C3)}${BOX.lbr} `;
|
|
250
|
+
|
|
251
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ansi.gray + tTop + ansi.reset + ansi.gray + BOX.v + ansi.reset);
|
|
252
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ansi.bold + header + ansi.reset + ansi.gray + BOX.v + ansi.reset);
|
|
253
|
+
|
|
254
|
+
// Rows
|
|
255
|
+
findings.slice(0, 5).forEach(f => {
|
|
256
|
+
let sev = style.subtle(' INFO ');
|
|
257
|
+
if (f.severity === 'critical' || f.severity === 'BLOCK') sev = style.error('🛑 CRIT ');
|
|
258
|
+
else if (f.severity === 'high' || f.severity === 'WARN') sev = style.warning('🟡 WARN ');
|
|
259
|
+
|
|
260
|
+
const row = ` ${ansi.gray}${BOX.lv}${ansi.reset}${sev}${ansi.gray}${BOX.lv}${ansi.reset}${padRight(' '+(f.category||'General'), C2)}${ansi.gray}${BOX.lv}${ansi.reset}${padRight(' '+(f.message||f.title), C3)}${ansi.gray}${BOX.lv}${ansi.reset} `;
|
|
261
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + row + ansi.gray + BOX.v + ansi.reset);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ansi.gray + tBot + ansi.reset + ansi.gray + BOX.v + ansi.reset);
|
|
265
|
+
} else {
|
|
266
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(style.success('✅ NO ISSUES FOUND'), WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
814
267
|
}
|
|
815
|
-
|
|
816
|
-
lines.push('');
|
|
817
|
-
return lines.join('\n');
|
|
818
|
-
}
|
|
819
268
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
module.exports = {
|
|
825
|
-
// ANSI codes
|
|
826
|
-
ansi,
|
|
827
|
-
colors,
|
|
828
|
-
box,
|
|
829
|
-
icons,
|
|
830
|
-
|
|
831
|
-
// Components
|
|
832
|
-
Spinner,
|
|
833
|
-
ProgressBar,
|
|
834
|
-
PhaseProgress,
|
|
835
|
-
|
|
836
|
-
// Renderers
|
|
837
|
-
renderScoreCard,
|
|
838
|
-
renderFindingsList,
|
|
839
|
-
renderFinding,
|
|
840
|
-
renderTable,
|
|
841
|
-
renderSection,
|
|
842
|
-
renderDivider,
|
|
843
|
-
renderBanner,
|
|
844
|
-
|
|
845
|
-
// Utilities
|
|
846
|
-
truncate,
|
|
847
|
-
stripAnsi,
|
|
848
|
-
formatNumber,
|
|
849
|
-
formatDuration,
|
|
850
|
-
formatBytes,
|
|
851
|
-
getScoreColor,
|
|
852
|
-
getGrade,
|
|
853
|
-
};
|
|
269
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
270
|
+
console.log(lines.join('\n'));
|
|
271
|
+
}
|