@vibecheckai/cli 3.1.2 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +60 -33
  2. package/bin/registry.js +319 -34
  3. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  4. package/bin/runners/REPORT_AUDIT.md +64 -0
  5. package/bin/runners/lib/entitlements-v2.js +97 -28
  6. package/bin/runners/lib/entitlements.js +3 -6
  7. package/bin/runners/lib/init-wizard.js +1 -1
  8. package/bin/runners/lib/report-engine.js +459 -280
  9. package/bin/runners/lib/report-html.js +1154 -1423
  10. package/bin/runners/lib/report-output.js +187 -0
  11. package/bin/runners/lib/report-templates.js +848 -850
  12. package/bin/runners/lib/scan-output.js +545 -0
  13. package/bin/runners/lib/server-usage.js +0 -12
  14. package/bin/runners/lib/ship-output.js +641 -0
  15. package/bin/runners/lib/status-output.js +253 -0
  16. package/bin/runners/lib/terminal-ui.js +853 -0
  17. package/bin/runners/runCheckpoint.js +502 -0
  18. package/bin/runners/runContracts.js +105 -0
  19. package/bin/runners/runExport.js +93 -0
  20. package/bin/runners/runFix.js +31 -24
  21. package/bin/runners/runInit.js +377 -112
  22. package/bin/runners/runInstall.js +1 -5
  23. package/bin/runners/runLabs.js +3 -3
  24. package/bin/runners/runPolish.js +2452 -0
  25. package/bin/runners/runProve.js +2 -2
  26. package/bin/runners/runReport.js +251 -200
  27. package/bin/runners/runRuntime.js +110 -0
  28. package/bin/runners/runScan.js +477 -379
  29. package/bin/runners/runSecurity.js +92 -0
  30. package/bin/runners/runShip.js +137 -207
  31. package/bin/runners/runStatus.js +16 -68
  32. package/bin/runners/utils.js +5 -5
  33. package/bin/vibecheck.js +25 -11
  34. package/mcp-server/index.js +150 -18
  35. package/mcp-server/package.json +2 -2
  36. package/mcp-server/premium-tools.js +13 -13
  37. package/mcp-server/tier-auth.js +292 -27
  38. package/mcp-server/vibecheck-tools.js +9 -9
  39. package/package.json +1 -1
  40. package/bin/runners/runClaimVerifier.js +0 -483
  41. package/bin/runners/runContextCompiler.js +0 -385
  42. package/bin/runners/runGate.js +0 -17
  43. package/bin/runners/runInitGha.js +0 -164
  44. package/bin/runners/runInteractive.js +0 -388
  45. package/bin/runners/runMdc.js +0 -204
  46. package/bin/runners/runMissionGenerator.js +0 -282
  47. package/bin/runners/runTruthpack.js +0 -636
@@ -0,0 +1,853 @@
1
+ /**
2
+ * Terminal UI - Premium CLI Components
3
+ *
4
+ * Reusable across all vibecheck commands:
5
+ * - Advanced spinners with phases
6
+ * - Multi-line progress displays
7
+ * - Score visualizations
8
+ * - Tables and cards
9
+ * - Color utilities
10
+ *
11
+ * Zero dependencies - pure ANSI escape codes
12
+ */
13
+
14
+ // ═══════════════════════════════════════════════════════════════════════════════
15
+ // ANSI ESCAPE CODES
16
+ // ═══════════════════════════════════════════════════════════════════════════════
17
+
18
+ const ESC = '\x1b';
19
+
20
+ const ansi = {
21
+ // Text styles
22
+ reset: `${ESC}[0m`,
23
+ bold: `${ESC}[1m`,
24
+ dim: `${ESC}[2m`,
25
+ italic: `${ESC}[3m`,
26
+ underline: `${ESC}[4m`,
27
+ inverse: `${ESC}[7m`,
28
+ strikethrough: `${ESC}[9m`,
29
+
30
+ // Standard colors
31
+ black: `${ESC}[30m`,
32
+ red: `${ESC}[31m`,
33
+ green: `${ESC}[32m`,
34
+ yellow: `${ESC}[33m`,
35
+ blue: `${ESC}[34m`,
36
+ magenta: `${ESC}[35m`,
37
+ cyan: `${ESC}[36m`,
38
+ white: `${ESC}[37m`,
39
+
40
+ // Bright colors
41
+ gray: `${ESC}[90m`,
42
+ brightRed: `${ESC}[91m`,
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`,
52
+ bgRed: `${ESC}[41m`,
53
+ bgGreen: `${ESC}[42m`,
54
+ bgYellow: `${ESC}[43m`,
55
+ bgBlue: `${ESC}[44m`,
56
+ bgMagenta: `${ESC}[45m`,
57
+ bgCyan: `${ESC}[46m`,
58
+ bgWhite: `${ESC}[47m`,
59
+
60
+ // Cursor control
61
+ hideCursor: `${ESC}[?25l`,
62
+ showCursor: `${ESC}[?25h`,
63
+ saveCursor: `${ESC}[s`,
64
+ restoreCursor: `${ESC}[u`,
65
+ clearLine: `${ESC}[2K`,
66
+ clearScreen: `${ESC}[2J`,
67
+ 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
+ };
118
+
119
+ // ═══════════════════════════════════════════════════════════════════════════════
120
+ // BOX DRAWING CHARACTERS
121
+ // ═══════════════════════════════════════════════════════════════════════════════
122
+
123
+ const box = {
124
+ // Rounded corners
125
+ topLeft: '╭',
126
+ topRight: '╮',
127
+ bottomLeft: '╰',
128
+ bottomRight: '╯',
129
+ horizontal: '─',
130
+ vertical: '│',
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: '▐',
162
+ };
163
+
164
+ // ═══════════════════════════════════════════════════════════════════════════════
165
+ // ICONS & SYMBOLS
166
+ // ═══════════════════════════════════════════════════════════════════════════════
167
+
168
+ const icons = {
169
+ // Status
170
+ success: '✓',
171
+ error: '✗',
172
+ warning: '⚠',
173
+ info: 'ℹ',
174
+ question: '?',
175
+
176
+ // Arrows
177
+ arrowRight: '→',
178
+ arrowLeft: '←',
179
+ arrowUp: '↑',
180
+ arrowDown: '↓',
181
+ arrowBoth: '↔',
182
+
183
+ // Pointers
184
+ pointer: '❯',
185
+ pointerSmall: '›',
186
+ bullet: '•',
187
+ dot: '·',
188
+
189
+ // Misc
190
+ star: '★',
191
+ heart: '♥',
192
+ lightning: '⚡',
193
+ fire: '🔥',
194
+ rocket: '🚀',
195
+ check: '☑',
196
+ radioOn: '◉',
197
+ radioOff: '○',
198
+
199
+ // Severity badges
200
+ critical: '🚨',
201
+ high: '🔴',
202
+ medium: '🟡',
203
+ low: '🔵',
204
+ };
205
+
206
+ // ═══════════════════════════════════════════════════════════════════════════════
207
+ // SPINNER - Advanced Multi-Phase Spinner
208
+ // ═══════════════════════════════════════════════════════════════════════════════
209
+
210
+ const SPINNER_FRAMES = {
211
+ dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
212
+ line: ['|', '/', '-', '\\'],
213
+ arc: ['◜', '◠', '◝', '◞', '◡', '◟'],
214
+ circle: ['◐', '◓', '◑', '◒'],
215
+ bounce: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'],
216
+ pulse: ['█', '▓', '▒', '░', '▒', '▓'],
217
+ };
218
+
219
+ class Spinner {
220
+ constructor(options = {}) {
221
+ this.frames = SPINNER_FRAMES[options.type || 'dots'];
222
+ this.interval = options.interval || 80;
223
+ this.stream = options.stream || process.stdout;
224
+ this.color = options.color || colors.primary;
225
+ this.frameIndex = 0;
226
+ this.timer = null;
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
+ }
334
+ }
335
+
336
+ // ═══════════════════════════════════════════════════════════════════════════════
337
+ // PROGRESS BAR
338
+ // ═══════════════════════════════════════════════════════════════════════════════
339
+
340
+ class ProgressBar {
341
+ constructor(options = {}) {
342
+ this.total = options.total || 100;
343
+ this.width = options.width || 40;
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
+ }
370
+ }
371
+
372
+ // ═══════════════════════════════════════════════════════════════════════════════
373
+ // MULTI-LINE PROGRESS - Phase-Based Progress Display
374
+ // ═══════════════════════════════════════════════════════════════════════════════
375
+
376
+ class PhaseProgress {
377
+ constructor(phases, options = {}) {
378
+ this.phases = phases.map(p => ({
379
+ name: p.name || p,
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
+ }
510
+ }
511
+
512
+ // ═══════════════════════════════════════════════════════════════════════════════
513
+ // SCORE DISPLAY - Animated Score Card
514
+ // ═══════════════════════════════════════════════════════════════════════════════
515
+
516
+ function getScoreColor(score) {
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
+ }
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');
595
+ }
596
+
597
+ // ═══════════════════════════════════════════════════════════════════════════════
598
+ // FINDINGS LIST - Premium Findings Display
599
+ // ═══════════════════════════════════════════════════════════════════════════════
600
+
601
+ function renderFindingsList(findings, options = {}) {
602
+ const { maxItems = 10, showCode = false, groupBySeverity = true } = options;
603
+
604
+ if (!findings || findings.length === 0) {
605
+ return `\n ${colors.success}${icons.success}${ansi.reset} ${ansi.bold}No issues found${ansi.reset}\n`;
606
+ }
607
+
608
+ const lines = [];
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
+ }
645
+ }
646
+
647
+ return lines.join('\n');
648
+ }
649
+
650
+ function renderFinding(finding, options = {}) {
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;
688
+ }
689
+
690
+ // ═══════════════════════════════════════════════════════════════════════════════
691
+ // TABLE RENDERING
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');
724
+ }
725
+
726
+ // ═══════════════════════════════════════════════════════════════════════════════
727
+ // SECTION HEADERS
728
+ // ═══════════════════════════════════════════════════════════════════════════════
729
+
730
+ function renderSection(title, icon = '◆') {
731
+ return `\n ${colors.accent}${icon}${ansi.reset} ${ansi.bold}${title}${ansi.reset}\n ${ansi.dim}${'─'.repeat(60)}${ansi.reset}`;
732
+ }
733
+
734
+ function renderDivider(char = '─', width = 60) {
735
+ return ` ${ansi.dim}${char.repeat(width)}${ansi.reset}`;
736
+ }
737
+
738
+ // ═══════════════════════════════════════════════════════════════════════════════
739
+ // UTILITY FUNCTIONS
740
+ // ═══════════════════════════════════════════════════════════════════════════════
741
+
742
+ function truncate(str, len) {
743
+ if (!str) return '';
744
+ str = String(str);
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
+
758
+ function formatDuration(ms) {
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`;
762
+ }
763
+
764
+ function formatBytes(bytes) {
765
+ if (bytes === 0) return '0 B';
766
+ const k = 1024;
767
+ const sizes = ['B', 'KB', 'MB', 'GB'];
768
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
769
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
770
+ }
771
+
772
+ // ═══════════════════════════════════════════════════════════════════════════════
773
+ // BANNER GENERATOR
774
+ // ═══════════════════════════════════════════════════════════════════════════════
775
+
776
+ function renderBanner(name = 'VIBECHECK', subtitle = '') {
777
+ const gradient = [
778
+ ansi.rgb(0, 200, 255),
779
+ ansi.rgb(30, 180, 255),
780
+ ansi.rgb(60, 160, 255),
781
+ ansi.rgb(90, 140, 255),
782
+ ansi.rgb(120, 120, 255),
783
+ ansi.rgb(150, 100, 255),
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');
818
+ }
819
+
820
+ // ═══════════════════════════════════════════════════════════════════════════════
821
+ // EXPORTS
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
+ };