@vibecheckai/cli 3.2.1 → 3.2.3
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 +231 -733
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runShip.js +7 -8
- package/package.json +1 -1
|
@@ -1,34 +1,33 @@
|
|
|
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
|
+
"use strict";
|
|
11
|
+
|
|
14
12
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
-
//
|
|
13
|
+
// 1. CORE CONSTANTS & ANSI
|
|
16
14
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
17
15
|
|
|
16
|
+
const WIDTH = 76;
|
|
17
|
+
|
|
18
18
|
const ESC = '\x1b';
|
|
19
|
+
const SUPPORTS_TRUECOLOR = process.env.COLORTERM === 'truecolor' ||
|
|
20
|
+
process.env.TERM_PROGRAM === 'iTerm.app' ||
|
|
21
|
+
process.env.TERM_PROGRAM === 'Apple_Terminal' ||
|
|
22
|
+
process.env.WT_SESSION;
|
|
19
23
|
|
|
20
24
|
const ansi = {
|
|
21
|
-
// Text styles
|
|
22
25
|
reset: `${ESC}[0m`,
|
|
23
26
|
bold: `${ESC}[1m`,
|
|
24
27
|
dim: `${ESC}[2m`,
|
|
25
28
|
italic: `${ESC}[3m`,
|
|
26
29
|
underline: `${ESC}[4m`,
|
|
27
|
-
|
|
28
|
-
strikethrough: `${ESC}[9m`,
|
|
29
|
-
|
|
30
|
-
// Standard colors
|
|
31
|
-
black: `${ESC}[30m`,
|
|
30
|
+
// Colors
|
|
32
31
|
red: `${ESC}[31m`,
|
|
33
32
|
green: `${ESC}[32m`,
|
|
34
33
|
yellow: `${ESC}[33m`,
|
|
@@ -36,785 +35,294 @@ const ansi = {
|
|
|
36
35
|
magenta: `${ESC}[35m`,
|
|
37
36
|
cyan: `${ESC}[36m`,
|
|
38
37
|
white: `${ESC}[37m`,
|
|
39
|
-
|
|
40
|
-
// Bright colors
|
|
41
38
|
gray: `${ESC}[90m`,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
brightMagenta: `${ESC}[95m`,
|
|
47
|
-
brightCyan: `${ESC}[96m`,
|
|
48
|
-
brightWhite: `${ESC}[97m`,
|
|
49
|
-
|
|
50
|
-
// Background colors
|
|
51
|
-
bgBlack: `${ESC}[40m`,
|
|
39
|
+
// RGB colors (truecolor support)
|
|
40
|
+
rgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `${ESC}[38;2;${r};${g};${b}m` : '',
|
|
41
|
+
bgRgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `${ESC}[48;2;${r};${g};${b}m` : '',
|
|
42
|
+
// Backgrounds
|
|
52
43
|
bgRed: `${ESC}[41m`,
|
|
53
44
|
bgGreen: `${ESC}[42m`,
|
|
54
45
|
bgYellow: `${ESC}[43m`,
|
|
55
46
|
bgBlue: `${ESC}[44m`,
|
|
56
47
|
bgMagenta: `${ESC}[45m`,
|
|
57
48
|
bgCyan: `${ESC}[46m`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Cursor control
|
|
49
|
+
// Cursor
|
|
61
50
|
hideCursor: `${ESC}[?25l`,
|
|
62
51
|
showCursor: `${ESC}[?25h`,
|
|
63
|
-
saveCursor: `${ESC}[s`,
|
|
64
|
-
restoreCursor: `${ESC}[u`,
|
|
65
52
|
clearLine: `${ESC}[2K`,
|
|
66
|
-
clearScreen: `${ESC}[2J`,
|
|
67
53
|
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
54
|
};
|
|
118
55
|
|
|
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: '▐',
|
|
56
|
+
// Semantic Palette
|
|
57
|
+
const style = {
|
|
58
|
+
success: (t) => `${ansi.green}${t}${ansi.reset}`,
|
|
59
|
+
error: (t) => `${ansi.red}${t}${ansi.reset}`,
|
|
60
|
+
warning: (t) => `${ansi.yellow}${t}${ansi.reset}`,
|
|
61
|
+
info: (t) => `${ansi.cyan}${t}${ansi.reset}`,
|
|
62
|
+
subtle: (t) => `${ansi.gray}${t}${ansi.reset}`,
|
|
63
|
+
highlight: (t) => `${ansi.bold}${ansi.white}${t}${ansi.reset}`,
|
|
64
|
+
label: (t) => `${ansi.blue}${t}${ansi.reset}`,
|
|
65
|
+
// Backgrounds for badges
|
|
66
|
+
bgSuccess: (t) => `${ansi.bgGreen}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
|
|
67
|
+
bgError: (t) => `${ansi.bgRed}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
|
|
68
|
+
bgWarn: (t) => `${ansi.bgYellow}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
|
|
162
69
|
};
|
|
163
70
|
|
|
164
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
165
|
-
// ICONS & SYMBOLS
|
|
166
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
167
|
-
|
|
168
71
|
const icons = {
|
|
169
|
-
// Status
|
|
170
72
|
success: '✓',
|
|
171
73
|
error: '✗',
|
|
172
74
|
warning: '⚠',
|
|
173
75
|
info: 'ℹ',
|
|
174
|
-
question: '?',
|
|
175
|
-
|
|
176
|
-
// Arrows
|
|
177
|
-
arrowRight: '→',
|
|
178
|
-
arrowLeft: '←',
|
|
179
|
-
arrowUp: '↑',
|
|
180
|
-
arrowDown: '↓',
|
|
181
|
-
arrowBoth: '↔',
|
|
182
|
-
|
|
183
|
-
// Pointers
|
|
184
|
-
pointer: '❯',
|
|
185
|
-
pointerSmall: '›',
|
|
186
76
|
bullet: '•',
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
star: '★',
|
|
191
|
-
heart: '♥',
|
|
192
|
-
lightning: '⚡',
|
|
193
|
-
fire: '🔥',
|
|
194
|
-
rocket: '🚀',
|
|
195
|
-
check: '☑',
|
|
77
|
+
pointer: '❯',
|
|
78
|
+
arrowRight: '→',
|
|
79
|
+
line: '─',
|
|
196
80
|
radioOn: '◉',
|
|
197
81
|
radioOff: '○',
|
|
198
|
-
|
|
199
|
-
// Severity badges
|
|
200
|
-
critical: '🚨',
|
|
201
|
-
high: '🔴',
|
|
202
|
-
medium: '🟡',
|
|
203
|
-
low: '🔵',
|
|
82
|
+
lock: '🔒',
|
|
204
83
|
};
|
|
205
84
|
|
|
206
85
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
207
|
-
//
|
|
86
|
+
// 2. LAYOUT PRIMITIVES (The "Grid")
|
|
208
87
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
209
88
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
89
|
+
const BOX = {
|
|
90
|
+
// Outer Frame (Double)
|
|
91
|
+
tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║',
|
|
92
|
+
trT: '╠', tlT: '╣',
|
|
93
|
+
// Inner Dividers (Single)
|
|
94
|
+
ltl: '┌', ltr: '┐', lbl: '└', lbr: '┘', lh: '─', lv: '│',
|
|
95
|
+
lt: '┬', lb: '┴', lx: '┼', ltrT: '├', ltlT: '┤'
|
|
217
96
|
};
|
|
218
97
|
|
|
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
|
-
}
|
|
98
|
+
/**
|
|
99
|
+
* Centers text within the standard width, accounting for ANSI codes
|
|
100
|
+
*/
|
|
101
|
+
function padCenter(str, width = WIDTH - 2) {
|
|
102
|
+
const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
|
|
103
|
+
const padding = Math.max(0, width - visibleLen);
|
|
104
|
+
const left = Math.floor(padding / 2);
|
|
105
|
+
return ' '.repeat(left) + str + ' '.repeat(padding - left);
|
|
334
106
|
}
|
|
335
107
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Pads text to the right, accounting for ANSI codes
|
|
110
|
+
*/
|
|
111
|
+
function padRight(str, len) {
|
|
112
|
+
const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
|
|
113
|
+
const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
|
|
114
|
+
const finalLen = truncated.replace(/\u001b\[\d+m/g, '').length;
|
|
115
|
+
return truncated + ' '.repeat(Math.max(0, len - finalLen));
|
|
116
|
+
}
|
|
339
117
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
}
|
|
118
|
+
/**
|
|
119
|
+
* Truncates text with ellipsis
|
|
120
|
+
*/
|
|
121
|
+
function truncate(str, len) {
|
|
122
|
+
if (!str) return '';
|
|
123
|
+
const clean = str.replace(/\u001b\[\d+m/g, '');
|
|
124
|
+
if (clean.length <= len) return str;
|
|
125
|
+
return clean.substring(0, len - 3) + '...';
|
|
370
126
|
}
|
|
371
127
|
|
|
372
128
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
373
|
-
//
|
|
129
|
+
// 3. COMPONENT: SPINNER
|
|
374
130
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
375
131
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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;
|
|
132
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
133
|
+
|
|
134
|
+
class Spinner {
|
|
135
|
+
constructor(text = '', colorStr = ansi.cyan) {
|
|
136
|
+
this.text = text;
|
|
137
|
+
this.colorStr = colorStr;
|
|
138
|
+
this.timer = null;
|
|
139
|
+
this.frameIndex = 0;
|
|
438
140
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (this.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
this.
|
|
445
|
-
|
|
141
|
+
|
|
142
|
+
start(text) {
|
|
143
|
+
if (text) this.text = text;
|
|
144
|
+
process.stdout.write(ansi.hideCursor);
|
|
145
|
+
this.timer = setInterval(() => {
|
|
146
|
+
const frame = SPINNER_FRAMES[this.frameIndex];
|
|
147
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
148
|
+
process.stdout.write(`\r ${this.colorStr}${frame}${ansi.reset} ${this.text}`);
|
|
149
|
+
}, 80);
|
|
446
150
|
return this;
|
|
447
151
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (this.
|
|
451
|
-
|
|
452
|
-
|
|
152
|
+
|
|
153
|
+
stop(symbol = icons.success, color = ansi.green, finalMsg) {
|
|
154
|
+
if (this.timer) clearInterval(this.timer);
|
|
155
|
+
process.stdout.write(`\r${ansi.clearLine}`);
|
|
156
|
+
if (finalMsg !== null) { // Pass null to clear completely
|
|
157
|
+
const msg = finalMsg || this.text;
|
|
158
|
+
console.log(` ${color}${symbol}${ansi.reset} ${msg}`);
|
|
453
159
|
}
|
|
454
|
-
|
|
455
|
-
this.stream.write(ansi.showCursor);
|
|
160
|
+
process.stdout.write(ansi.showCursor);
|
|
456
161
|
return this;
|
|
457
162
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
}
|
|
163
|
+
|
|
164
|
+
succeed(text) { return this.stop(icons.success, ansi.green, text); }
|
|
165
|
+
fail(text) { return this.stop(icons.error, ansi.red, text); }
|
|
166
|
+
warn(text) { return this.stop(icons.warning, ansi.yellow, text); }
|
|
510
167
|
}
|
|
511
168
|
|
|
512
169
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
513
|
-
//
|
|
170
|
+
// 4. COMPONENT: PROGRESS BAR
|
|
514
171
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
515
172
|
|
|
516
|
-
function
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
}
|
|
530
|
-
|
|
531
|
-
function renderScoreCard(score, options = {}) {
|
|
532
|
-
const {
|
|
533
|
-
verdict = score >= 80 ? 'SHIP' : score >= 60 ? 'WARN' : 'BLOCK',
|
|
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}`);
|
|
575
|
-
|
|
576
|
-
// Findings summary
|
|
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');
|
|
173
|
+
function renderProgressBar(percentage, width = 20, color = ansi.green) {
|
|
174
|
+
const filled = Math.round((percentage / 100) * width);
|
|
175
|
+
return `${color}${'█'.repeat(filled)}${ansi.gray}${'░'.repeat(width - filled)}${ansi.reset}`;
|
|
595
176
|
}
|
|
596
177
|
|
|
597
178
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
598
|
-
//
|
|
179
|
+
// 5. STANDARD RENDERERS (The "Look")
|
|
599
180
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
600
181
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return `\n ${colors.success}${icons.success}${ansi.reset} ${ansi.bold}No issues found${ansi.reset}\n`;
|
|
606
|
-
}
|
|
607
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Renders the Standard Double-Border Header with Logo
|
|
184
|
+
*/
|
|
185
|
+
function renderScreenHeader(logoAscii, title, logoColor = (t) => t, context) {
|
|
608
186
|
const lines = [];
|
|
609
187
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
188
|
+
// Top Frame
|
|
189
|
+
lines.push(ansi.gray + BOX.tl + BOX.h.repeat(WIDTH - 2) + BOX.tr + ansi.reset);
|
|
190
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
191
|
+
|
|
192
|
+
// Logo Processing
|
|
193
|
+
logoAscii.trim().split('\n').forEach(line => {
|
|
194
|
+
const cleanLine = line.replace(/\r/g, '');
|
|
195
|
+
const maxLen = Math.max(...logoAscii.split('\n').map(l => l.length));
|
|
196
|
+
const padding = ' '.repeat(Math.floor((WIDTH - 2 - maxLen) / 2));
|
|
197
|
+
const rightPad = ' '.repeat(WIDTH - 2 - padding.length - cleanLine.length);
|
|
617
198
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
}
|
|
199
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padding + logoColor(cleanLine) + rightPad + ansi.gray + BOX.v + ansi.reset);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Title
|
|
203
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
204
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(ansi.bold + ansi.white + title + ansi.reset, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
205
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
206
|
+
|
|
207
|
+
// Context Bar (Optional)
|
|
208
|
+
if (context) {
|
|
209
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
210
|
+
const ctxStr = `${ansi.gray}${context.label}: ${ansi.reset}${ansi.cyan}${context.value}${ansi.reset}`;
|
|
211
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(ctxStr, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
212
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
636
213
|
} else {
|
|
637
|
-
|
|
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
|
-
}
|
|
214
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
645
215
|
}
|
|
646
|
-
|
|
647
|
-
return lines.join('\n');
|
|
648
|
-
}
|
|
649
216
|
|
|
650
|
-
|
|
651
|
-
const { showCode = false, color = ansi.dim } = options;
|
|
652
|
-
const lines = [];
|
|
653
|
-
|
|
654
|
-
const severityColor = {
|
|
655
|
-
critical: colors.critical,
|
|
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)}`);
|
|
682
|
-
}
|
|
683
|
-
lines.push(` ${ansi.dim}└──────────────────────────────────────${ansi.reset}`);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
lines.push('');
|
|
687
|
-
return lines;
|
|
217
|
+
console.log(lines.join('\n'));
|
|
688
218
|
}
|
|
689
219
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Renders the Standard Footer
|
|
222
|
+
*/
|
|
223
|
+
function renderScreenFooter() {
|
|
224
|
+
console.log(ansi.gray + BOX.bl + BOX.h.repeat(WIDTH - 2) + BOX.br + ansi.reset);
|
|
225
|
+
}
|
|
693
226
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Renders a Standard Verdict/Telemetry Table
|
|
229
|
+
*/
|
|
230
|
+
function renderVerdictTable(stats, score, findings) {
|
|
703
231
|
const lines = [];
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
lines.push(
|
|
709
|
-
lines.push(
|
|
710
|
-
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
232
|
+
|
|
233
|
+
// Telemetry Row
|
|
234
|
+
const heapMB = Math.round(stats.heap / 1024 / 1024);
|
|
235
|
+
const statsStr = `📡 TELEMETRY │ ⏱ ${stats.duration}ms │ 📂 ${stats.files || '?'} Files │ 📦 ${heapMB}MB`;
|
|
236
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(statsStr, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
237
|
+
lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
|
|
238
|
+
|
|
239
|
+
// Score Row
|
|
240
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
241
|
+
const bar = renderProgressBar(score, 20);
|
|
242
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(`HEALTH SCORE [${bar}] ${score} / 100`, WIDTH + 18) + ansi.gray + BOX.v + ansi.reset);
|
|
243
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
244
|
+
|
|
245
|
+
if (findings.length > 0) {
|
|
246
|
+
// Table Header
|
|
247
|
+
const C1=10, C2=13, C3=41;
|
|
248
|
+
const tTop = ` ${BOX.ltl}${BOX.lh.repeat(C1)}${BOX.lt}${BOX.lh.repeat(C2)}${BOX.lt}${BOX.lh.repeat(C3)}${BOX.ltr} `;
|
|
249
|
+
const header = ` ${BOX.lv}${padRight(' SEVERITY', C1)}${BOX.lv}${padRight(' TYPE', C2)}${BOX.lv}${padRight(' FINDING', C3)}${BOX.lv} `;
|
|
250
|
+
const tBot = ` ${BOX.lbl}${BOX.lh.repeat(C1)}${BOX.lb}${BOX.lh.repeat(C2)}${BOX.lb}${BOX.lh.repeat(C3)}${BOX.lbr} `;
|
|
251
|
+
|
|
252
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ansi.gray + tTop + ansi.reset + ansi.gray + BOX.v + ansi.reset);
|
|
253
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ansi.bold + header + ansi.reset + ansi.gray + BOX.v + ansi.reset);
|
|
254
|
+
|
|
255
|
+
// Rows
|
|
256
|
+
findings.slice(0, 5).forEach(f => {
|
|
257
|
+
let sev = style.subtle(' INFO ');
|
|
258
|
+
if (f.severity === 'critical' || f.severity === 'BLOCK') sev = style.error('🛑 CRIT ');
|
|
259
|
+
else if (f.severity === 'high' || f.severity === 'WARN') sev = style.warning('🟡 WARN ');
|
|
260
|
+
|
|
261
|
+
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} `;
|
|
262
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + row + ansi.gray + BOX.v + ansi.reset);
|
|
717
263
|
});
|
|
718
|
-
|
|
264
|
+
|
|
265
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ansi.gray + tBot + ansi.reset + ansi.gray + BOX.v + ansi.reset);
|
|
266
|
+
} else {
|
|
267
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(style.success('✅ NO ISSUES FOUND'), WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
719
268
|
}
|
|
720
|
-
|
|
721
|
-
lines.push(
|
|
722
|
-
|
|
723
|
-
return lines.join('\n');
|
|
269
|
+
|
|
270
|
+
lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
|
|
271
|
+
console.log(lines.join('\n'));
|
|
724
272
|
}
|
|
725
273
|
|
|
726
274
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
727
|
-
//
|
|
275
|
+
// COLORS OBJECT (for compatibility)
|
|
728
276
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
729
277
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
278
|
+
const colors = {
|
|
279
|
+
success: ansi.green,
|
|
280
|
+
error: ansi.red,
|
|
281
|
+
warning: ansi.yellow,
|
|
282
|
+
info: ansi.cyan,
|
|
283
|
+
accent: ansi.cyan,
|
|
284
|
+
muted: ansi.gray,
|
|
285
|
+
highlight: ansi.white,
|
|
286
|
+
};
|
|
737
287
|
|
|
738
288
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
739
289
|
// UTILITY FUNCTIONS
|
|
740
290
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
741
291
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const visible = stripAnsi(str);
|
|
746
|
-
if (visible.length <= len) return str;
|
|
747
|
-
return str.slice(0, len - 3) + '...';
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function stripAnsi(str) {
|
|
751
|
-
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function formatNumber(num) {
|
|
755
|
-
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
756
|
-
}
|
|
757
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Format duration in milliseconds to human-readable string
|
|
294
|
+
*/
|
|
758
295
|
function formatDuration(ms) {
|
|
759
296
|
if (ms < 1000) return `${ms}ms`;
|
|
760
297
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
761
|
-
|
|
298
|
+
const minutes = Math.floor(ms / 60000);
|
|
299
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
300
|
+
return `${minutes}m ${seconds}s`;
|
|
762
301
|
}
|
|
763
302
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
303
|
+
/**
|
|
304
|
+
* Render a section header
|
|
305
|
+
*/
|
|
306
|
+
function renderSection(title, icon = '•') {
|
|
307
|
+
return `\n ${ansi.cyan}${icon}${ansi.reset} ${ansi.bold}${title}${ansi.reset}\n ${ansi.gray}${BOX.lh.repeat(WIDTH - 4)}${ansi.reset}`;
|
|
770
308
|
}
|
|
771
309
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Render banner (placeholder - actual banner is in runScan.js)
|
|
312
|
+
*/
|
|
313
|
+
function renderBanner() {
|
|
314
|
+
// Banner is handled in individual command files
|
|
315
|
+
return '';
|
|
316
|
+
}
|
|
775
317
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
];
|
|
785
|
-
|
|
786
|
-
const asciiArt = {
|
|
787
|
-
V: ['██╗ ██╗', '██║ ██║', '██║ ██║', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚═══╝ '],
|
|
788
|
-
I: ['██╗', '██║', '██║', '██║', '██║', '╚═╝'],
|
|
789
|
-
B: ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '],
|
|
790
|
-
E: ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'],
|
|
791
|
-
C: [' ██████╗', '██╔════╝', '██║ ', '██║ ', '╚██████╗', ' ╚═════╝'],
|
|
792
|
-
H: ['██╗ ██╗', '██║ ██║', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'],
|
|
793
|
-
K: ['██╗ ██╗', '██║ ██╔╝', '█████╔╝ ', '██╔═██╗ ', '██║ ██╗', '╚═╝ ╚═╝'],
|
|
794
|
-
};
|
|
795
|
-
|
|
796
|
-
const lines = [];
|
|
797
|
-
lines.push('');
|
|
798
|
-
|
|
799
|
-
for (let row = 0; row < 6; row++) {
|
|
800
|
-
let line = ' ';
|
|
801
|
-
for (const char of name) {
|
|
802
|
-
if (asciiArt[char]) {
|
|
803
|
-
line += asciiArt[char][row];
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
lines.push(`${gradient[row]}${line}${ansi.reset}`);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (subtitle) {
|
|
810
|
-
lines.push('');
|
|
811
|
-
lines.push(` ${ansi.dim}${box.topLeft}${'─'.repeat(subtitle.length + 4)}${box.topRight}${ansi.reset}`);
|
|
812
|
-
lines.push(` ${ansi.dim}${box.vertical}${ansi.reset} ${subtitle} ${ansi.dim}${box.vertical}${ansi.reset}`);
|
|
813
|
-
lines.push(` ${ansi.dim}${box.bottomLeft}${'─'.repeat(subtitle.length + 4)}${box.bottomRight}${ansi.reset}`);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
lines.push('');
|
|
817
|
-
return lines.join('\n');
|
|
318
|
+
/**
|
|
319
|
+
* PhaseProgress class (placeholder)
|
|
320
|
+
*/
|
|
321
|
+
class PhaseProgress {
|
|
322
|
+
constructor() {}
|
|
323
|
+
start() { return this; }
|
|
324
|
+
update() { return this; }
|
|
325
|
+
stop() { return this; }
|
|
818
326
|
}
|
|
819
327
|
|
|
820
328
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -822,32 +330,22 @@ function renderBanner(name = 'VIBECHECK', subtitle = '') {
|
|
|
822
330
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
823
331
|
|
|
824
332
|
module.exports = {
|
|
825
|
-
|
|
333
|
+
WIDTH,
|
|
826
334
|
ansi,
|
|
827
335
|
colors,
|
|
828
|
-
|
|
336
|
+
style,
|
|
829
337
|
icons,
|
|
830
|
-
|
|
831
|
-
|
|
338
|
+
BOX,
|
|
339
|
+
padCenter,
|
|
340
|
+
padRight,
|
|
341
|
+
truncate,
|
|
832
342
|
Spinner,
|
|
833
|
-
ProgressBar,
|
|
834
343
|
PhaseProgress,
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
renderFinding,
|
|
840
|
-
renderTable,
|
|
344
|
+
renderProgressBar,
|
|
345
|
+
renderScreenHeader,
|
|
346
|
+
renderScreenFooter,
|
|
347
|
+
renderVerdictTable,
|
|
841
348
|
renderSection,
|
|
842
|
-
renderDivider,
|
|
843
349
|
renderBanner,
|
|
844
|
-
|
|
845
|
-
// Utilities
|
|
846
|
-
truncate,
|
|
847
|
-
stripAnsi,
|
|
848
|
-
formatNumber,
|
|
849
350
|
formatDuration,
|
|
850
|
-
|
|
851
|
-
getScoreColor,
|
|
852
|
-
getGrade,
|
|
853
|
-
};
|
|
351
|
+
};
|