@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,868 +1,190 @@
1
1
  /**
2
- * Scan Output - World-Class Premium Terminal Display
3
- *
2
+ * Enterprise Scan Output - Premium Format
4
3
  * Features:
5
- * - Beautiful ASCII tables with box drawing
6
- * - Color-coded severity with gradients
7
- * - Progress bars and sparklines
8
- * - Category breakdown with visual hierarchy
9
- * - JSON/SARIF export
4
+ * - "SCAN" ASCII Art
5
+ * - Fixed alignment tables
6
+ * - "Redacted" Upsell section for Scan context
10
7
  */
11
8
 
12
- const {
13
- ansi,
14
- colors,
15
- box,
16
- icons,
17
- renderScoreCard,
18
- renderFindingsList,
19
- renderSection,
20
- renderDivider,
21
- renderTable,
22
- formatDuration,
23
- formatNumber,
24
- truncate,
25
- } = require('./terminal-ui');
9
+ const chalk = require('chalk');
26
10
 
27
11
  // ═══════════════════════════════════════════════════════════════════════════════
28
- // ENHANCED COLOR PALETTE
12
+ // CONFIGURATION
29
13
  // ═══════════════════════════════════════════════════════════════════════════════
30
14
 
31
- const palette = {
32
- // Severity gradient
33
- critical: ansi.rgb(255, 50, 50),
34
- criticalBg: ansi.bgRgb(180, 20, 20),
35
- high: ansi.rgb(255, 120, 50),
36
- highBg: ansi.bgRgb(180, 80, 20),
37
- medium: ansi.rgb(255, 200, 50),
38
- mediumBg: ansi.bgRgb(150, 120, 20),
39
- low: ansi.rgb(100, 180, 255),
40
- lowBg: ansi.bgRgb(40, 80, 150),
41
-
42
- // Status colors
43
- pass: ansi.rgb(50, 255, 120),
44
- passBg: ansi.bgRgb(20, 120, 50),
45
- warn: ansi.rgb(255, 200, 50),
46
- warnBg: ansi.bgRgb(150, 120, 20),
47
- fail: ansi.rgb(255, 50, 50),
48
- failBg: ansi.bgRgb(180, 20, 20),
49
-
50
- // UI elements
51
- border: ansi.rgb(70, 70, 90),
52
- borderBright: ansi.rgb(100, 100, 130),
53
- header: ansi.rgb(150, 150, 200),
54
- muted: ansi.rgb(100, 100, 120),
55
- accent: ansi.rgb(100, 200, 255),
56
- highlight: ansi.rgb(255, 220, 100),
57
-
58
- // Category colors (rainbow)
59
- cat1: ansi.rgb(255, 100, 100),
60
- cat2: ansi.rgb(255, 150, 100),
61
- cat3: ansi.rgb(255, 200, 100),
62
- cat4: ansi.rgb(200, 255, 100),
63
- cat5: ansi.rgb(100, 255, 150),
64
- cat6: ansi.rgb(100, 255, 200),
65
- cat7: ansi.rgb(100, 200, 255),
66
- cat8: ansi.rgb(100, 150, 255),
67
- cat9: ansi.rgb(150, 100, 255),
68
- cat10: ansi.rgb(200, 100, 255),
69
- cat11: ansi.rgb(255, 100, 200),
70
- cat12: ansi.rgb(255, 100, 150),
71
- };
72
-
73
- // ═══════════════════════════════════════════════════════════════════════════════
74
- // BOX DRAWING HELPERS
75
- // ═══════════════════════════════════════════════════════════════════════════════
15
+ const WIDTH = 76;
76
16
 
77
- const B = {
78
- // Heavy box
79
- tl: '', tr: '', bl: '┗', br: '┛',
80
- h: '', v: '',
81
- lT: '┣', rT: '┫', tT: '┳', bT: '┻', x: '╋',
17
+ const BOX = {
18
+ topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
19
+ horizontal: '', vertical: '',
20
+ teeRight: '', teeLeft: '',
82
21
 
83
- // Light box
84
- ltl: '┌', ltr: '┐', lbl: '└', lbr: '┘',
85
- lh: '─', lv: '│',
86
-
87
- // Double box
88
- dtl: '╔', dtr: '╗', dbl: '╚', dbr: '╝',
89
- dh: '═', dv: '║',
90
-
91
- // Rounded
92
- rtl: '╭', rtr: '╮', rbl: '╰', rbr: '╯',
93
-
94
- // Blocks
95
- full: '█', light: '░', medium: '▒', dark: '▓',
96
- left: '▌', right: '▐', top: '▀', bottom: '▄',
97
-
98
- // Progress
99
- pb: ['░', '▒', '▓', '█'],
100
- spark: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'],
22
+ // Table Borders (Light)
23
+ tTopLeft: '┌', tTopRight: '┐', tBottomLeft: '└', tBottomRight: '┘',
24
+ tHorizontal: '─', tVertical: '│',
25
+ tTeeTop: '┬', tTeeBottom: '┴', tTee: '┼', tTeeLeft: '├', tTeeRight: '┤'
101
26
  };
102
27
 
103
- function repeatChar(char, n) {
104
- return char.repeat(Math.max(0, n));
105
- }
28
+ const LOGO_SCAN = `
29
+ █████████ █████████ █████████ █████████
30
+ ███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███
31
+ ░███ ░░░ ░███ ░░░ ░███ ░███ ░███ ░███
32
+ ░░█████████ ░███ ░███████████ ░███ ░███
33
+ ░░░░░░░░███░███ ░███░░░░░███ ░███ ░███
34
+ ███ ░███░███ ███ ░███ ░███ ░███ ░███
35
+ ░░█████████ ░░█████████ ░███ ░███ ░███ ░███
36
+ ░░░░░░░░░ ░░░░░░░░░ ░░░░ ░░░░ ░░░░ ░░░░
37
+ `;
106
38
 
107
- function padCenter(str, width) {
108
- const len = stripAnsi(str).length;
109
- const left = Math.floor((width - len) / 2);
110
- const right = width - len - left;
111
- return ' '.repeat(left) + str + ' '.repeat(right);
112
- }
113
-
114
- function stripAnsi(str) {
115
- return str.replace(/\x1b\[[0-9;]*m/g, '');
116
- }
39
+ // Column widths for the table (Must correspond to math below)
40
+ const COL_1 = 10; // Severity
41
+ const COL_2 = 13; // Component
42
+ const COL_3 = 41; // Message
117
43
 
118
44
  // ═══════════════════════════════════════════════════════════════════════════════
119
- // PREMIUM SCORE DISPLAY
45
+ // UTILITIES
120
46
  // ═══════════════════════════════════════════════════════════════════════════════
121
47
 
122
- function renderPremiumScoreCard(score, options = {}) {
123
- const { verdict = 'BLOCK', findings = {}, duration, cached } = options;
124
- const lines = [];
125
-
126
- const width = 70;
127
- const innerWidth = width - 4;
128
-
129
- // Determine colors based on verdict/score
130
- let verdictColor, verdictBg, gradientColors;
131
- if (verdict === 'SHIP' || verdict === 'PASS' || score >= 80) {
132
- verdictColor = palette.pass;
133
- verdictBg = palette.passBg;
134
- gradientColors = [ansi.rgb(50, 200, 100), ansi.rgb(100, 255, 150), ansi.rgb(150, 255, 200)];
135
- } else if (verdict === 'WARN' || score >= 50) {
136
- verdictColor = palette.warn;
137
- verdictBg = palette.warnBg;
138
- gradientColors = [ansi.rgb(200, 150, 50), ansi.rgb(255, 200, 100), ansi.rgb(255, 220, 150)];
139
- } else {
140
- verdictColor = palette.fail;
141
- verdictBg = palette.failBg;
142
- gradientColors = [ansi.rgb(200, 50, 50), ansi.rgb(255, 100, 100), ansi.rgb(255, 150, 150)];
143
- }
144
-
145
- // Top border with gradient
146
- lines.push('');
147
- lines.push(` ${palette.border}${B.dtl}${B.dh.repeat(width - 2)}${B.dtr}${ansi.reset}`);
148
-
149
- // Score display
150
- const scoreStr = String(score).padStart(3);
151
- const gradeStr = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
152
- const gradeColor = score >= 80 ? palette.pass : score >= 60 ? palette.warn : palette.fail;
153
-
154
- lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
155
-
156
- // Big score
157
- const scoreLine = `${palette.muted}SCORE${ansi.reset} ${verdictColor}${ansi.bold}${scoreStr}${ansi.reset}${palette.muted}/100${ansi.reset} ${palette.muted}GRADE${ansi.reset} ${gradeColor}${ansi.bold}${gradeStr}${ansi.reset}`;
158
- lines.push(` ${palette.border}${B.dv}${ansi.reset} ${scoreLine}${' '.repeat(14)}${palette.border}${B.dv}${ansi.reset}`);
159
-
160
- lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
161
-
162
- // Progress bar
163
- const barWidth = 40;
164
- const filled = Math.round((score / 100) * barWidth);
165
- let progressBar = '';
166
- for (let i = 0; i < barWidth; i++) {
167
- if (i < filled) {
168
- // Gradient fill
169
- const gradientIdx = Math.floor((i / filled) * gradientColors.length);
170
- progressBar += `${gradientColors[Math.min(gradientIdx, gradientColors.length - 1)]}${B.full}${ansi.reset}`;
171
- } else {
172
- progressBar += `${palette.muted}${B.light}${ansi.reset}`;
173
- }
174
- }
175
- lines.push(` ${palette.border}${B.dv}${ansi.reset} ${progressBar}${' '.repeat(innerWidth - barWidth - 4)}${palette.border}${B.dv}${ansi.reset}`);
176
-
177
- lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
178
-
179
- // Verdict badge
180
- const verdictText = verdict === 'SHIP' ? ' ✓ SHIP ' : verdict === 'WARN' ? ' ⚠ WARN ' : ' ✗ BLOCK ';
181
- const verdictDesc = verdict === 'SHIP' ? 'Ready to ship!' : verdict === 'WARN' ? 'Review before shipping' : 'Fix issues before shipping';
182
- const badgeLine = `${verdictBg}${ansi.bold}${verdictText}${ansi.reset} ${palette.muted}${verdictDesc}${ansi.reset}`;
183
- lines.push(` ${palette.border}${B.dv}${ansi.reset} ${badgeLine} ${palette.border}${B.dv}${ansi.reset}`);
184
-
185
- lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
186
-
187
- // Findings summary
188
- const { critical = 0, high = 0, medium = 0, low = 0 } = findings;
189
- const critStr = `${palette.critical}${critical}${ansi.reset} critical`;
190
- const highStr = `${palette.high}${high}${ansi.reset} high`;
191
- const medStr = `${palette.medium}${medium}${ansi.reset} medium`;
192
- const lowStr = `${palette.low}${low}${ansi.reset} low`;
193
- const findingsLine = ` ${critStr} ${palette.muted}│${ansi.reset} ${highStr} ${palette.muted}│${ansi.reset} ${medStr} ${palette.muted}│${ansi.reset} ${lowStr}`;
194
- lines.push(` ${palette.border}${B.dv}${ansi.reset}${findingsLine}${' '.repeat(9)}${palette.border}${B.dv}${ansi.reset}`);
195
-
196
- lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
197
-
198
- // Duration
199
- if (duration) {
200
- const durationStr = `${palette.muted}Completed in ${formatDuration(duration)}${cached ? ' (cached)' : ''}${ansi.reset}`;
201
- lines.push(` ${palette.border}${B.dv}${ansi.reset} ${durationStr}${' '.repeat(innerWidth - stripAnsi(durationStr).length - 4)}${palette.border}${B.dv}${ansi.reset}`);
202
- lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
203
- }
204
-
205
- // Bottom border
206
- lines.push(` ${palette.border}${B.dbl}${B.dh.repeat(width - 2)}${B.dbr}${ansi.reset}`);
207
-
208
- return lines.join('\n');
48
+ function padCenter(str, width) {
49
+ const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
50
+ const padding = Math.max(0, width - visibleLen);
51
+ const left = Math.floor(padding / 2);
52
+ const right = padding - left;
53
+ return ' '.repeat(left) + str + ' '.repeat(right);
209
54
  }
210
55
 
211
- // ═══════════════════════════════════════════════════════════════════════════════
212
- // DIFF SUMMARY (NEW / FIXED / PERSISTING)
213
- // ═══════════════════════════════════════════════════════════════════════════════
214
-
215
- function renderDiffSummary(diff) {
216
- if (!diff) return '';
217
-
218
- const lines = [];
219
- const width = 70;
220
-
221
- lines.push('');
222
- lines.push(` ${palette.header}${ansi.bold}📊 CHANGES SINCE LAST SCAN${ansi.reset}`);
223
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
224
- lines.push('');
225
-
226
- const parts = [];
227
-
228
- // NEW findings (bad - things got worse)
229
- if (diff.summary.newCount > 0) {
230
- parts.push(` ${ansi.bgRgb(180, 50, 50)}${ansi.bold} +${diff.summary.newCount} NEW ${ansi.reset}`);
231
- lines.push(` ${palette.critical}▲${ansi.reset} ${ansi.bold}${diff.summary.newCount}${ansi.reset} ${palette.critical}new issues${ansi.reset} introduced`);
232
- }
233
-
234
- // FIXED findings (good - things got better)
235
- if (diff.summary.fixedCount > 0) {
236
- parts.push(` ${ansi.bgRgb(50, 150, 80)}${ansi.bold} -${diff.summary.fixedCount} FIXED ${ansi.reset}`);
237
- lines.push(` ${palette.pass}▼${ansi.reset} ${ansi.bold}${diff.summary.fixedCount}${ansi.reset} ${palette.pass}issues fixed${ansi.reset} since last scan`);
238
- }
239
-
240
- // PERSISTING (neutral - still there)
241
- if (diff.summary.persistingCount > 0) {
242
- lines.push(` ${palette.muted}●${ansi.reset} ${ansi.bold}${diff.summary.persistingCount}${ansi.reset} ${palette.muted}persisting issues${ansi.reset}`);
243
- }
244
-
245
- // Show top new issues
246
- if (diff.new && diff.new.length > 0) {
247
- lines.push('');
248
- lines.push(` ${palette.critical}${ansi.bold}New issues:${ansi.reset}`);
249
- for (const finding of diff.new.slice(0, 3)) {
250
- const file = finding.file || finding.evidence?.[0]?.file || '';
251
- const shortFile = file.split('/').slice(-2).join('/');
252
- lines.push(` ${palette.critical}+${ansi.reset} ${finding.category}: ${palette.muted}${shortFile}${ansi.reset}`);
253
- }
254
- if (diff.new.length > 3) {
255
- lines.push(` ${palette.muted}... and ${diff.new.length - 3} more${ansi.reset}`);
256
- }
257
- }
258
-
259
- // Show top fixed issues
260
- if (diff.fixed && diff.fixed.length > 0) {
261
- lines.push('');
262
- lines.push(` ${palette.pass}${ansi.bold}Fixed issues:${ansi.reset}`);
263
- for (const finding of diff.fixed.slice(0, 3)) {
264
- const file = finding.file || '';
265
- const shortFile = file.split('/').slice(-2).join('/');
266
- lines.push(` ${palette.pass}-${ansi.reset} ${finding.category}: ${palette.muted}${shortFile}${ansi.reset}`);
267
- }
268
- if (diff.fixed.length > 3) {
269
- lines.push(` ${palette.muted}... and ${diff.fixed.length - 3} more${ansi.reset}`);
270
- }
271
- }
272
-
273
- lines.push('');
274
-
275
- return lines.join('\n');
56
+ function padRight(str, len) {
57
+ const visibleLen = str.length; // Simplified for this usage
58
+ const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
59
+ return truncated + ' '.repeat(Math.max(0, len - truncated.length));
276
60
  }
277
61
 
278
- // ═══════════════════════════════════════════════════════════════════════════════
279
- // BLOCKERS TABLE
280
- // ═══════════════════════════════════════════════════════════════════════════════
281
-
282
- function renderBlockersTable(blockers, options = {}) {
283
- const { maxItems = 10, showStatus = false } = options;
284
-
285
- if (!blockers || blockers.length === 0) {
286
- const lines = [];
287
- lines.push('');
288
- lines.push(` ${palette.pass}${ansi.bold}✓ NO BLOCKERS${ansi.reset} ${palette.muted}— Your code is ready to ship!${ansi.reset}`);
289
- return lines.join('\n');
290
- }
291
-
292
- const lines = [];
293
- const width = 70;
294
-
295
- // Count new blockers for header
296
- const newCount = blockers.filter(b => b.status === 'NEW').length;
297
- const headerExtra = newCount > 0 ? ` ${ansi.bgRgb(180, 50, 50)}${ansi.bold} +${newCount} NEW ${ansi.reset}` : '';
298
-
299
- lines.push('');
300
- lines.push(` ${palette.critical}${ansi.bold}🚨 SHIP BLOCKERS (${blockers.length})${ansi.reset}${headerExtra}`);
301
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
302
- lines.push('');
303
-
304
- for (let i = 0; i < Math.min(blockers.length, maxItems); i++) {
305
- const blocker = blockers[i];
306
- const sevColor = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
307
- ? palette.criticalBg
308
- : palette.highBg;
309
- const sevLabel = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
310
- ? ' BLOCK '
311
- : ' HIGH ';
312
-
313
- // Row number
314
- const num = `${palette.muted}${String(i + 1).padStart(2)}.${ansi.reset}`;
315
-
316
- // Severity badge
317
- const badge = `${sevColor}${ansi.bold}${sevLabel}${ansi.reset}`;
318
-
319
- // Status badge (NEW/PERSISTING)
320
- let statusBadge = '';
321
- if (showStatus && blocker.status === 'NEW') {
322
- statusBadge = ` ${ansi.bgRgb(180, 50, 50)}${ansi.bold} NEW ${ansi.reset}`;
323
- }
324
-
325
- // Title (truncated)
326
- const title = truncate(blocker.title || blocker.message || 'Unknown issue', 42);
327
-
328
- lines.push(` ${num} ${badge}${statusBadge} ${ansi.bold}${title}${ansi.reset}`);
329
-
330
- // File location
331
- if (blocker.file || blocker.evidence?.[0]?.file) {
332
- const file = blocker.file || blocker.evidence?.[0]?.file;
333
- const line = blocker.line || blocker.evidence?.[0]?.lines;
334
- const fileDisplay = file + (line ? `:${line}` : '');
335
- lines.push(` ${palette.muted}└─${ansi.reset} ${palette.accent}${truncate(fileDisplay, 55)}${ansi.reset}`);
336
- }
337
-
338
- // Evidence/why
339
- if (blocker.description || blocker.why) {
340
- const desc = truncate(blocker.description || blocker.why, 55);
341
- lines.push(` ${palette.muted}${desc}${ansi.reset}`);
342
- }
343
-
344
- lines.push('');
345
- }
62
+ function padLogoBlock(ascii, width) {
63
+ const lines = ascii.trim().split('\n');
64
+ const maxContentWidth = Math.max(...lines.map(l => l.length));
346
65
 
347
- if (blockers.length > maxItems) {
348
- lines.push(` ${palette.muted}... and ${blockers.length - maxItems} more blockers${ansi.reset}`);
349
- lines.push('');
350
- }
351
-
352
- return lines.join('\n');
66
+ return lines.map(line => {
67
+ const solidLine = line + ' '.repeat(maxContentWidth - line.length);
68
+ return padCenter(solidLine, width);
69
+ });
353
70
  }
354
71
 
355
- // ═══════════════════════════════════════════════════════════════════════════════
356
- // CATEGORY TABLE WITH VISUAL BARS
357
- // ═══════════════════════════════════════════════════════════════════════════════
358
-
359
- function renderCategoryTable(findings) {
360
- if (!findings || findings.length === 0) return '';
361
-
362
- // Group by category
363
- const categories = {};
364
- let maxTotal = 0;
365
-
366
- for (const f of findings) {
367
- const cat = f.category || f.ruleId?.split('/')[0] || 'Other';
368
- if (!categories[cat]) {
369
- categories[cat] = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
370
- }
371
- categories[cat].total++;
372
- maxTotal = Math.max(maxTotal, categories[cat].total);
373
-
374
- const sev = (f.severity || '').toLowerCase();
375
- if (sev === 'critical' || sev === 'block') categories[cat].critical++;
376
- else if (sev === 'high') categories[cat].high++;
377
- else if (sev === 'medium' || sev === 'warn' || sev === 'warning') categories[cat].medium++;
378
- else categories[cat].low++;
379
- }
380
-
381
- const categoryIcons = {
382
- ENVCONTRACT: '📋', MISSINGROUTE: '🔗', HARDCODEDSECRET: '🔑', TODOFIXME: '📝',
383
- FAKESUCCESS: '✨', GHOSTAUTH: '👻', MOCKDATA: '🎭', CONSOLELOG: '🖥️',
384
- DEADCODE: '💀', DEPRECATEDAPI: '⚠️', EMPTYCATCH: '🕳️', UNSAFEREGEX: '⚡',
385
- SECURITY: '🛡️', BILLING: '💳', ROUTE: '🔗', AUTH: '🔐', SECRET: '🔑',
386
- };
387
-
388
- const categoryColors = [
389
- palette.cat1, palette.cat2, palette.cat3, palette.cat4, palette.cat5,
390
- palette.cat6, palette.cat7, palette.cat8, palette.cat9, palette.cat10,
391
- palette.cat11, palette.cat12,
392
- ];
393
-
394
- const lines = [];
395
- const width = 70;
396
-
397
- lines.push('');
398
- lines.push(` ${palette.header}${ansi.bold}📊 FINDINGS BY CATEGORY${ansi.reset}`);
399
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
400
- lines.push('');
401
-
402
- // Table header
403
- lines.push(` ${palette.muted} CATEGORY COUNT SEVERITY BREAKDOWN BAR${ansi.reset}`);
404
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
405
-
406
- // Sort by total count descending
407
- const sortedCategories = Object.entries(categories)
408
- .sort((a, b) => {
409
- // Sort by criticality first, then by total
410
- const aCrit = a[1].critical * 1000 + a[1].high * 100 + a[1].total;
411
- const bCrit = b[1].critical * 1000 + b[1].high * 100 + b[1].total;
412
- return bCrit - aCrit;
413
- });
414
-
415
- let colorIdx = 0;
416
- for (const [cat, counts] of sortedCategories) {
417
- const icon = categoryIcons[cat.toUpperCase()] || '•';
418
- const catColor = categoryColors[colorIdx % categoryColors.length];
419
- colorIdx++;
420
-
421
- // Category name
422
- const catName = truncate(cat, 15).padEnd(15);
423
-
424
- // Count
425
- const countStr = String(counts.total).padStart(4);
426
-
427
- // Severity breakdown
428
- const critStr = counts.critical > 0 ? `${palette.critical}${counts.critical}C${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
429
- const highStr = counts.high > 0 ? `${palette.high}${counts.high}H${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
430
- const medStr = counts.medium > 0 ? `${palette.medium}${counts.medium}M${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
431
- const lowStr = counts.low > 0 ? `${palette.low}${counts.low}L${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
432
-
433
- // Visual bar
434
- const barWidth = 15;
435
- const barFill = Math.round((counts.total / maxTotal) * barWidth);
436
- let bar = '';
437
- for (let i = 0; i < barWidth; i++) {
438
- if (i < barFill) {
439
- bar += `${catColor}${B.full}${ansi.reset}`;
440
- } else {
441
- bar += `${palette.muted}${B.light}${ansi.reset}`;
442
- }
443
- }
444
-
445
- lines.push(` ${icon} ${catColor}${catName}${ansi.reset} ${ansi.bold}${countStr}${ansi.reset} ${critStr} ${highStr} ${medStr} ${lowStr} ${bar}`);
446
- }
447
-
448
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
449
-
450
- // Total
451
- const totalCount = findings.length;
452
- const totalCrit = findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length;
453
- const totalHigh = findings.filter(f => f.severity === 'high').length;
454
- const totalMed = findings.filter(f => f.severity === 'medium' || f.severity === 'WARN' || f.severity === 'warning').length;
455
- const totalLow = findings.filter(f => f.severity === 'low' || f.severity === 'INFO').length;
456
-
457
- lines.push(` ${palette.muted} TOTAL${ansi.reset} ${ansi.bold}${String(totalCount).padStart(4)}${ansi.reset} ${palette.critical}${totalCrit}C${ansi.reset} ${palette.high}${totalHigh}H${ansi.reset} ${palette.medium}${totalMed}M${ansi.reset} ${palette.low}${totalLow}L${ansi.reset}`);
458
-
459
- return lines.join('\n');
460
- }
461
-
462
- // ═══════════════════════════════════════════════════════════════════════════════
463
- // TOP FINDINGS DETAILS
464
- // ═══════════════════════════════════════════════════════════════════════════════
465
-
466
- function renderTopFindings(findings, options = {}) {
467
- const { maxItems = 5, showFix = true } = options;
468
-
469
- if (!findings || findings.length === 0) return '';
470
-
471
- // Get top critical/high findings
472
- const topFindings = findings
473
- .filter(f => f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high')
474
- .slice(0, maxItems);
475
-
476
- if (topFindings.length === 0) return '';
477
-
478
- const lines = [];
479
- const width = 70;
480
-
481
- lines.push('');
482
- lines.push(` ${palette.header}${ansi.bold}🔍 TOP ISSUES TO FIX${ansi.reset}`);
483
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
484
-
485
- for (let i = 0; i < topFindings.length; i++) {
486
- const f = topFindings[i];
487
- const num = `${palette.accent}${i + 1}${ansi.reset}`;
488
- const icon = f.severity === 'critical' || f.severity === 'BLOCK' ? '🔴' : '🟠';
489
-
490
- lines.push('');
491
- lines.push(` ${num}. ${icon} ${ansi.bold}${truncate(f.title || f.message, 55)}${ansi.reset}`);
492
-
493
- if (f.file) {
494
- lines.push(` ${palette.accent}${f.file}${f.line ? `:${f.line}` : ''}${ansi.reset}`);
495
- }
496
-
497
- if (f.description || f.why) {
498
- lines.push(` ${palette.muted}${truncate(f.description || f.why, 60)}${ansi.reset}`);
499
- }
500
-
501
- if (showFix && (f.fix || f.fixHints?.[0])) {
502
- const fixHint = f.fix || f.fixHints?.[0];
503
- lines.push(` ${palette.pass}→ ${truncate(fixHint, 58)}${ansi.reset}`);
504
- }
505
- }
506
-
507
- return lines.join('\n');
72
+ function renderProgressBar(score, width = 20) {
73
+ const filled = Math.round((score / 100) * width);
74
+ const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
75
+ return bar;
508
76
  }
509
77
 
510
78
  // ═══════════════════════════════════════════════════════════════════════════════
511
- // ANALYSIS LAYERS STATUS
79
+ // MAIN FORMATTER
512
80
  // ═══════════════════════════════════════════════════════════════════════════════
513
81
 
514
- function renderLayers(layers) {
515
- if (!layers || layers.length === 0) return '';
516
-
517
- const lines = [];
518
- const width = 70;
519
-
520
- lines.push('');
521
- lines.push(` ${palette.header}${ansi.bold}⚡ ANALYSIS LAYERS${ansi.reset}`);
522
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
523
- lines.push('');
524
-
525
- const layerConfig = {
526
- ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code parsing' },
527
- truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
528
- reality: { name: 'Reality Check', icon: '🎭', desc: 'Runtime testing' },
529
- realitySniff: { name: 'Reality Sniff', icon: '🔬', desc: 'AI artifact detection' },
530
- detection: { name: 'Detection', icon: '🛡️', desc: 'Security patterns' },
531
- codeQuality: { name: 'Code Quality', icon: '📋', desc: 'TODO, MOCK, console.log' },
532
- };
533
-
534
- for (const layer of layers) {
535
- const config = layerConfig[layer.name] || { name: layer.name, icon: '○', desc: '' };
536
-
537
- let status, statusIcon;
538
- if (layer.skipped) {
539
- status = `${palette.muted}skipped${ansi.reset}`;
540
- statusIcon = `${palette.muted}○${ansi.reset}`;
541
- } else if (layer.error) {
542
- status = `${palette.fail}error${ansi.reset}`;
543
- statusIcon = `${palette.fail}✗${ansi.reset}`;
544
- } else {
545
- status = `${palette.pass}done${ansi.reset}`;
546
- statusIcon = `${palette.pass}✓${ansi.reset}`;
547
- }
548
-
549
- const duration = layer.duration ? `${palette.muted}${layer.duration}ms${ansi.reset}` : '';
550
- const findingsCount = layer.findings !== undefined ? `${palette.accent}${layer.findings}${ansi.reset} findings` : '';
551
-
552
- lines.push(` ${statusIcon} ${config.icon} ${config.name.padEnd(18)} ${status.padEnd(20)} ${duration.padEnd(12)} ${findingsCount}`);
553
- }
554
-
555
- return lines.join('\n');
556
- }
557
-
558
- // ═══════════════════════════════════════════════════════════════════════════════
559
- // COVERAGE MAP
560
- // ═══════════════════════════════════════════════════════════════════════════════
82
+ function formatScanOutput(result) {
83
+ const {
84
+ score = 0,
85
+ findings = [],
86
+ duration = 0,
87
+ scannedFiles = 0
88
+ } = result;
561
89
 
562
- function renderCoverageMap(coverage) {
563
- if (!coverage) return '';
564
-
90
+ const hasIssues = findings.length > 0;
91
+ const heapMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
565
92
  const lines = [];
566
- const width = 70;
567
-
568
- lines.push('');
569
- lines.push(` ${palette.header}${ansi.bold}🗺️ ROUTE COVERAGE${ansi.reset}`);
570
- lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
571
- lines.push('');
572
-
573
- const pct = coverage.coveragePercent || 0;
574
- const color = pct >= 80 ? palette.pass : pct >= 60 ? palette.warn : palette.fail;
575
-
576
- // Coverage percentage with big number
577
- lines.push(` ${color}${ansi.bold}${pct}%${ansi.reset} ${palette.muted}of routes reachable from root${ansi.reset}`);
578
-
579
- // Progress bar
580
- const barWidth = 50;
581
- const filled = Math.round((pct / 100) * barWidth);
582
- let bar = '';
583
- for (let i = 0; i < barWidth; i++) {
584
- if (i < filled) {
585
- bar += `${color}${B.full}${ansi.reset}`;
586
- } else {
587
- bar += `${palette.muted}${B.light}${ansi.reset}`;
588
- }
589
- }
590
- lines.push(` ${bar}`);
591
- lines.push('');
592
-
593
- // Stats grid
594
- const stats = [
595
- ['Total Routes', coverage.totalRoutes || 0, palette.accent],
596
- ['Reachable', coverage.reachableFromRoot || 0, palette.pass],
597
- ['Orphaned', coverage.orphanedRoutes || 0, coverage.orphanedRoutes > 0 ? palette.warn : palette.pass],
598
- ['Dead Links', coverage.deadLinks || 0, coverage.deadLinks > 0 ? palette.fail : palette.pass],
599
- ];
600
-
601
- let statsLine = ' ';
602
- for (const [label, value, col] of stats) {
603
- statsLine += `${palette.muted}${label}:${ansi.reset} ${col}${ansi.bold}${value}${ansi.reset} `;
604
- }
605
- lines.push(statsLine);
606
-
607
- return lines.join('\n');
608
- }
609
-
610
- // ═══════════════════════════════════════════════════════════════════════════════
611
- // UPSELL BANNER
612
- // ═══════════════════════════════════════════════════════════════════════════════
613
93
 
614
- function renderUpsellBanner(severityCounts) {
615
- const { critical = 0, high = 0, medium = 0, low = 0 } = severityCounts;
616
- const total = critical + high + medium + low;
617
-
618
- if (total === 0) return '';
619
-
620
- const lines = [];
621
- const width = 68;
622
-
623
- lines.push('');
624
- lines.push(` ${palette.border}${B.rtl}${B.lh.repeat(width)}${B.rtr}${ansi.reset}`);
625
- lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
626
-
627
- if (critical > 0) {
628
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.fail}⚠ ${critical} critical issues${ansi.reset} blocking your ship!${' '.repeat(width - 42)}${palette.border}${B.lv}${ansi.reset}`);
629
- lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
630
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck fix${ansi.reset} ${palette.muted}→ AI-generated fixes for each issue${ansi.reset}${' '.repeat(width - 52)}${palette.border}${B.lv}${ansi.reset}`);
631
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.muted}Requires STARTER vibecheck.dev/pricing${ansi.reset}${' '.repeat(width - 43)}${palette.border}${B.lv}${ansi.reset}`);
632
- } else if (medium > 0) {
633
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.warn}⚡ ${medium} warnings${ansi.reset} detected in your codebase${' '.repeat(width - 46)}${palette.border}${B.lv}${ansi.reset}`);
634
- lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
635
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck ship${ansi.reset} ${palette.muted}→ Full verdict with AI risk score${ansi.reset}${' '.repeat(width - 51)}${palette.border}${B.lv}${ansi.reset}`);
636
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck fix${ansi.reset} ${palette.muted}→ AI fixes • Requires STARTER${ansi.reset}${' '.repeat(width - 48)}${palette.border}${B.lv}${ansi.reset}`);
94
+ // 1. OUTER FRAME TOP
95
+ lines.push(chalk.gray(BOX.topLeft + BOX.horizontal.repeat(WIDTH - 2) + BOX.topRight));
96
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
97
+
98
+ // 2. LOGO (Cyan if clean, Red if issues)
99
+ const logoColor = hasIssues ? chalk.red : chalk.cyan;
100
+ padLogoBlock(LOGO_SCAN, WIDTH - 2).forEach(line => {
101
+ lines.push(chalk.gray(BOX.vertical) + logoColor(line) + chalk.gray(BOX.vertical));
102
+ });
103
+
104
+ const subTitle = hasIssues ? 'INTEGRITY SCAN • ISSUES DETECTED' : 'INTEGRITY SCAN • CLEAN';
105
+ lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.bold.white(subTitle), WIDTH - 2) + chalk.gray(BOX.vertical));
106
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
107
+
108
+ // 3. TELEMETRY
109
+ lines.push(chalk.gray(BOX.teeRight + BOX.horizontal.repeat(WIDTH - 2) + BOX.teeLeft));
110
+ const stats = `📡 TELEMETRY ${duration}ms │ 📂 ${scannedFiles} Files │ 📦 ${heapMB}MB`;
111
+ lines.push(chalk.gray(BOX.vertical) + padCenter(stats, WIDTH - 2) + chalk.gray(BOX.vertical));
112
+ lines.push(chalk.gray(BOX.teeRight + BOX.horizontal.repeat(WIDTH - 2) + BOX.teeLeft));
113
+
114
+ if (!hasIssues) {
115
+ // -- CLEAN STATE --
116
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
117
+ lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.green('✅ NO STATIC ISSUES FOUND'), WIDTH - 2) + chalk.gray(BOX.vertical));
118
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
637
119
  } else {
638
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.pass}✓ Looking good!${ansi.reset} Only minor issues found${' '.repeat(width - 40)}${palette.border}${B.lv}${ansi.reset}`);
639
- lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
640
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck prove${ansi.reset} ${palette.muted}→ Runtime proof + Verified Badge${ansi.reset}${' '.repeat(width - 50)}${palette.border}${B.lv}${ansi.reset}`);
641
- lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.muted}Requires PRO vibecheck.dev/pricing${ansi.reset}${' '.repeat(width - 39)}${palette.border}${B.lv}${ansi.reset}`);
642
- }
643
-
644
- lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
645
- lines.push(` ${palette.border}${B.rbl}${B.lh.repeat(width)}${B.rbr}${ansi.reset}`);
646
-
647
- return lines.join('\n');
648
- }
649
-
650
- // ═══════════════════════════════════════════════════════════════════════════════
651
- // MAIN OUTPUT FORMATTER
652
- // ═══════════════════════════════════════════════════════════════════════════════
120
+ // -- ISSUES STATE --
121
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
122
+ const scoreBar = renderProgressBar(score, 20);
123
+ lines.push(chalk.gray(BOX.vertical) + padCenter(`HEALTH SCORE [${scoreBar}] ${score} / 100`, WIDTH + 18) + chalk.gray(BOX.vertical));
124
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
125
+
126
+ // 4. FINDINGS TABLE
127
+ lines.push(chalk.gray(BOX.vertical) + padCenter('DETECTED VULNERABILITIES (STATIC)', WIDTH - 2) + chalk.gray(BOX.vertical));
128
+
129
+ // Table Construction
130
+ const C1 = COL_1, C2 = COL_2, C3 = COL_3;
131
+ const tTop = ` ${BOX.tTopLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTeeTop}${BOX.tHorizontal.repeat(C2)}${BOX.tTeeTop}${BOX.tHorizontal.repeat(C3)}${BOX.tTopRight} `;
132
+ const tMid = ` ${BOX.tTeeLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTee}${BOX.tHorizontal.repeat(C2)}${BOX.tTee}${BOX.tHorizontal.repeat(C3)}${BOX.tTeeRight} `;
133
+ const tBot = ` ${BOX.tBottomLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTeeBottom}${BOX.tHorizontal.repeat(C2)}${BOX.tTeeBottom}${BOX.tHorizontal.repeat(C3)}${BOX.tBottomRight} `;
134
+
135
+ lines.push(chalk.gray(BOX.vertical) + chalk.gray(tTop) + chalk.gray(BOX.vertical));
136
+ const header = ` ${BOX.tVertical}${padRight(' SEVERITY', C1)}${BOX.tVertical}${padRight(' TYPE', C2)}${BOX.tVertical}${padRight(' FINDING', C3)}${BOX.tVertical} `;
137
+ lines.push(chalk.gray(BOX.vertical) + chalk.bold(header) + chalk.gray(BOX.vertical));
138
+ lines.push(chalk.gray(BOX.vertical) + chalk.gray(tMid) + chalk.gray(BOX.vertical));
139
+
140
+ // Rows
141
+ const topItems = findings.slice(0, 5);
142
+ topItems.forEach(item => {
143
+ const severity = item.severity === 'critical' ? chalk.red('🛑 CRIT ') : chalk.yellow('🟡 WARN ');
144
+ const type = padRight(' ' + (item.type || 'Unknown'), C2);
145
+ const desc = padRight(' ' + (item.message || ''), C3);
146
+
147
+ const row = ` ${chalk.gray(BOX.tVertical)}${padRight(severity, C1)}${chalk.gray(BOX.tVertical)}${type}${chalk.gray(BOX.tVertical)}${desc}${chalk.gray(BOX.tVertical)} `;
148
+ lines.push(chalk.gray(BOX.vertical) + row + chalk.gray(BOX.vertical));
149
+ });
653
150
 
654
- function formatScanOutput(result, options = {}) {
655
- const { verbose = false, json = false } = options;
656
-
657
- if (json) {
658
- return JSON.stringify(result, null, 2);
659
- }
660
-
661
- const { verdict, findings = [], layers = [], coverage, breakdown, timings = {}, diff } = result;
662
-
663
- // Count findings by severity
664
- const severityCounts = {
665
- critical: findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length,
666
- high: findings.filter(f => f.severity === 'high').length,
667
- medium: findings.filter(f => f.severity === 'medium' || f.severity === 'WARN' || f.severity === 'warning').length,
668
- low: findings.filter(f => f.severity === 'low' || f.severity === 'INFO' || f.severity === 'info').length,
669
- };
670
-
671
- // Calculate score
672
- const score = verdict?.score ?? calculateScore(severityCounts);
673
- const verdictStatus = verdict?.verdict || (severityCounts.critical > 0 ? 'BLOCK' : severityCounts.high > 0 ? 'WARN' : 'SHIP');
674
-
675
- const lines = [];
676
-
677
- // Diff summary (if baseline comparison was done)
678
- if (diff && (diff.summary.newCount > 0 || diff.summary.fixedCount > 0)) {
679
- lines.push(renderDiffSummary(diff));
680
- }
681
-
682
- // Premium score card
683
- lines.push(renderPremiumScoreCard(score, {
684
- verdict: verdictStatus,
685
- findings: severityCounts,
686
- duration: timings.total,
687
- cached: result.cached,
688
- }));
689
-
690
- // Blockers table (with status badges)
691
- const blockers = findings.filter(f =>
692
- f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high'
693
- );
694
- lines.push(renderBlockersTable(blockers, { showStatus: !!diff }));
695
-
696
- // Category table with visual bars
697
- if (findings.length > 0) {
698
- lines.push(renderCategoryTable(findings));
699
- }
700
-
701
- // Verbose output
702
- if (verbose) {
703
- // Top findings with fix hints
704
- lines.push(renderTopFindings(findings));
705
-
706
- // Coverage map
707
- if (coverage) {
708
- lines.push(renderCoverageMap(coverage));
709
- }
710
-
711
- // Layers status
712
- if (layers.length > 0) {
713
- lines.push(renderLayers(layers));
714
- }
151
+ lines.push(chalk.gray(BOX.vertical) + chalk.gray(tBot) + chalk.gray(BOX.vertical));
152
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
715
153
  }
716
-
717
- // Timing
718
- if (timings.total) {
719
- lines.push('');
720
- lines.push(` ${palette.muted}Completed in ${formatDuration(timings.total)}${ansi.reset}`);
721
- }
722
-
723
- // Upsell banner
724
- lines.push(renderUpsellBanner(severityCounts));
725
-
726
- lines.push('');
727
- return lines.join('\n');
728
- }
729
154
 
730
- // ═══════════════════════════════════════════════════════════════════════════════
731
- // UTILITIES
732
- // ═══════════════════════════════════════════════════════════════════════════════
733
-
734
- function calculateScore(severityCounts) {
735
- const deductions =
736
- (severityCounts.critical || 0) * 25 +
737
- (severityCounts.high || 0) * 15 +
738
- (severityCounts.medium || 0) * 5 +
739
- (severityCounts.low || 0) * 1;
155
+ // 5. LOCKED / UPSELL SECTION (Redacted for SCAN context)
156
+ lines.push(chalk.gray(BOX.teeRight + BOX.horizontal.repeat(WIDTH - 2) + BOX.teeLeft));
740
157
 
741
- return Math.max(0, 100 - deductions);
742
- }
158
+ const lockTitle = chalk.white.bold('🔒 DEEP SCAN ANALYSIS: ') + chalk.gray('UNAVAILABLE');
159
+ lines.push(chalk.gray(BOX.vertical) + padCenter(lockTitle, WIDTH + 9) + chalk.gray(BOX.vertical));
160
+ lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.gray('─'.repeat(60)), WIDTH + 9) + chalk.gray(BOX.vertical));
743
161
 
744
- const EXIT_CODES = {
745
- SUCCESS: 0,
746
- WARNING: 1,
747
- FAILURE: 2,
748
- ERROR: 3,
749
- };
162
+ lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.dim('Static analysis is limited. The following runtime vectors'), WIDTH + 9) + chalk.gray(BOX.vertical));
163
+ lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.dim('cannot be verified without the Vibecheck Truth Engine.'), WIDTH + 9) + chalk.gray(BOX.vertical));
164
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
750
165
 
751
- function getExitCode(verdict) {
752
- if (!verdict) return EXIT_CODES.ERROR;
753
- const status = verdict.verdict || verdict;
754
- switch (status) {
755
- case 'PASS':
756
- case 'SHIP':
757
- return EXIT_CODES.SUCCESS;
758
- case 'WARN':
759
- return EXIT_CODES.WARNING;
760
- case 'FAIL':
761
- case 'BLOCK':
762
- return EXIT_CODES.FAILURE;
763
- default:
764
- return EXIT_CODES.ERROR;
765
- }
766
- }
166
+ // Redacted Lines
167
+ const redacted = [
168
+ '░░ [REDACTED] 🔒 Runtime API Schema Validation (Drift Detection)',
169
+ '░░ [REDACTED] 🔒 Live Auth Boundary Verification (RBAC)',
170
+ '░░ [REDACTED] 🔒 Environment Variable Resolution (Config)'
171
+ ];
767
172
 
768
- function printError(error, context = '') {
769
- const prefix = context ? `${context}: ` : '';
770
- console.error('');
771
- console.error(` ${palette.fail}✗${ansi.reset} ${ansi.bold}${prefix}${error.message || error}${ansi.reset}`);
772
- if (error.code) {
773
- console.error(` ${palette.muted}Error code: ${error.code}${ansi.reset}`);
774
- }
775
- if (error.suggestion || error.fix) {
776
- console.error(` ${palette.pass}→${ansi.reset} ${error.suggestion || error.fix}`);
777
- }
778
- console.error('');
779
- return EXIT_CODES.ERROR;
780
- }
173
+ redacted.forEach(r => {
174
+ lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.dim(r), WIDTH + 9) + chalk.gray(BOX.vertical));
175
+ });
781
176
 
782
- // SARIF export
783
- function formatSARIF(findings, options = {}) {
784
- const { projectPath = '.', version = '1.0.0' } = options;
785
- const rules = new Map();
786
- const results = [];
177
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
787
178
 
788
- for (const f of findings) {
789
- const ruleId = f.ruleId || f.id || `vibecheck/${f.category || 'general'}`;
790
- if (!rules.has(ruleId)) {
791
- rules.set(ruleId, {
792
- id: ruleId,
793
- name: f.category || 'general',
794
- shortDescription: { text: f.title || f.message },
795
- defaultConfiguration: { level: sarifLevel(f.severity) },
796
- helpUri: 'https://vibecheck.dev/docs/rules/' + ruleId,
797
- });
798
- }
799
-
800
- const result = {
801
- ruleId,
802
- level: sarifLevel(f.severity),
803
- message: { text: f.message || f.title },
804
- locations: [],
805
- };
806
-
807
- if (f.file) {
808
- result.locations.push({
809
- physicalLocation: {
810
- artifactLocation: { uri: f.file },
811
- region: f.line ? { startLine: f.line, startColumn: f.column || 1 } : undefined,
812
- },
813
- });
814
- }
815
-
816
- if (f.fix) {
817
- result.fixes = [{ description: { text: f.fix } }];
818
- }
819
-
820
- results.push(result);
821
- }
822
-
823
- return {
824
- $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
825
- version: '2.1.0',
826
- runs: [{
827
- tool: {
828
- driver: {
829
- name: 'vibecheck',
830
- version,
831
- informationUri: 'https://vibecheck.dev',
832
- rules: Array.from(rules.values()),
833
- },
834
- },
835
- results,
836
- invocations: [{ executionSuccessful: true, endTimeUtc: new Date().toISOString() }],
837
- }],
838
- };
839
- }
179
+ // CTA
180
+ lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.bold('👉 UNLOCK INSIGHTS:'), WIDTH + 4) + chalk.gray(BOX.vertical));
181
+ lines.push(chalk.gray(BOX.vertical) + padCenter('Run ' + chalk.cyan('vibecheck upgrade pro') + ' to enable Deep Scan.', WIDTH + 13) + chalk.gray(BOX.vertical));
840
182
 
841
- function sarifLevel(severity) {
842
- const levels = {
843
- critical: 'error', BLOCK: 'error', high: 'error',
844
- medium: 'warning', WARN: 'warning', warning: 'warning',
845
- low: 'note', INFO: 'note', info: 'none',
846
- };
847
- return levels[severity] || 'warning';
848
- }
183
+ // BOTTOM FRAME
184
+ lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
185
+ lines.push(chalk.gray(BOX.bottomLeft + BOX.horizontal.repeat(WIDTH - 2) + BOX.bottomRight));
849
186
 
850
- // Legacy compatibility exports
851
- function renderBreakdown(breakdown) {
852
- // Simplified - can expand later
853
- return '';
187
+ return lines.join('\n');
854
188
  }
855
189
 
856
- module.exports = {
857
- formatScanOutput,
858
- formatSARIF,
859
- renderLayers,
860
- renderCoverageMap,
861
- renderBreakdown,
862
- renderBlockers: renderBlockersTable,
863
- renderCategorySummary: renderCategoryTable,
864
- calculateScore,
865
- getExitCode,
866
- printError,
867
- EXIT_CODES,
868
- };
190
+ module.exports = { formatScanOutput };