@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.
@@ -1,34 +1,33 @@
1
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
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
- // ANSI ESCAPE CODES
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
- inverse: `${ESC}[7m`,
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
- 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`,
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
- bgWhite: `${ESC}[47m`,
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
- // 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: '▐',
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
- dot: '·',
188
-
189
- // Misc
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
- // SPINNER - Advanced Multi-Phase Spinner
86
+ // 2. LAYOUT PRIMITIVES (The "Grid")
208
87
  // ═══════════════════════════════════════════════════════════════════════════════
209
88
 
210
- const SPINNER_FRAMES = {
211
- dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
212
- line: ['|', '/', '-', '\\'],
213
- arc: ['', '◠', '', '◞', '◡', '◟'],
214
- circle: ['◐', '◓', '◑', '◒'],
215
- bounce: ['', '⠂', '', '⡀', '', '', '', ''],
216
- pulse: ['', '▓', '', '', '', ''],
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
- 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
- }
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
- // PROGRESS BAR
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
- 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
- }
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
- // MULTI-LINE PROGRESS - Phase-Based Progress Display
129
+ // 3. COMPONENT: SPINNER
374
130
  // ═══════════════════════════════════════════════════════════════════════════════
375
131
 
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;
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
- 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
- }
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
- 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;
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
- this._render();
455
- this.stream.write(ansi.showCursor);
160
+ process.stdout.write(ansi.showCursor);
456
161
  return this;
457
162
  }
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
- }
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
- // SCORE DISPLAY - Animated Score Card
170
+ // 4. COMPONENT: PROGRESS BAR
514
171
  // ═══════════════════════════════════════════════════════════════════════════════
515
172
 
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');
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
- // FINDINGS LIST - Premium Findings Display
179
+ // 5. STANDARD RENDERERS (The "Look")
599
180
  // ═══════════════════════════════════════════════════════════════════════════════
600
181
 
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
-
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
- 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
- };
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
- 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
- }
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
- 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
- }
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
- 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;
217
+ console.log(lines.join('\n'));
688
218
  }
689
219
 
690
- // ═══════════════════════════════════════════════════════════════════════════════
691
- // TABLE RENDERING
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
- 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
-
227
+ /**
228
+ * Renders a Standard Verdict/Telemetry Table
229
+ */
230
+ function renderVerdictTable(stats, score, findings) {
703
231
  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));
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
- lines.push(` ${ansi.dim}${box.vertical}${ansi.reset}${cells.join(`${ansi.dim}│${ansi.reset}`)}${ansi.dim}${box.vertical}${ansi.reset}`);
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(` ${ansi.dim}${box.bottomLeft}${''.repeat(totalWidth - 2)}${box.bottomRight}${ansi.reset}`);
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
- // SECTION HEADERS
275
+ // COLORS OBJECT (for compatibility)
728
276
  // ═══════════════════════════════════════════════════════════════════════════════
729
277
 
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
- }
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
- 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
-
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
- return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
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
- 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];
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
- // BANNER GENERATOR
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
- 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');
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
- // ANSI codes
333
+ WIDTH,
826
334
  ansi,
827
335
  colors,
828
- box,
336
+ style,
829
337
  icons,
830
-
831
- // Components
338
+ BOX,
339
+ padCenter,
340
+ padRight,
341
+ truncate,
832
342
  Spinner,
833
- ProgressBar,
834
343
  PhaseProgress,
835
-
836
- // Renderers
837
- renderScoreCard,
838
- renderFindingsList,
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
- formatBytes,
851
- getScoreColor,
852
- getGrade,
853
- };
351
+ };