@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.
- package/bin/runners/lib/cli-output.js +236 -210
- package/bin/runners/lib/scan-output.js +144 -822
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/terminal-ui.js +188 -770
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runShip.js +7 -8
- package/package.json +1 -1
|
@@ -1,868 +1,190 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scan Output -
|
|
3
|
-
*
|
|
2
|
+
* Enterprise Scan Output - Premium Format
|
|
4
3
|
* Features:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
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
|
-
//
|
|
12
|
+
// CONFIGURATION
|
|
29
13
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
14
|
|
|
31
|
-
const
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
lT: '┣', rT: '┫', tT: '┳', bT: '┻', x: '╋',
|
|
17
|
+
const BOX = {
|
|
18
|
+
topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
|
|
19
|
+
horizontal: '═', vertical: '║',
|
|
20
|
+
teeRight: '╠', teeLeft: '╣',
|
|
82
21
|
|
|
83
|
-
// Light
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
28
|
+
const LOGO_SCAN = `
|
|
29
|
+
█████████ █████████ █████████ █████████
|
|
30
|
+
███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███
|
|
31
|
+
░███ ░░░ ░███ ░░░ ░███ ░███ ░███ ░███
|
|
32
|
+
░░█████████ ░███ ░███████████ ░███ ░███
|
|
33
|
+
░░░░░░░░███░███ ░███░░░░░███ ░███ ░███
|
|
34
|
+
███ ░███░███ ███ ░███ ░███ ░███ ░███
|
|
35
|
+
░░█████████ ░░█████████ ░███ ░███ ░███ ░███
|
|
36
|
+
░░░░░░░░░ ░░░░░░░░░ ░░░░ ░░░░ ░░░░ ░░░░
|
|
37
|
+
`;
|
|
106
38
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
//
|
|
45
|
+
// UTILITIES
|
|
120
46
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
121
47
|
|
|
122
|
-
function
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
79
|
+
// MAIN FORMATTER
|
|
512
80
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
513
81
|
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
lines.push(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
lines.push(
|
|
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
|
-
|
|
639
|
-
lines.push(
|
|
640
|
-
|
|
641
|
-
lines.push(
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
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 };
|