@vibecheckai/cli 3.2.1 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,34 +1,28 @@
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
+ import process from 'process';
11
+
14
12
  // ═══════════════════════════════════════════════════════════════════════════════
15
- // ANSI ESCAPE CODES
13
+ // 1. CORE CONSTANTS & ANSI
16
14
  // ═══════════════════════════════════════════════════════════════════════════════
17
15
 
18
- const ESC = '\x1b';
16
+ export const WIDTH = 76;
19
17
 
20
- const ansi = {
21
- // Text styles
18
+ const ESC = '\x1b';
19
+ export const ansi = {
22
20
  reset: `${ESC}[0m`,
23
21
  bold: `${ESC}[1m`,
24
22
  dim: `${ESC}[2m`,
25
23
  italic: `${ESC}[3m`,
26
24
  underline: `${ESC}[4m`,
27
- inverse: `${ESC}[7m`,
28
- strikethrough: `${ESC}[9m`,
29
-
30
- // Standard colors
31
- black: `${ESC}[30m`,
25
+ // Colors
32
26
  red: `${ESC}[31m`,
33
27
  green: `${ESC}[32m`,
34
28
  yellow: `${ESC}[33m`,
@@ -36,818 +30,242 @@ const ansi = {
36
30
  magenta: `${ESC}[35m`,
37
31
  cyan: `${ESC}[36m`,
38
32
  white: `${ESC}[37m`,
39
-
40
- // Bright colors
41
33
  gray: `${ESC}[90m`,
42
- 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`,
34
+ // Backgrounds
52
35
  bgRed: `${ESC}[41m`,
53
36
  bgGreen: `${ESC}[42m`,
54
37
  bgYellow: `${ESC}[43m`,
55
38
  bgBlue: `${ESC}[44m`,
56
39
  bgMagenta: `${ESC}[45m`,
57
40
  bgCyan: `${ESC}[46m`,
58
- bgWhite: `${ESC}[47m`,
59
-
60
- // Cursor control
41
+ // Cursor
61
42
  hideCursor: `${ESC}[?25l`,
62
43
  showCursor: `${ESC}[?25h`,
63
- saveCursor: `${ESC}[s`,
64
- restoreCursor: `${ESC}[u`,
65
44
  clearLine: `${ESC}[2K`,
66
- clearScreen: `${ESC}[2J`,
67
45
  cursorUp: (n = 1) => `${ESC}[${n}A`,
68
- cursorDown: (n = 1) => `${ESC}[${n}B`,
69
- cursorRight: (n = 1) => `${ESC}[${n}C`,
70
- cursorLeft: (n = 1) => `${ESC}[${n}D`,
71
- cursorTo: (x, y) => `${ESC}[${y};${x}H`,
72
-
73
- // 24-bit color (truecolor)
74
- rgb: (r, g, b) => `${ESC}[38;2;${r};${g};${b}m`,
75
- bgRgb: (r, g, b) => `${ESC}[48;2;${r};${g};${b}m`,
76
- };
77
-
78
- // ═══════════════════════════════════════════════════════════════════════════════
79
- // COLOR PALETTE - Electric Blue Theme
80
- // ═══════════════════════════════════════════════════════════════════════════════
81
-
82
- const colors = {
83
- // Primary brand colors
84
- primary: ansi.rgb(99, 102, 241), // Indigo
85
- secondary: ansi.rgb(139, 92, 246), // Purple
86
- accent: ansi.rgb(6, 182, 212), // Cyan
87
-
88
- // Semantic colors
89
- success: ansi.rgb(16, 185, 129), // Emerald
90
- warning: ansi.rgb(245, 158, 11), // Amber
91
- error: ansi.rgb(239, 68, 68), // Red
92
- info: ansi.rgb(59, 130, 246), // Blue
93
-
94
- // Severity colors
95
- critical: ansi.rgb(220, 38, 38), // Red-600
96
- high: ansi.rgb(234, 88, 12), // Orange-600
97
- medium: ansi.rgb(202, 138, 4), // Yellow-600
98
- low: ansi.rgb(37, 99, 235), // Blue-600
99
-
100
- // Gradient stops
101
- gradient: {
102
- cyan: ansi.rgb(0, 255, 255),
103
- blue: ansi.rgb(100, 149, 237),
104
- purple: ansi.rgb(138, 43, 226),
105
- pink: ansi.rgb(236, 72, 153),
106
- orange: ansi.rgb(251, 146, 60),
107
- },
108
-
109
- // Background variants
110
- bg: {
111
- success: ansi.bgRgb(16, 185, 129),
112
- warning: ansi.bgRgb(245, 158, 11),
113
- error: ansi.bgRgb(220, 38, 38),
114
- info: ansi.bgRgb(59, 130, 246),
115
- muted: ansi.bgRgb(39, 39, 42),
116
- },
117
46
  };
118
47
 
119
- // ═══════════════════════════════════════════════════════════════════════════════
120
- // 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: '▐',
48
+ // Semantic Palette
49
+ export const style = {
50
+ success: (t: string) => `${ansi.green}${t}${ansi.reset}`,
51
+ error: (t: string) => `${ansi.red}${t}${ansi.reset}`,
52
+ warning: (t: string) => `${ansi.yellow}${t}${ansi.reset}`,
53
+ info: (t: string) => `${ansi.cyan}${t}${ansi.reset}`,
54
+ subtle: (t: string) => `${ansi.gray}${t}${ansi.reset}`,
55
+ highlight: (t: string) => `${ansi.bold}${ansi.white}${t}${ansi.reset}`,
56
+ label: (t: string) => `${ansi.blue}${t}${ansi.reset}`,
57
+ // Backgrounds for badges
58
+ bgSuccess: (t: string) => `${ansi.bgGreen}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
59
+ bgError: (t: string) => `${ansi.bgRed}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
60
+ bgWarn: (t: string) => `${ansi.bgYellow}${ansi.bold}${ansi.white} ${t} ${ansi.reset}`,
162
61
  };
163
62
 
164
- // ═══════════════════════════════════════════════════════════════════════════════
165
- // ICONS & SYMBOLS
166
- // ═══════════════════════════════════════════════════════════════════════════════
167
-
168
- const icons = {
169
- // Status
63
+ export const icons = {
170
64
  success: '✓',
171
65
  error: '✗',
172
66
  warning: '⚠',
173
67
  info: 'ℹ',
174
- question: '?',
175
-
176
- // Arrows
177
- arrowRight: '→',
178
- arrowLeft: '←',
179
- arrowUp: '↑',
180
- arrowDown: '↓',
181
- arrowBoth: '↔',
182
-
183
- // Pointers
184
- pointer: '❯',
185
- pointerSmall: '›',
186
68
  bullet: '•',
187
- dot: '·',
188
-
189
- // Misc
190
- star: '★',
191
- heart: '♥',
192
- lightning: '⚡',
193
- fire: '🔥',
194
- rocket: '🚀',
195
- check: '☑',
69
+ pointer: '',
70
+ arrowRight: '→',
71
+ line: '─',
196
72
  radioOn: '◉',
197
73
  radioOff: '○',
198
-
199
- // Severity badges
200
- critical: '🚨',
201
- high: '🔴',
202
- medium: '🟡',
203
- low: '🔵',
74
+ lock: '🔒',
204
75
  };
205
76
 
206
77
  // ═══════════════════════════════════════════════════════════════════════════════
207
- // SPINNER - Advanced Multi-Phase Spinner
78
+ // 2. LAYOUT PRIMITIVES (The "Grid")
208
79
  // ═══════════════════════════════════════════════════════════════════════════════
209
80
 
210
- const SPINNER_FRAMES = {
211
- dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
212
- line: ['|', '/', '-', '\\'],
213
- arc: ['', '◠', '', '◞', '◡', '◟'],
214
- circle: ['◐', '◓', '◑', '◒'],
215
- bounce: ['', '⠂', '', '⡀', '', '', '', ''],
216
- pulse: ['', '▓', '', '', '', ''],
81
+ export const BOX = {
82
+ // Outer Frame (Double)
83
+ tl: '', tr: '', bl: '', br: '', h: '═', v: '║',
84
+ trT: '', tlT: '',
85
+ // Inner Dividers (Single)
86
+ ltl: '', ltr: '', lbl: '', lbr: '', lh: '', lv: '',
87
+ lt: '', lb: '', lx: '', ltrT: '', ltlT: ''
217
88
  };
218
89
 
219
- 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
- }
90
+ /**
91
+ * Centers text within the standard width, accounting for ANSI codes
92
+ */
93
+ export function padCenter(str: string, width: number = WIDTH - 2): string {
94
+ const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
95
+ const padding = Math.max(0, width - visibleLen);
96
+ const left = Math.floor(padding / 2);
97
+ return ' '.repeat(left) + str + ' '.repeat(padding - left);
334
98
  }
335
99
 
336
- // ═══════════════════════════════════════════════════════════════════════════════
337
- // 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
- }
100
+ /**
101
+ * Pads text to the right, accounting for ANSI codes
102
+ */
103
+ export function padRight(str: string, len: number): string {
104
+ const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
105
+ const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
106
+ const finalLen = truncated.replace(/\u001b\[\d+m/g, '').length;
107
+ return truncated + ' '.repeat(Math.max(0, len - finalLen));
370
108
  }
371
109
 
372
- // ═══════════════════════════════════════════════════════════════════════════════
373
- // 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
- }
110
+ /**
111
+ * Truncates text with ellipsis
112
+ */
113
+ export function truncate(str: string, len: number): string {
114
+ if (!str) return '';
115
+ const clean = str.replace(/\u001b\[\d+m/g, '');
116
+ if (clean.length <= len) return str;
117
+ return clean.substring(0, len - 3) + '...';
510
118
  }
511
119
 
512
120
  // ═══════════════════════════════════════════════════════════════════════════════
513
- // SCORE DISPLAY - Animated Score Card
121
+ // 3. COMPONENT: SPINNER
514
122
  // ═══════════════════════════════════════════════════════════════════════════════
515
123
 
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
- }
124
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
530
125
 
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}`);
126
+ export class Spinner {
127
+ private timer: NodeJS.Timeout | null = null;
128
+ private frameIndex = 0;
575
129
 
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
- // ═══════════════════════════════════════════════════════════════════════════════
130
+ constructor(public text: string = '', private colorStr = ansi.cyan) {}
600
131
 
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
- }
132
+ start(text?: string) {
133
+ if (text) this.text = text;
134
+ process.stdout.write(ansi.hideCursor);
135
+ this.timer = setInterval(() => {
136
+ const frame = SPINNER_FRAMES[this.frameIndex];
137
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
138
+ process.stdout.write(`\r ${this.colorStr}${frame}${ansi.reset} ${this.text}`);
139
+ }, 80);
140
+ return this;
645
141
  }
646
-
647
- return lines.join('\n');
648
- }
649
142
 
650
- 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)}`);
143
+ stop(symbol = icons.success, color = ansi.green, finalMsg?: string) {
144
+ if (this.timer) clearInterval(this.timer);
145
+ process.stdout.write(`\r${ansi.clearLine}`);
146
+ if (finalMsg !== null) { // Pass null to clear completely
147
+ const msg = finalMsg || this.text;
148
+ console.log(` ${color}${symbol}${ansi.reset} ${msg}`);
682
149
  }
683
- lines.push(` ${ansi.dim}└──────────────────────────────────────${ansi.reset}`);
150
+ process.stdout.write(ansi.showCursor);
151
+ return this;
684
152
  }
685
-
686
- lines.push('');
687
- return lines;
688
- }
689
153
 
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');
154
+ succeed(text?: string) { return this.stop(icons.success, ansi.green, text); }
155
+ fail(text?: string) { return this.stop(icons.error, ansi.red, text); }
156
+ warn(text?: string) { return this.stop(icons.warning, ansi.yellow, text); }
724
157
  }
725
158
 
726
159
  // ═══════════════════════════════════════════════════════════════════════════════
727
- // SECTION HEADERS
160
+ // 4. COMPONENT: PROGRESS BAR
728
161
  // ═══════════════════════════════════════════════════════════════════════════════
729
162
 
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}`;
163
+ export function renderProgressBar(percentage: number, width = 20, color = ansi.green): string {
164
+ const filled = Math.round((percentage / 100) * width);
165
+ return `${color}${'█'.repeat(filled)}${ansi.gray}${'░'.repeat(width - filled)}${ansi.reset}`;
736
166
  }
737
167
 
738
168
  // ═══════════════════════════════════════════════════════════════════════════════
739
- // UTILITY FUNCTIONS
169
+ // 5. STANDARD RENDERERS (The "Look")
740
170
  // ═══════════════════════════════════════════════════════════════════════════════
741
171
 
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
- }
172
+ /**
173
+ * Renders the Standard Double-Border Header with Logo
174
+ */
175
+ export function renderScreenHeader(
176
+ logoAscii: string,
177
+ title: string,
178
+ logoColor: (t: string) => string = (t) => t,
179
+ context?: { label: string, value: string }
180
+ ) {
181
+ const lines: string[] = [];
182
+
183
+ // Top Frame
184
+ lines.push(ansi.gray + BOX.tl + BOX.h.repeat(WIDTH - 2) + BOX.tr + ansi.reset);
185
+ lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
186
+
187
+ // Logo Processing
188
+ logoAscii.trim().split('\n').forEach(line => {
189
+ const cleanLine = line.replace(/\r/g, '');
190
+ const maxLen = Math.max(...logoAscii.split('\n').map(l => l.length));
191
+ const padding = ' '.repeat(Math.floor((WIDTH - 2 - maxLen) / 2));
192
+ const rightPad = ' '.repeat(WIDTH - 2 - padding.length - cleanLine.length);
193
+
194
+ lines.push(ansi.gray + BOX.v + ansi.reset + padding + logoColor(cleanLine) + rightPad + ansi.gray + BOX.v + ansi.reset);
195
+ });
753
196
 
754
- function formatNumber(num) {
755
- return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
756
- }
197
+ // Title
198
+ lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
199
+ lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(ansi.bold + ansi.white + title + ansi.reset, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
200
+ lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
201
+
202
+ // Context Bar (Optional)
203
+ if (context) {
204
+ lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
205
+ const ctxStr = `${ansi.gray}${context.label}: ${ansi.reset}${ansi.cyan}${context.value}${ansi.reset}`;
206
+ lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(ctxStr, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
207
+ lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
208
+ } else {
209
+ lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
210
+ }
757
211
 
758
- 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`;
212
+ console.log(lines.join('\n'));
762
213
  }
763
214
 
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];
215
+ /**
216
+ * Renders the Standard Footer
217
+ */
218
+ export function renderScreenFooter() {
219
+ console.log(ansi.gray + BOX.bl + BOX.h.repeat(WIDTH - 2) + BOX.br + ansi.reset);
770
220
  }
771
221
 
772
- // ═══════════════════════════════════════════════════════════════════════════════
773
- // 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}`);
222
+ /**
223
+ * Renders a Standard Verdict/Telemetry Table
224
+ */
225
+ export function renderVerdictTable(
226
+ stats: { duration: number, heap: number, files?: number },
227
+ score: number,
228
+ findings: any[]
229
+ ) {
230
+ const lines: string[] = [];
231
+
232
+ // Telemetry Row
233
+ const heapMB = Math.round(stats.heap / 1024 / 1024);
234
+ const statsStr = `📡 TELEMETRY │ ⏱ ${stats.duration}ms │ 📂 ${stats.files || '?'} Files │ 📦 ${heapMB}MB`;
235
+ lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(statsStr, WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
236
+ lines.push(ansi.gray + BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT + ansi.reset);
237
+
238
+ // Score Row
239
+ lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
240
+ const bar = renderProgressBar(score, 20);
241
+ lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(`HEALTH SCORE [${bar}] ${score} / 100`, WIDTH + 18) + ansi.gray + BOX.v + ansi.reset);
242
+ lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
243
+
244
+ if (findings.length > 0) {
245
+ // Table Header
246
+ const C1=10, C2=13, C3=41;
247
+ const tTop = ` ${BOX.ltl}${BOX.lh.repeat(C1)}${BOX.lt}${BOX.lh.repeat(C2)}${BOX.lt}${BOX.lh.repeat(C3)}${BOX.ltr} `;
248
+ const header = ` ${BOX.lv}${padRight(' SEVERITY', C1)}${BOX.lv}${padRight(' TYPE', C2)}${BOX.lv}${padRight(' FINDING', C3)}${BOX.lv} `;
249
+ const tBot = ` ${BOX.lbl}${BOX.lh.repeat(C1)}${BOX.lb}${BOX.lh.repeat(C2)}${BOX.lb}${BOX.lh.repeat(C3)}${BOX.lbr} `;
250
+
251
+ lines.push(ansi.gray + BOX.v + ansi.reset + ansi.gray + tTop + ansi.reset + ansi.gray + BOX.v + ansi.reset);
252
+ lines.push(ansi.gray + BOX.v + ansi.reset + ansi.bold + header + ansi.reset + ansi.gray + BOX.v + ansi.reset);
253
+
254
+ // Rows
255
+ findings.slice(0, 5).forEach(f => {
256
+ let sev = style.subtle(' INFO ');
257
+ if (f.severity === 'critical' || f.severity === 'BLOCK') sev = style.error('🛑 CRIT ');
258
+ else if (f.severity === 'high' || f.severity === 'WARN') sev = style.warning('🟡 WARN ');
259
+
260
+ const row = ` ${ansi.gray}${BOX.lv}${ansi.reset}${sev}${ansi.gray}${BOX.lv}${ansi.reset}${padRight(' '+(f.category||'General'), C2)}${ansi.gray}${BOX.lv}${ansi.reset}${padRight(' '+(f.message||f.title), C3)}${ansi.gray}${BOX.lv}${ansi.reset} `;
261
+ lines.push(ansi.gray + BOX.v + ansi.reset + row + ansi.gray + BOX.v + ansi.reset);
262
+ });
263
+
264
+ lines.push(ansi.gray + BOX.v + ansi.reset + ansi.gray + tBot + ansi.reset + ansi.gray + BOX.v + ansi.reset);
265
+ } else {
266
+ lines.push(ansi.gray + BOX.v + ansi.reset + padCenter(style.success('✅ NO ISSUES FOUND'), WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
814
267
  }
815
-
816
- lines.push('');
817
- return lines.join('\n');
818
- }
819
268
 
820
- // ═══════════════════════════════════════════════════════════════════════════════
821
- // 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
- };
269
+ lines.push(ansi.gray + BOX.v + ansi.reset + ' '.repeat(WIDTH - 2) + ansi.gray + BOX.v + ansi.reset);
270
+ console.log(lines.join('\n'));
271
+ }