@vibecheckai/cli 3.1.2 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -33
- package/bin/registry.js +319 -34
- package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
- package/bin/runners/REPORT_AUDIT.md +64 -0
- package/bin/runners/lib/entitlements-v2.js +97 -28
- package/bin/runners/lib/entitlements.js +3 -6
- package/bin/runners/lib/init-wizard.js +1 -1
- package/bin/runners/lib/report-engine.js +459 -280
- package/bin/runners/lib/report-html.js +1154 -1423
- package/bin/runners/lib/report-output.js +187 -0
- package/bin/runners/lib/report-templates.js +848 -850
- package/bin/runners/lib/scan-output.js +545 -0
- package/bin/runners/lib/server-usage.js +0 -12
- package/bin/runners/lib/ship-output.js +641 -0
- package/bin/runners/lib/status-output.js +253 -0
- package/bin/runners/lib/terminal-ui.js +853 -0
- package/bin/runners/runCheckpoint.js +502 -0
- package/bin/runners/runContracts.js +105 -0
- package/bin/runners/runExport.js +93 -0
- package/bin/runners/runFix.js +31 -24
- package/bin/runners/runInit.js +377 -112
- package/bin/runners/runInstall.js +1 -5
- package/bin/runners/runLabs.js +3 -3
- package/bin/runners/runPolish.js +2452 -0
- package/bin/runners/runProve.js +2 -2
- package/bin/runners/runReport.js +251 -200
- package/bin/runners/runRuntime.js +110 -0
- package/bin/runners/runScan.js +477 -379
- package/bin/runners/runSecurity.js +92 -0
- package/bin/runners/runShip.js +137 -207
- package/bin/runners/runStatus.js +16 -68
- package/bin/runners/utils.js +5 -5
- package/bin/vibecheck.js +25 -11
- package/mcp-server/index.js +150 -18
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +13 -13
- package/mcp-server/tier-auth.js +292 -27
- package/mcp-server/vibecheck-tools.js +9 -9
- package/package.json +1 -1
- package/bin/runners/runClaimVerifier.js +0 -483
- package/bin/runners/runContextCompiler.js +0 -385
- package/bin/runners/runGate.js +0 -17
- package/bin/runners/runInitGha.js +0 -164
- package/bin/runners/runInteractive.js +0 -388
- package/bin/runners/runMdc.js +0 -204
- package/bin/runners/runMissionGenerator.js +0 -282
- package/bin/runners/runTruthpack.js +0 -636
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan Output - Premium Scan Results Display
|
|
3
|
+
*
|
|
4
|
+
* Handles all scan output formatting:
|
|
5
|
+
* - Layer status display
|
|
6
|
+
* - Findings grouped by category
|
|
7
|
+
* - Coverage maps
|
|
8
|
+
* - JSON/SARIF export
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
ansi,
|
|
13
|
+
colors,
|
|
14
|
+
box,
|
|
15
|
+
icons,
|
|
16
|
+
renderScoreCard,
|
|
17
|
+
renderFindingsList,
|
|
18
|
+
renderSection,
|
|
19
|
+
renderDivider,
|
|
20
|
+
renderTable,
|
|
21
|
+
formatDuration,
|
|
22
|
+
formatNumber,
|
|
23
|
+
truncate,
|
|
24
|
+
} = require('./terminal-ui');
|
|
25
|
+
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
// LAYER STATUS DISPLAY
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
function renderLayers(layers) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
lines.push(renderSection('ANALYSIS LAYERS', '⚡'));
|
|
33
|
+
lines.push('');
|
|
34
|
+
|
|
35
|
+
const layerConfig = {
|
|
36
|
+
ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code parsing' },
|
|
37
|
+
truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
|
|
38
|
+
reality: { name: 'Reality Check', icon: '🎭', desc: 'Playwright runtime' },
|
|
39
|
+
realitySniff: { name: 'Reality Sniff', icon: '🔬', desc: 'AI artifact detection' },
|
|
40
|
+
detection: { name: 'Detection Engines', icon: '🛡️', desc: 'Security patterns' },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (const layer of layers) {
|
|
44
|
+
const config = layerConfig[layer.name] || { name: layer.name, icon: '○', desc: '' };
|
|
45
|
+
|
|
46
|
+
let status, statusColor;
|
|
47
|
+
if (layer.skipped) {
|
|
48
|
+
status = `${ansi.dim}○ skipped${ansi.reset}`;
|
|
49
|
+
} else if (layer.error) {
|
|
50
|
+
status = `${colors.error}${icons.error} error${ansi.reset}`;
|
|
51
|
+
} else {
|
|
52
|
+
status = `${colors.success}${icons.success}${ansi.reset}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const duration = layer.duration ? `${ansi.dim}${layer.duration}ms${ansi.reset}` : '';
|
|
56
|
+
const findings = layer.findings !== undefined ? `${colors.accent}${layer.findings}${ansi.reset} ${ansi.dim}findings${ansi.reset}` : '';
|
|
57
|
+
|
|
58
|
+
lines.push(` ${status} ${config.icon} ${config.name.padEnd(20)} ${duration.padEnd(15)} ${findings}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
65
|
+
// COVERAGE MAP DISPLAY
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
function renderCoverageMap(coverage) {
|
|
69
|
+
if (!coverage) return '';
|
|
70
|
+
|
|
71
|
+
const lines = [];
|
|
72
|
+
lines.push(renderSection('ROUTE COVERAGE', '🗺️'));
|
|
73
|
+
lines.push('');
|
|
74
|
+
|
|
75
|
+
const pct = coverage.coveragePercent || 0;
|
|
76
|
+
const color = pct >= 80 ? colors.success : pct >= 60 ? colors.warning : colors.error;
|
|
77
|
+
|
|
78
|
+
// Coverage bar
|
|
79
|
+
const barWidth = 50;
|
|
80
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
81
|
+
const bar = `${color}${'█'.repeat(filled)}${ansi.dim}${'░'.repeat(barWidth - filled)}${ansi.reset}`;
|
|
82
|
+
|
|
83
|
+
lines.push(` ${color}${ansi.bold}${pct}%${ansi.reset} ${ansi.dim}of routes reachable from${ansi.reset} ${colors.accent}/${ansi.reset}`);
|
|
84
|
+
lines.push(` ${bar}`);
|
|
85
|
+
lines.push('');
|
|
86
|
+
|
|
87
|
+
// Stats
|
|
88
|
+
const stats = [
|
|
89
|
+
['Total Routes', coverage.totalRoutes || 0],
|
|
90
|
+
['Reachable', coverage.reachableFromRoot || 0],
|
|
91
|
+
['Orphaned', coverage.orphanedRoutes || 0],
|
|
92
|
+
['Dead Links', coverage.deadLinks || 0],
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const [label, value] of stats) {
|
|
96
|
+
const valueColor = label === 'Orphaned' || label === 'Dead Links'
|
|
97
|
+
? (value > 0 ? colors.error : colors.success)
|
|
98
|
+
: ansi.reset;
|
|
99
|
+
lines.push(` ${ansi.dim}${label}:${ansi.reset} ${valueColor}${ansi.bold}${value}${ansi.reset}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Isolated clusters
|
|
103
|
+
if (coverage.isolatedClusters?.length > 0) {
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push(` ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}Isolated clusters:${ansi.reset}`);
|
|
106
|
+
for (const cluster of coverage.isolatedClusters.slice(0, 3)) {
|
|
107
|
+
const auth = cluster.requiresAuth ? ` ${ansi.dim}(auth required)${ansi.reset}` : '';
|
|
108
|
+
lines.push(` ${ansi.dim}├─${ansi.reset} ${ansi.bold}${cluster.name}${ansi.reset}${auth} ${ansi.dim}(${cluster.nodeIds?.length || 0} routes)${ansi.reset}`);
|
|
109
|
+
}
|
|
110
|
+
if (coverage.isolatedClusters.length > 3) {
|
|
111
|
+
lines.push(` ${ansi.dim}└─ ... and ${coverage.isolatedClusters.length - 3} more clusters${ansi.reset}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Unreachable routes
|
|
116
|
+
if (coverage.unreachableRoutes?.length > 0) {
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(` ${colors.error}${icons.error}${ansi.reset} ${ansi.dim}Unreachable routes:${ansi.reset}`);
|
|
119
|
+
for (const route of coverage.unreachableRoutes.slice(0, 5)) {
|
|
120
|
+
lines.push(` ${ansi.dim}├─${ansi.reset} ${colors.error}${route}${ansi.reset}`);
|
|
121
|
+
}
|
|
122
|
+
if (coverage.unreachableRoutes.length > 5) {
|
|
123
|
+
lines.push(` ${ansi.dim}└─ ... and ${coverage.unreachableRoutes.length - 5} more${ansi.reset}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
131
|
+
// BREAKDOWN DISPLAY
|
|
132
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
133
|
+
|
|
134
|
+
function renderBreakdown(breakdown) {
|
|
135
|
+
if (!breakdown) return '';
|
|
136
|
+
|
|
137
|
+
const lines = [];
|
|
138
|
+
lines.push(renderSection('SCORE BREAKDOWN', '📊'));
|
|
139
|
+
lines.push('');
|
|
140
|
+
|
|
141
|
+
const items = [
|
|
142
|
+
{ key: 'deadLinks', label: 'Dead Links', icon: '🔗' },
|
|
143
|
+
{ key: 'orphanRoutes', label: 'Orphan Routes', icon: '👻' },
|
|
144
|
+
{ key: 'runtimeFailures', label: 'Runtime 404s', icon: '💥' },
|
|
145
|
+
{ key: 'unresolvedDynamic', label: 'Unresolved Dynamic', icon: '❓' },
|
|
146
|
+
{ key: 'placeholders', label: 'Placeholders', icon: '📝' },
|
|
147
|
+
{ key: 'secretsExposed', label: 'Secrets Exposed', icon: '🔐' },
|
|
148
|
+
{ key: 'authBypass', label: 'Auth Bypass', icon: '🚪' },
|
|
149
|
+
{ key: 'mockData', label: 'Mock Data', icon: '🎭' },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
const data = breakdown[item.key];
|
|
154
|
+
if (!data && data !== 0) continue;
|
|
155
|
+
|
|
156
|
+
const count = typeof data === 'object' ? data.count : data;
|
|
157
|
+
const penalty = typeof data === 'object' ? data.penalty : 0;
|
|
158
|
+
|
|
159
|
+
const status = count === 0 ? `${colors.success}${icons.success}${ansi.reset}` : `${colors.error}${icons.error}${ansi.reset}`;
|
|
160
|
+
const countColor = count === 0 ? colors.success : colors.error;
|
|
161
|
+
const penaltyStr = penalty > 0 ? `${ansi.dim}-${penalty} pts${ansi.reset}` : `${ansi.dim} ---${ansi.reset}`;
|
|
162
|
+
|
|
163
|
+
lines.push(` ${status} ${item.icon} ${item.label.padEnd(22)} ${countColor}${ansi.bold}${String(count).padStart(3)}${ansi.reset} ${penaltyStr}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
170
|
+
// BLOCKERS DISPLAY
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
172
|
+
|
|
173
|
+
function renderBlockers(blockers, options = {}) {
|
|
174
|
+
const { maxItems = 8 } = options;
|
|
175
|
+
|
|
176
|
+
if (!blockers || blockers.length === 0) {
|
|
177
|
+
const lines = [];
|
|
178
|
+
lines.push(renderSection('SHIP BLOCKERS', '🚀'));
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(` ${colors.success}${ansi.bold}${icons.success} No blockers! You're clear to ship.${ansi.reset}`);
|
|
181
|
+
return lines.join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const lines = [];
|
|
185
|
+
lines.push(renderSection(`SHIP BLOCKERS (${blockers.length})`, '🚨'));
|
|
186
|
+
lines.push('');
|
|
187
|
+
|
|
188
|
+
for (const blocker of blockers.slice(0, maxItems)) {
|
|
189
|
+
const sevColor = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
|
|
190
|
+
? colors.bg.error
|
|
191
|
+
: colors.bg.warning;
|
|
192
|
+
const sevLabel = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
|
|
193
|
+
? 'CRITICAL'
|
|
194
|
+
: ' HIGH ';
|
|
195
|
+
|
|
196
|
+
lines.push(` ${sevColor}${ansi.bold} ${sevLabel} ${ansi.reset} ${ansi.bold}${truncate(blocker.title || blocker.message, 45)}${ansi.reset}`);
|
|
197
|
+
|
|
198
|
+
if (blocker.description) {
|
|
199
|
+
lines.push(` ${ansi.dim}${truncate(blocker.description, 55)}${ansi.reset}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (blocker.file) {
|
|
203
|
+
const fileDisplay = blocker.file + (blocker.line ? `:${blocker.line}` : '');
|
|
204
|
+
lines.push(` ${colors.accent}${truncate(fileDisplay, 50)}${ansi.reset}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (blocker.fix || blocker.fixSuggestion) {
|
|
208
|
+
lines.push(` ${colors.success}→ ${truncate(blocker.fix || blocker.fixSuggestion, 50)}${ansi.reset}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
lines.push('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (blockers.length > maxItems) {
|
|
215
|
+
lines.push(` ${ansi.dim}... and ${blockers.length - maxItems} more blockers${ansi.reset}`);
|
|
216
|
+
lines.push('');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
223
|
+
// CATEGORY SUMMARY
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
function renderCategorySummary(findings) {
|
|
227
|
+
if (!findings || findings.length === 0) return '';
|
|
228
|
+
|
|
229
|
+
// Group by category
|
|
230
|
+
const categories = {};
|
|
231
|
+
for (const f of findings) {
|
|
232
|
+
const cat = f.category || f.ruleId?.split('/')[0] || 'other';
|
|
233
|
+
if (!categories[cat]) {
|
|
234
|
+
categories[cat] = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
|
|
235
|
+
}
|
|
236
|
+
categories[cat].total++;
|
|
237
|
+
|
|
238
|
+
const sev = (f.severity || '').toLowerCase();
|
|
239
|
+
if (sev === 'critical' || sev === 'block') categories[cat].critical++;
|
|
240
|
+
else if (sev === 'high') categories[cat].high++;
|
|
241
|
+
else if (sev === 'medium' || sev === 'warn' || sev === 'warning') categories[cat].medium++;
|
|
242
|
+
else categories[cat].low++;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const lines = [];
|
|
246
|
+
lines.push(renderSection('FINDINGS BY CATEGORY', '📁'));
|
|
247
|
+
lines.push('');
|
|
248
|
+
|
|
249
|
+
const categoryIcons = {
|
|
250
|
+
ROUTE: '🔗',
|
|
251
|
+
AUTH: '🔐',
|
|
252
|
+
SECRET: '🔑',
|
|
253
|
+
BILLING: '💳',
|
|
254
|
+
MOCK: '🎭',
|
|
255
|
+
DEAD_UI: '👻',
|
|
256
|
+
FAKE_SUCCESS: '✨',
|
|
257
|
+
REALITY: '🔬',
|
|
258
|
+
QUALITY: '📋',
|
|
259
|
+
CONFIG: '⚙️',
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const sortedCategories = Object.entries(categories)
|
|
263
|
+
.sort((a, b) => {
|
|
264
|
+
// Sort by criticality: critical > high > medium > low
|
|
265
|
+
const aCrit = a[1].critical * 1000 + a[1].high * 100 + a[1].medium * 10;
|
|
266
|
+
const bCrit = b[1].critical * 1000 + b[1].high * 100 + b[1].medium * 10;
|
|
267
|
+
return bCrit - aCrit;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
for (const [cat, counts] of sortedCategories) {
|
|
271
|
+
const icon = categoryIcons[cat.toUpperCase()] || '•';
|
|
272
|
+
const critStr = counts.critical > 0 ? `${colors.critical}${counts.critical}C${ansi.reset} ` : '';
|
|
273
|
+
const highStr = counts.high > 0 ? `${colors.high}${counts.high}H${ansi.reset} ` : '';
|
|
274
|
+
const medStr = counts.medium > 0 ? `${colors.medium}${counts.medium}M${ansi.reset} ` : '';
|
|
275
|
+
const lowStr = counts.low > 0 ? `${colors.low}${counts.low}L${ansi.reset}` : '';
|
|
276
|
+
|
|
277
|
+
lines.push(` ${icon} ${cat.padEnd(15)} ${ansi.dim}${String(counts.total).padStart(3)} total${ansi.reset} ${critStr}${highStr}${medStr}${lowStr}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return lines.join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
284
|
+
// FULL SCAN OUTPUT
|
|
285
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
286
|
+
|
|
287
|
+
function formatScanOutput(result, options = {}) {
|
|
288
|
+
const { verbose = false, json = false } = options;
|
|
289
|
+
|
|
290
|
+
if (json) {
|
|
291
|
+
return JSON.stringify(result, null, 2);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const { verdict, findings = [], layers = [], coverage, breakdown, timings = {} } = result;
|
|
295
|
+
|
|
296
|
+
// Count findings by severity
|
|
297
|
+
const severityCounts = {
|
|
298
|
+
critical: findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length,
|
|
299
|
+
high: findings.filter(f => f.severity === 'high').length,
|
|
300
|
+
medium: findings.filter(f => f.severity === 'medium' || f.severity === 'WARN' || f.severity === 'warning').length,
|
|
301
|
+
low: findings.filter(f => f.severity === 'low' || f.severity === 'INFO' || f.severity === 'info').length,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Calculate score
|
|
305
|
+
const score = verdict?.score ?? calculateScore(severityCounts);
|
|
306
|
+
const verdictStatus = verdict?.verdict || (severityCounts.critical > 0 ? 'BLOCK' : severityCounts.high > 0 ? 'WARN' : 'SHIP');
|
|
307
|
+
|
|
308
|
+
const lines = [];
|
|
309
|
+
|
|
310
|
+
// Score card
|
|
311
|
+
lines.push(renderScoreCard(score, {
|
|
312
|
+
verdict: verdictStatus,
|
|
313
|
+
findings: severityCounts,
|
|
314
|
+
duration: timings.total,
|
|
315
|
+
cached: result.cached,
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
// Blockers (critical + high severity findings)
|
|
319
|
+
const blockers = findings.filter(f =>
|
|
320
|
+
f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high'
|
|
321
|
+
);
|
|
322
|
+
lines.push(renderBlockers(blockers));
|
|
323
|
+
|
|
324
|
+
// Category summary
|
|
325
|
+
if (findings.length > 0) {
|
|
326
|
+
lines.push(renderCategorySummary(findings));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Verbose output
|
|
330
|
+
if (verbose) {
|
|
331
|
+
// Coverage map
|
|
332
|
+
if (coverage) {
|
|
333
|
+
lines.push('');
|
|
334
|
+
lines.push(renderCoverageMap(coverage));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Breakdown
|
|
338
|
+
if (breakdown) {
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push(renderBreakdown(breakdown));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Layers
|
|
344
|
+
if (layers.length > 0) {
|
|
345
|
+
lines.push('');
|
|
346
|
+
lines.push(renderLayers(layers));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// All findings
|
|
350
|
+
if (findings.length > 0) {
|
|
351
|
+
lines.push('');
|
|
352
|
+
lines.push(renderFindingsList(findings, { maxItems: 20, groupBySeverity: true }));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Timing summary
|
|
357
|
+
if (timings.total) {
|
|
358
|
+
lines.push('');
|
|
359
|
+
lines.push(` ${ansi.dim}Completed in ${formatDuration(timings.total)}${ansi.reset}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
lines.push('');
|
|
363
|
+
return lines.join('\n');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
367
|
+
// SCORE CALCULATION
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
369
|
+
|
|
370
|
+
function calculateScore(severityCounts) {
|
|
371
|
+
const deductions =
|
|
372
|
+
(severityCounts.critical || 0) * 25 +
|
|
373
|
+
(severityCounts.high || 0) * 15 +
|
|
374
|
+
(severityCounts.medium || 0) * 5 +
|
|
375
|
+
(severityCounts.low || 0) * 1;
|
|
376
|
+
|
|
377
|
+
return Math.max(0, 100 - deductions);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
381
|
+
// EXIT CODE DETERMINATION
|
|
382
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
383
|
+
|
|
384
|
+
const EXIT_CODES = {
|
|
385
|
+
SUCCESS: 0,
|
|
386
|
+
WARNING: 1,
|
|
387
|
+
FAILURE: 2,
|
|
388
|
+
ERROR: 3,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
function getExitCode(verdict) {
|
|
392
|
+
if (!verdict) return EXIT_CODES.ERROR;
|
|
393
|
+
|
|
394
|
+
const status = verdict.verdict || verdict;
|
|
395
|
+
|
|
396
|
+
switch (status) {
|
|
397
|
+
case 'PASS':
|
|
398
|
+
case 'SHIP':
|
|
399
|
+
return EXIT_CODES.SUCCESS;
|
|
400
|
+
case 'WARN':
|
|
401
|
+
return EXIT_CODES.WARNING;
|
|
402
|
+
case 'FAIL':
|
|
403
|
+
case 'BLOCK':
|
|
404
|
+
return EXIT_CODES.FAILURE;
|
|
405
|
+
default:
|
|
406
|
+
return EXIT_CODES.ERROR;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
411
|
+
// ERROR DISPLAY
|
|
412
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
413
|
+
|
|
414
|
+
function printError(error, context = '') {
|
|
415
|
+
const prefix = context ? `${context}: ` : '';
|
|
416
|
+
|
|
417
|
+
console.error('');
|
|
418
|
+
console.error(` ${colors.error}${icons.error}${ansi.reset} ${ansi.bold}${prefix}${error.message || error}${ansi.reset}`);
|
|
419
|
+
|
|
420
|
+
if (error.code) {
|
|
421
|
+
console.error(` ${ansi.dim}Error code: ${error.code}${ansi.reset}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (error.suggestion || error.fix) {
|
|
425
|
+
console.error(` ${colors.success}${icons.arrowRight}${ansi.reset} ${error.suggestion || error.fix}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (error.docs || error.helpUrl) {
|
|
429
|
+
console.error(` ${ansi.dim}See: ${error.docs || error.helpUrl}${ansi.reset}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
console.error('');
|
|
433
|
+
|
|
434
|
+
// Return appropriate exit code
|
|
435
|
+
if (error.code === 'VALIDATION_ERROR') return EXIT_CODES.FAILURE;
|
|
436
|
+
if (error.code === 'LIMIT_EXCEEDED') return EXIT_CODES.WARNING;
|
|
437
|
+
return EXIT_CODES.ERROR;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
441
|
+
// SARIF OUTPUT
|
|
442
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
443
|
+
|
|
444
|
+
function formatSARIF(findings, options = {}) {
|
|
445
|
+
const { projectPath = '.', version = '1.0.0' } = options;
|
|
446
|
+
|
|
447
|
+
const rules = new Map();
|
|
448
|
+
const results = [];
|
|
449
|
+
|
|
450
|
+
for (const f of findings) {
|
|
451
|
+
const ruleId = f.ruleId || f.id || `vibecheck/${f.category || 'general'}`;
|
|
452
|
+
|
|
453
|
+
if (!rules.has(ruleId)) {
|
|
454
|
+
rules.set(ruleId, {
|
|
455
|
+
id: ruleId,
|
|
456
|
+
name: f.category || 'general',
|
|
457
|
+
shortDescription: { text: f.title || f.message },
|
|
458
|
+
defaultConfiguration: {
|
|
459
|
+
level: sarifLevel(f.severity),
|
|
460
|
+
},
|
|
461
|
+
helpUri: 'https://vibecheck.dev/docs/rules/' + ruleId,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const result = {
|
|
466
|
+
ruleId,
|
|
467
|
+
level: sarifLevel(f.severity),
|
|
468
|
+
message: { text: f.message || f.title },
|
|
469
|
+
locations: [],
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
if (f.file) {
|
|
473
|
+
result.locations.push({
|
|
474
|
+
physicalLocation: {
|
|
475
|
+
artifactLocation: { uri: f.file },
|
|
476
|
+
region: f.line ? { startLine: f.line, startColumn: f.column || 1 } : undefined,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (f.fix) {
|
|
482
|
+
result.fixes = [{ description: { text: f.fix } }];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
results.push(result);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
490
|
+
version: '2.1.0',
|
|
491
|
+
runs: [{
|
|
492
|
+
tool: {
|
|
493
|
+
driver: {
|
|
494
|
+
name: 'vibecheck',
|
|
495
|
+
version,
|
|
496
|
+
informationUri: 'https://vibecheck.dev',
|
|
497
|
+
rules: Array.from(rules.values()),
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
results,
|
|
501
|
+
invocations: [{
|
|
502
|
+
executionSuccessful: true,
|
|
503
|
+
endTimeUtc: new Date().toISOString(),
|
|
504
|
+
}],
|
|
505
|
+
}],
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function sarifLevel(severity) {
|
|
510
|
+
const levels = {
|
|
511
|
+
critical: 'error',
|
|
512
|
+
BLOCK: 'error',
|
|
513
|
+
high: 'error',
|
|
514
|
+
medium: 'warning',
|
|
515
|
+
WARN: 'warning',
|
|
516
|
+
warning: 'warning',
|
|
517
|
+
low: 'note',
|
|
518
|
+
INFO: 'note',
|
|
519
|
+
info: 'none',
|
|
520
|
+
};
|
|
521
|
+
return levels[severity] || 'warning';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
525
|
+
// EXPORTS
|
|
526
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
527
|
+
|
|
528
|
+
module.exports = {
|
|
529
|
+
// Main formatters
|
|
530
|
+
formatScanOutput,
|
|
531
|
+
formatSARIF,
|
|
532
|
+
|
|
533
|
+
// Component renderers
|
|
534
|
+
renderLayers,
|
|
535
|
+
renderCoverageMap,
|
|
536
|
+
renderBreakdown,
|
|
537
|
+
renderBlockers,
|
|
538
|
+
renderCategorySummary,
|
|
539
|
+
|
|
540
|
+
// Utilities
|
|
541
|
+
calculateScore,
|
|
542
|
+
getExitCode,
|
|
543
|
+
printError,
|
|
544
|
+
EXIT_CODES,
|
|
545
|
+
};
|
|
@@ -269,18 +269,6 @@ class ServerUsageEnforcement {
|
|
|
269
269
|
return { synced: 0, pending: 0 };
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
// Check if API key is configured before attempting sync
|
|
273
|
-
const token = getAuthToken();
|
|
274
|
-
if (!token) {
|
|
275
|
-
// No API key - silently allow offline mode (no warning needed for missing config)
|
|
276
|
-
return {
|
|
277
|
-
synced: 0,
|
|
278
|
-
pending: offline.queue.length,
|
|
279
|
-
error: 'No API key configured',
|
|
280
|
-
offline: true,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
272
|
const result = await apiRequest('/sync', 'POST');
|
|
285
273
|
|
|
286
274
|
if (result.offline || !result.success) {
|