@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
package/bin/runners/runScan.js
CHANGED
|
@@ -19,321 +19,64 @@ const { enforceLimit, trackUsage } = require("./lib/entitlements");
|
|
|
19
19
|
const { emitScanStart, emitScanComplete } = require("./lib/audit-bridge");
|
|
20
20
|
|
|
21
21
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
-
//
|
|
22
|
+
// ENHANCED TERMINAL UI & OUTPUT MODULES
|
|
23
23
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
white: '\x1b[37m',
|
|
44
|
-
// Bright colors
|
|
45
|
-
gray: '\x1b[90m',
|
|
46
|
-
brightRed: '\x1b[91m',
|
|
47
|
-
brightGreen: '\x1b[92m',
|
|
48
|
-
brightYellow: '\x1b[93m',
|
|
49
|
-
brightBlue: '\x1b[94m',
|
|
50
|
-
brightMagenta: '\x1b[95m',
|
|
51
|
-
brightCyan: '\x1b[96m',
|
|
52
|
-
brightWhite: '\x1b[97m',
|
|
53
|
-
// Background
|
|
54
|
-
bgBlack: '\x1b[40m',
|
|
55
|
-
bgRed: '\x1b[41m',
|
|
56
|
-
bgGreen: '\x1b[42m',
|
|
57
|
-
bgYellow: '\x1b[43m',
|
|
58
|
-
bgBlue: '\x1b[44m',
|
|
59
|
-
bgMagenta: '\x1b[45m',
|
|
60
|
-
bgCyan: '\x1b[46m',
|
|
61
|
-
bgWhite: '\x1b[47m',
|
|
62
|
-
bgBrightBlack: '\x1b[100m',
|
|
63
|
-
bgBrightRed: '\x1b[101m',
|
|
64
|
-
bgBrightGreen: '\x1b[102m',
|
|
65
|
-
bgBrightYellow: '\x1b[103m',
|
|
66
|
-
// Cursor
|
|
67
|
-
cursorUp: (n = 1) => `\x1b[${n}A`,
|
|
68
|
-
cursorDown: (n = 1) => `\x1b[${n}B`,
|
|
69
|
-
cursorRight: (n = 1) => `\x1b[${n}C`,
|
|
70
|
-
cursorLeft: (n = 1) => `\x1b[${n}D`,
|
|
71
|
-
clearLine: '\x1b[2K',
|
|
72
|
-
clearScreen: '\x1b[2J',
|
|
73
|
-
saveCursor: '\x1b[s',
|
|
74
|
-
restoreCursor: '\x1b[u',
|
|
75
|
-
hideCursor: '\x1b[?25l',
|
|
76
|
-
showCursor: '\x1b[?25h',
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// 256-color support
|
|
80
|
-
const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
|
|
81
|
-
const bgRgb = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
|
|
82
|
-
|
|
83
|
-
// Gradient colors for the banner
|
|
84
|
-
const gradientCyan = rgb(0, 255, 255);
|
|
85
|
-
const gradientBlue = rgb(100, 149, 237);
|
|
86
|
-
const gradientPurple = rgb(138, 43, 226);
|
|
87
|
-
const gradientPink = rgb(255, 105, 180);
|
|
88
|
-
const gradientOrange = rgb(255, 165, 0);
|
|
25
|
+
const {
|
|
26
|
+
ansi,
|
|
27
|
+
colors,
|
|
28
|
+
Spinner,
|
|
29
|
+
PhaseProgress,
|
|
30
|
+
renderBanner,
|
|
31
|
+
renderSection,
|
|
32
|
+
formatDuration,
|
|
33
|
+
} = require("./lib/terminal-ui");
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
formatScanOutput,
|
|
37
|
+
formatSARIF,
|
|
38
|
+
getExitCode,
|
|
39
|
+
printError,
|
|
40
|
+
EXIT_CODES,
|
|
41
|
+
calculateScore,
|
|
42
|
+
} = require("./lib/scan-output");
|
|
89
43
|
|
|
90
44
|
const BANNER = `
|
|
91
|
-
${rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${
|
|
92
|
-
${rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${
|
|
93
|
-
${rgb(60, 160, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${
|
|
94
|
-
${rgb(90, 140, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${
|
|
95
|
-
${rgb(120, 120, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${
|
|
96
|
-
${rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${
|
|
97
|
-
|
|
98
|
-
${
|
|
99
|
-
${
|
|
100
|
-
${
|
|
45
|
+
${ansi.rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
|
|
46
|
+
${ansi.rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
|
|
47
|
+
${ansi.rgb(60, 160, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${ansi.reset}
|
|
48
|
+
${ansi.rgb(90, 140, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${ansi.reset}
|
|
49
|
+
${ansi.rgb(120, 120, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${ansi.reset}
|
|
50
|
+
${ansi.rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
|
|
51
|
+
|
|
52
|
+
${ansi.dim} ┌─────────────────────────────────────────────────────────────────────┐${ansi.reset}
|
|
53
|
+
${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}Route Integrity${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}Security${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}Quality${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(100, 100, 100)}Ship with Confidence${ansi.reset} ${ansi.dim}│${ansi.reset}
|
|
54
|
+
${ansi.dim} └─────────────────────────────────────────────────────────────────────┘${ansi.reset}
|
|
101
55
|
`;
|
|
102
56
|
|
|
103
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
104
|
-
// TERMINAL UTILITIES
|
|
105
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
106
|
-
|
|
107
|
-
const BOX_CHARS = {
|
|
108
|
-
topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
|
|
109
|
-
horizontal: '─', vertical: '│',
|
|
110
|
-
teeRight: '├', teeLeft: '┤', teeDown: '┬', teeUp: '┴',
|
|
111
|
-
cross: '┼',
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
115
|
-
let spinnerIndex = 0;
|
|
116
|
-
let spinnerInterval = null;
|
|
117
|
-
|
|
118
|
-
function formatNumber(num) {
|
|
119
|
-
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function truncate(str, len) {
|
|
123
|
-
if (str.length <= len) return str;
|
|
124
|
-
return str.slice(0, len - 3) + '...';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function progressBar(percent, width = 30) {
|
|
128
|
-
const filled = Math.round((percent / 100) * width);
|
|
129
|
-
const empty = width - filled;
|
|
130
|
-
const filledColor = percent >= 80 ? rgb(0, 255, 100) : percent >= 50 ? rgb(255, 200, 0) : rgb(255, 80, 80);
|
|
131
|
-
return `${filledColor}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function startSpinner(message) {
|
|
135
|
-
process.stdout.write(c.hideCursor);
|
|
136
|
-
spinnerInterval = setInterval(() => {
|
|
137
|
-
process.stdout.write(`\r ${c.cyan}${SPINNER_FRAMES[spinnerIndex]}${c.reset} ${message} `);
|
|
138
|
-
spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
|
|
139
|
-
}, 80);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function stopSpinner(message, success = true) {
|
|
143
|
-
if (spinnerInterval) {
|
|
144
|
-
clearInterval(spinnerInterval);
|
|
145
|
-
spinnerInterval = null;
|
|
146
|
-
}
|
|
147
|
-
const icon = success ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
|
|
148
|
-
process.stdout.write(`\r${c.clearLine} ${icon} ${message}\n`);
|
|
149
|
-
process.stdout.write(c.showCursor);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
57
|
function printBanner() {
|
|
153
58
|
console.log(BANNER);
|
|
154
59
|
}
|
|
155
60
|
|
|
156
|
-
|
|
157
|
-
console.log(`${color} ${char.repeat(69)}${c.reset}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function printSection(title, icon = '◆') {
|
|
161
|
-
console.log();
|
|
162
|
-
console.log(` ${rgb(100, 200, 255)}${icon}${c.reset} ${c.bold}${title}${c.reset}`);
|
|
163
|
-
printDivider();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
167
|
-
// SCORE DISPLAY
|
|
168
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
169
|
-
|
|
170
|
-
function getScoreColor(score) {
|
|
171
|
-
if (score >= 90) return rgb(0, 255, 100);
|
|
172
|
-
if (score >= 80) return rgb(100, 255, 100);
|
|
173
|
-
if (score >= 70) return rgb(200, 255, 0);
|
|
174
|
-
if (score >= 60) return rgb(255, 200, 0);
|
|
175
|
-
if (score >= 50) return rgb(255, 150, 0);
|
|
176
|
-
return rgb(255, 80, 80);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function getGradeColor(grade) {
|
|
180
|
-
const colors = {
|
|
181
|
-
'A': rgb(0, 255, 100),
|
|
182
|
-
'B': rgb(100, 255, 100),
|
|
183
|
-
'C': rgb(255, 200, 0),
|
|
184
|
-
'D': rgb(255, 150, 0),
|
|
185
|
-
'F': rgb(255, 80, 80),
|
|
186
|
-
};
|
|
187
|
-
return colors[grade] || c.white;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function printScoreCard(score, grade, canShip) {
|
|
191
|
-
const scoreColor = getScoreColor(score);
|
|
192
|
-
const gradeColor = getGradeColor(grade);
|
|
193
|
-
|
|
194
|
-
console.log();
|
|
195
|
-
console.log(` ${c.dim}╭────────────────────────────────────────────────────────────────╮${c.reset}`);
|
|
196
|
-
console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
|
|
197
|
-
|
|
198
|
-
const scoreStr = `${score}`;
|
|
199
|
-
const scorePadding = ' '.repeat(Math.max(0, 3 - scoreStr.length));
|
|
200
|
-
console.log(` ${c.dim}│${c.reset} ${c.dim}INTEGRITY SCORE${c.reset} ${scoreColor}${c.bold}${scorePadding}${scoreStr}${c.reset}${c.dim}/100${c.reset} ${c.dim}GRADE${c.reset} ${gradeColor}${c.bold}${grade}${c.reset} ${c.dim}│${c.reset}`);
|
|
201
|
-
console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
|
|
202
|
-
console.log(` ${c.dim}│${c.reset} ${progressBar(score, 40)} ${c.dim}│${c.reset}`);
|
|
203
|
-
console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
|
|
204
|
-
|
|
205
|
-
if (canShip) {
|
|
206
|
-
console.log(` ${c.dim}│${c.reset} ${bgRgb(0, 150, 80)}${c.bold} ✓ CLEAR TO SHIP ${c.reset} ${c.dim}│${c.reset}`);
|
|
207
|
-
} else {
|
|
208
|
-
console.log(` ${c.dim}│${c.reset} ${bgRgb(200, 50, 50)}${c.bold} ✗ NOT SHIP READY ${c.reset} ${c.dim}│${c.reset}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
|
|
212
|
-
console.log(` ${c.dim}╰────────────────────────────────────────────────────────────────╯${c.reset}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
216
|
-
// COVERAGE MAP VISUALIZATION
|
|
217
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
218
|
-
|
|
61
|
+
// Legacy compatibility functions - now use enhanced modules
|
|
219
62
|
function printCoverageMap(coverageMap) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const pct = coverageMap.coveragePercent;
|
|
223
|
-
const color = pct >= 80 ? rgb(0, 255, 100) : pct >= 60 ? rgb(255, 200, 0) : rgb(255, 80, 80);
|
|
224
|
-
|
|
225
|
-
console.log();
|
|
226
|
-
console.log(` ${color}${c.bold}${pct}%${c.reset} ${c.dim}of shipped routes reachable from${c.reset} ${c.cyan}/${c.reset}`);
|
|
227
|
-
console.log(` ${progressBar(pct, 50)}`);
|
|
228
|
-
console.log();
|
|
229
|
-
console.log(` ${c.dim}Routes:${c.reset} ${coverageMap.reachableFromRoot}${c.dim}/${c.reset}${coverageMap.totalShippedRoutes} ${c.dim}reachable${c.reset}`);
|
|
230
|
-
|
|
231
|
-
if (coverageMap.isolatedClusters && coverageMap.isolatedClusters.length > 0) {
|
|
232
|
-
console.log();
|
|
233
|
-
console.log(` ${c.yellow}⚠${c.reset} ${c.dim}Isolated clusters:${c.reset}`);
|
|
234
|
-
for (const cluster of coverageMap.isolatedClusters.slice(0, 3)) {
|
|
235
|
-
const auth = cluster.requiresAuth ? ` ${c.dim}(auth)${c.reset}` : '';
|
|
236
|
-
console.log(` ${c.dim}├─${c.reset} ${c.bold}${cluster.name}${c.reset}${auth} ${c.dim}(${cluster.nodeIds.length} routes)${c.reset}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (coverageMap.unreachableRoutes && coverageMap.unreachableRoutes.length > 0) {
|
|
241
|
-
console.log();
|
|
242
|
-
console.log(` ${c.red}✗${c.reset} ${c.dim}Unreachable routes:${c.reset}`);
|
|
243
|
-
for (const route of coverageMap.unreachableRoutes.slice(0, 5)) {
|
|
244
|
-
console.log(` ${c.dim}├─${c.reset} ${c.red}${route}${c.reset}`);
|
|
245
|
-
}
|
|
246
|
-
if (coverageMap.unreachableRoutes.length > 5) {
|
|
247
|
-
console.log(` ${c.dim}└─ ... and ${coverageMap.unreachableRoutes.length - 5} more${c.reset}`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
63
|
+
const { renderCoverageMap } = require("./lib/scan-output");
|
|
64
|
+
console.log(renderCoverageMap(coverageMap));
|
|
250
65
|
}
|
|
251
66
|
|
|
252
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
253
|
-
// BREAKDOWN DISPLAY
|
|
254
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
255
|
-
|
|
256
67
|
function printBreakdown(breakdown) {
|
|
257
|
-
|
|
258
|
-
console.log();
|
|
259
|
-
|
|
260
|
-
const items = [
|
|
261
|
-
{ key: 'deadLinks', label: 'Dead Links', icon: '🔗', color: rgb(255, 100, 100) },
|
|
262
|
-
{ key: 'orphanRoutes', label: 'Orphan Routes', icon: '👻', color: rgb(200, 150, 255) },
|
|
263
|
-
{ key: 'runtimeFailures', label: 'Runtime 404s', icon: '💥', color: rgb(255, 80, 80) },
|
|
264
|
-
{ key: 'unresolvedDynamic', label: 'Unresolved Dynamic', icon: '❓', color: rgb(255, 200, 100) },
|
|
265
|
-
{ key: 'placeholders', label: 'Placeholders', icon: '📝', color: rgb(255, 180, 100) },
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
for (const item of items) {
|
|
269
|
-
const data = breakdown[item.key] || { count: 0, penalty: 0 };
|
|
270
|
-
const status = data.count === 0 ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
|
|
271
|
-
const countColor = data.count === 0 ? c.green : item.color;
|
|
272
|
-
const countStr = String(data.count).padStart(3);
|
|
273
|
-
const penaltyStr = data.penalty > 0 ? `${c.dim}-${data.penalty} pts${c.reset}` : `${c.dim} ---${c.reset}`;
|
|
274
|
-
|
|
275
|
-
console.log(` ${status} ${item.icon} ${item.label.padEnd(22)} ${countColor}${c.bold}${countStr}${c.reset} ${penaltyStr}`);
|
|
276
|
-
}
|
|
68
|
+
const { renderBreakdown } = require("./lib/scan-output");
|
|
69
|
+
console.log(renderBreakdown(breakdown));
|
|
277
70
|
}
|
|
278
71
|
|
|
279
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
280
|
-
// BLOCKERS DISPLAY
|
|
281
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
282
|
-
|
|
283
72
|
function printBlockers(blockers) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
console.log();
|
|
287
|
-
console.log(` ${c.green}${c.bold}✓ No blockers! You're clear to ship.${c.reset}`);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
printSection(`SHIP BLOCKERS (${blockers.length})`, '🚨');
|
|
292
|
-
console.log();
|
|
293
|
-
|
|
294
|
-
for (const blocker of blockers.slice(0, 8)) {
|
|
295
|
-
const sevColor = blocker.severity === 'critical' ? bgRgb(180, 40, 40) : bgRgb(180, 120, 0);
|
|
296
|
-
const sevLabel = blocker.severity === 'critical' ? 'CRITICAL' : ' HIGH ';
|
|
297
|
-
|
|
298
|
-
console.log(` ${sevColor}${c.bold} ${sevLabel} ${c.reset} ${c.bold}${truncate(blocker.title, 45)}${c.reset}`);
|
|
299
|
-
console.log(` ${c.dim} ${truncate(blocker.description, 55)}${c.reset}`);
|
|
300
|
-
if (blocker.file) {
|
|
301
|
-
const fileDisplay = path.basename(blocker.file) + (blocker.line ? `:${blocker.line}` : '');
|
|
302
|
-
console.log(` ${c.dim} ${c.reset}${c.cyan}${fileDisplay}${c.reset}`);
|
|
303
|
-
}
|
|
304
|
-
if (blocker.fixSuggestion) {
|
|
305
|
-
console.log(` ${c.dim} ${c.green}→ ${blocker.fixSuggestion}${c.reset}`);
|
|
306
|
-
}
|
|
307
|
-
console.log();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (blockers.length > 8) {
|
|
311
|
-
console.log(` ${c.dim}... and ${blockers.length - 8} more blockers (see full report)${c.reset}`);
|
|
312
|
-
}
|
|
73
|
+
const { renderBlockers } = require("./lib/scan-output");
|
|
74
|
+
console.log(renderBlockers(blockers));
|
|
313
75
|
}
|
|
314
76
|
|
|
315
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
316
|
-
// LAYERS DISPLAY
|
|
317
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
318
|
-
|
|
319
77
|
function printLayers(layers) {
|
|
320
|
-
|
|
321
|
-
console.log();
|
|
322
|
-
|
|
323
|
-
const layerInfo = {
|
|
324
|
-
ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code analysis' },
|
|
325
|
-
truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
|
|
326
|
-
reality: { name: 'Reality Proof', icon: '🎭', desc: 'Playwright crawl' },
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
for (const layer of layers) {
|
|
330
|
-
const info = layerInfo[layer.layer] || { name: layer.layer, icon: '○', desc: '' };
|
|
331
|
-
const status = layer.executed ? `${c.green}✓${c.reset}` : `${c.dim}○${c.reset}`;
|
|
332
|
-
const duration = layer.executed ? `${c.dim}${layer.duration}ms${c.reset}` : `${c.dim}skipped${c.reset}`;
|
|
333
|
-
const findings = layer.executed ? `${c.cyan}${layer.findings}${c.reset} ${c.dim}findings${c.reset}` : '';
|
|
334
|
-
|
|
335
|
-
console.log(` ${status} ${info.icon} ${c.bold}${info.name.padEnd(15)}${c.reset} ${duration.padEnd(20)} ${findings}`);
|
|
336
|
-
}
|
|
78
|
+
const { renderLayers } = require("./lib/scan-output");
|
|
79
|
+
console.log(renderLayers(layers));
|
|
337
80
|
}
|
|
338
81
|
|
|
339
82
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -351,6 +94,8 @@ function parseArgs(args) {
|
|
|
351
94
|
sarif: false,
|
|
352
95
|
verbose: false,
|
|
353
96
|
help: false,
|
|
97
|
+
autofix: false,
|
|
98
|
+
save: true, // Always save results by default
|
|
354
99
|
};
|
|
355
100
|
|
|
356
101
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -364,6 +109,8 @@ function parseArgs(args) {
|
|
|
364
109
|
else if (arg === '--sarif') opts.sarif = true;
|
|
365
110
|
else if (arg === '--verbose' || arg === '-v') opts.verbose = true;
|
|
366
111
|
else if (arg === '--help' || arg === '-h') opts.help = true;
|
|
112
|
+
else if (arg === '--autofix' || arg === '--fix' || arg === '-f') opts.autofix = true;
|
|
113
|
+
else if (arg === '--no-save') opts.save = false;
|
|
367
114
|
else if (arg === '--path' || arg === '-p') opts.path = args[++i];
|
|
368
115
|
else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
|
|
369
116
|
else if (!arg.startsWith('-')) opts.path = path.resolve(arg);
|
|
@@ -375,33 +122,315 @@ function parseArgs(args) {
|
|
|
375
122
|
function printHelp() {
|
|
376
123
|
console.log(BANNER);
|
|
377
124
|
console.log(`
|
|
378
|
-
${
|
|
379
|
-
|
|
380
|
-
${
|
|
381
|
-
${
|
|
382
|
-
${
|
|
383
|
-
${
|
|
384
|
-
${
|
|
385
|
-
|
|
386
|
-
${
|
|
387
|
-
${
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
${
|
|
391
|
-
${
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
${
|
|
125
|
+
${ansi.bold}Usage:${ansi.reset} vibecheck scan [path] [options]
|
|
126
|
+
|
|
127
|
+
${ansi.bold}Scan Modes:${ansi.reset}
|
|
128
|
+
${colors.accent}(default)${ansi.reset} Layer 1: AST static analysis ${ansi.dim}(fast)${ansi.reset}
|
|
129
|
+
${colors.accent}--truth, -t${ansi.reset} Layer 1+2: Include build manifest verification ${ansi.dim}(CI/ship)${ansi.reset}
|
|
130
|
+
${colors.accent}--reality, -r${ansi.reset} Layer 1+2+3: Include Playwright runtime proof ${ansi.dim}(full)${ansi.reset}
|
|
131
|
+
${colors.accent}--reality-sniff${ansi.reset} Include Reality Sniff AI artifact detection ${ansi.dim}(recommended)${ansi.reset}
|
|
132
|
+
|
|
133
|
+
${ansi.bold}Fix Mode:${ansi.reset}
|
|
134
|
+
${colors.accent}--autofix, -f${ansi.reset} Apply safe fixes + generate AI missions ${ansi.rgb(0, 200, 255)}[STARTER]${ansi.reset}
|
|
135
|
+
|
|
136
|
+
${ansi.bold}Options:${ansi.reset}
|
|
137
|
+
${colors.accent}--url, -u${ansi.reset} Base URL for reality testing (e.g., http://localhost:3000)
|
|
138
|
+
${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
|
|
139
|
+
${colors.accent}--json${ansi.reset} Output results as JSON
|
|
140
|
+
${colors.accent}--sarif${ansi.reset} Output in SARIF format (GitHub code scanning)
|
|
141
|
+
${colors.accent}--no-save${ansi.reset} Don't save results to .vibecheck/results/
|
|
142
|
+
${colors.accent}--help, -h${ansi.reset} Show this help
|
|
143
|
+
|
|
144
|
+
${ansi.bold}Examples:${ansi.reset}
|
|
145
|
+
${ansi.dim}# Quick scan (AST only)${ansi.reset}
|
|
395
146
|
vibecheck scan
|
|
396
147
|
|
|
397
|
-
${
|
|
148
|
+
${ansi.dim}# Scan + autofix with missions${ansi.reset}
|
|
149
|
+
vibecheck scan --autofix
|
|
150
|
+
|
|
151
|
+
${ansi.dim}# CI/CD scan with manifest verification${ansi.reset}
|
|
398
152
|
vibecheck scan --truth
|
|
399
153
|
|
|
400
|
-
${
|
|
154
|
+
${ansi.dim}# Full proof with Playwright${ansi.reset}
|
|
401
155
|
vibecheck scan --reality --url http://localhost:3000
|
|
156
|
+
|
|
157
|
+
${ansi.bold}Output:${ansi.reset}
|
|
158
|
+
Results saved to: .vibecheck/results/latest.json
|
|
159
|
+
Missions saved to: .vibecheck/missions/ ${ansi.dim}(with --autofix)${ansi.reset}
|
|
402
160
|
`);
|
|
403
161
|
}
|
|
404
162
|
|
|
163
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
164
|
+
// AUTOFIX & MISSION GENERATION
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Normalize severity values to standard format
|
|
169
|
+
*/
|
|
170
|
+
function normalizeSeverity(severity) {
|
|
171
|
+
if (!severity) return 'medium';
|
|
172
|
+
const sev = String(severity).toLowerCase();
|
|
173
|
+
if (sev === 'block' || sev === 'critical') return 'critical';
|
|
174
|
+
if (sev === 'high') return 'high';
|
|
175
|
+
if (sev === 'warn' || sev === 'warning' || sev === 'medium') return 'medium';
|
|
176
|
+
if (sev === 'info' || sev === 'low') return 'low';
|
|
177
|
+
return 'medium'; // Default fallback
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function generateMissions(findings, projectPath, opts) {
|
|
181
|
+
const missionsDir = path.join(projectPath, '.vibecheck', 'missions');
|
|
182
|
+
|
|
183
|
+
// Ensure missions directory exists
|
|
184
|
+
if (!fs.existsSync(missionsDir)) {
|
|
185
|
+
fs.mkdirSync(missionsDir, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const missions = [];
|
|
189
|
+
const missionIndex = [];
|
|
190
|
+
|
|
191
|
+
for (let i = 0; i < findings.length; i++) {
|
|
192
|
+
const finding = findings[i];
|
|
193
|
+
const missionId = `mission-${String(i + 1).padStart(3, '0')}`;
|
|
194
|
+
const normalizedSeverity = normalizeSeverity(finding.severity);
|
|
195
|
+
|
|
196
|
+
// Generate AI-ready mission prompt
|
|
197
|
+
const mission = {
|
|
198
|
+
id: missionId,
|
|
199
|
+
createdAt: new Date().toISOString(),
|
|
200
|
+
finding: {
|
|
201
|
+
id: finding.id || `finding-${i + 1}`,
|
|
202
|
+
severity: normalizedSeverity,
|
|
203
|
+
originalSeverity: finding.severity, // Keep original for reference
|
|
204
|
+
category: finding.category,
|
|
205
|
+
title: finding.title || finding.message,
|
|
206
|
+
file: finding.file,
|
|
207
|
+
line: finding.line,
|
|
208
|
+
},
|
|
209
|
+
prompt: generateMissionPrompt(finding),
|
|
210
|
+
constraints: generateConstraints(finding),
|
|
211
|
+
verification: generateVerificationSteps(finding),
|
|
212
|
+
status: 'pending',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
missions.push(mission);
|
|
216
|
+
missionIndex.push({
|
|
217
|
+
id: missionId,
|
|
218
|
+
severity: normalizedSeverity,
|
|
219
|
+
title: finding.title || finding.message,
|
|
220
|
+
file: finding.file,
|
|
221
|
+
status: 'pending',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Save individual mission JSON
|
|
225
|
+
const missionPath = path.join(missionsDir, `${missionId}.json`);
|
|
226
|
+
fs.writeFileSync(missionPath, JSON.stringify(mission, null, 2));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Generate MISSIONS.md index
|
|
230
|
+
const missionsMarkdown = generateMissionsMarkdown(missionIndex, projectPath);
|
|
231
|
+
fs.writeFileSync(path.join(missionsDir, 'MISSIONS.md'), missionsMarkdown);
|
|
232
|
+
|
|
233
|
+
// Save missions summary
|
|
234
|
+
const summary = {
|
|
235
|
+
generatedAt: new Date().toISOString(),
|
|
236
|
+
totalMissions: missions.length,
|
|
237
|
+
bySeverity: {
|
|
238
|
+
critical: missionIndex.filter(m => m.severity === 'critical').length,
|
|
239
|
+
high: missionIndex.filter(m => m.severity === 'high').length,
|
|
240
|
+
medium: missionIndex.filter(m => m.severity === 'medium').length,
|
|
241
|
+
low: missionIndex.filter(m => m.severity === 'low').length,
|
|
242
|
+
},
|
|
243
|
+
missions: missionIndex,
|
|
244
|
+
};
|
|
245
|
+
fs.writeFileSync(path.join(missionsDir, 'summary.json'), JSON.stringify(summary, null, 2));
|
|
246
|
+
|
|
247
|
+
return { missions, summary };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function generateMissionPrompt(finding) {
|
|
251
|
+
const file = finding.file || 'unknown file';
|
|
252
|
+
const line = finding.line ? ` at line ${finding.line}` : '';
|
|
253
|
+
const title = finding.title || finding.message || 'Unknown issue';
|
|
254
|
+
const category = finding.category || 'general';
|
|
255
|
+
|
|
256
|
+
let prompt = `## Mission: Fix ${title}
|
|
257
|
+
|
|
258
|
+
**File:** \`${file}\`${line}
|
|
259
|
+
**Category:** ${category}
|
|
260
|
+
**Severity:** ${finding.severity || 'medium'}
|
|
261
|
+
|
|
262
|
+
### Problem
|
|
263
|
+
${finding.message || title}
|
|
264
|
+
|
|
265
|
+
### Context
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
// Add category-specific context
|
|
269
|
+
if (category === 'ROUTE' || category === 'routes') {
|
|
270
|
+
prompt += `This is a route integrity issue. The route may be:
|
|
271
|
+
- Referenced but not defined
|
|
272
|
+
- Defined but not reachable
|
|
273
|
+
- Missing proper error handling
|
|
274
|
+
`;
|
|
275
|
+
} else if (category === 'ENV' || category === 'env') {
|
|
276
|
+
prompt += `This is an environment variable issue. The variable may be:
|
|
277
|
+
- Used but not defined in .env
|
|
278
|
+
- Missing from .env.example
|
|
279
|
+
- Not documented
|
|
280
|
+
`;
|
|
281
|
+
} else if (category === 'AUTH' || category === 'auth' || category === 'security') {
|
|
282
|
+
prompt += `This is a security/authentication issue. Check for:
|
|
283
|
+
- Proper authentication middleware
|
|
284
|
+
- Authorization checks
|
|
285
|
+
- Secure defaults
|
|
286
|
+
`;
|
|
287
|
+
} else if (category === 'MOCK' || category === 'mock') {
|
|
288
|
+
prompt += `This is a mock/placeholder issue. The code may contain:
|
|
289
|
+
- TODO comments that need implementation
|
|
290
|
+
- Placeholder data that should be real
|
|
291
|
+
- Fake success responses
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
prompt += `
|
|
296
|
+
### Requirements
|
|
297
|
+
1. Fix the issue while maintaining existing functionality
|
|
298
|
+
2. Follow the project's coding style and patterns
|
|
299
|
+
3. Add appropriate error handling
|
|
300
|
+
4. Update tests if they exist
|
|
301
|
+
|
|
302
|
+
### Verification
|
|
303
|
+
After making changes:
|
|
304
|
+
1. Run \`vibecheck scan\` to verify the issue is resolved
|
|
305
|
+
2. Run any existing tests for the affected file
|
|
306
|
+
3. Manually verify the functionality if applicable
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
if (finding.fix || finding.fixSuggestion) {
|
|
310
|
+
prompt += `
|
|
311
|
+
### Suggested Fix
|
|
312
|
+
${finding.fix || finding.fixSuggestion}
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return prompt;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function generateConstraints(finding) {
|
|
320
|
+
const constraints = [
|
|
321
|
+
'Do not break existing functionality',
|
|
322
|
+
'Follow existing code patterns in the project',
|
|
323
|
+
'Add error handling where appropriate',
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
if (finding.category === 'security' || finding.category === 'AUTH') {
|
|
327
|
+
constraints.push('Ensure no security regressions');
|
|
328
|
+
constraints.push('Follow OWASP security guidelines');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (finding.category === 'ROUTE' || finding.category === 'routes') {
|
|
332
|
+
constraints.push('Maintain API backwards compatibility');
|
|
333
|
+
constraints.push('Update route documentation if changed');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return constraints;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function generateVerificationSteps(finding) {
|
|
340
|
+
const steps = [
|
|
341
|
+
'Run `vibecheck scan` and confirm this issue no longer appears',
|
|
342
|
+
'Run `vibecheck checkpoint` to ensure no regressions',
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
if (finding.file) {
|
|
346
|
+
steps.push(`Review changes in \`${finding.file}\``);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (finding.category === 'ROUTE' || finding.category === 'routes') {
|
|
350
|
+
steps.push('Test the affected route(s) manually or with automated tests');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (finding.category === 'ENV' || finding.category === 'env') {
|
|
354
|
+
steps.push('Verify environment variable is properly documented');
|
|
355
|
+
steps.push('Check .env.example is updated');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return steps;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function generateMissionsMarkdown(missionIndex, projectPath) {
|
|
362
|
+
const projectName = path.basename(projectPath);
|
|
363
|
+
const now = new Date().toISOString();
|
|
364
|
+
|
|
365
|
+
let md = `# Vibecheck Missions
|
|
366
|
+
|
|
367
|
+
> Generated: ${now}
|
|
368
|
+
> Project: ${projectName}
|
|
369
|
+
|
|
370
|
+
## Summary
|
|
371
|
+
|
|
372
|
+
| Severity | Count |
|
|
373
|
+
|----------|-------|
|
|
374
|
+
| Critical | ${missionIndex.filter(m => m.severity === 'critical').length} |
|
|
375
|
+
| High | ${missionIndex.filter(m => m.severity === 'high').length} |
|
|
376
|
+
| Medium | ${missionIndex.filter(m => m.severity === 'medium').length} |
|
|
377
|
+
| Low | ${missionIndex.filter(m => m.severity === 'low').length} |
|
|
378
|
+
| **Total** | **${missionIndex.length}** |
|
|
379
|
+
|
|
380
|
+
## Missions
|
|
381
|
+
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
// Group by severity
|
|
385
|
+
const bySeverity = {
|
|
386
|
+
critical: missionIndex.filter(m => m.severity === 'critical'),
|
|
387
|
+
high: missionIndex.filter(m => m.severity === 'high'),
|
|
388
|
+
medium: missionIndex.filter(m => m.severity === 'medium'),
|
|
389
|
+
low: missionIndex.filter(m => m.severity === 'low'),
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
for (const [severity, missions] of Object.entries(bySeverity)) {
|
|
393
|
+
if (missions.length === 0) continue;
|
|
394
|
+
|
|
395
|
+
const emoji = severity === 'critical' ? '🔴' : severity === 'high' ? '🟠' : severity === 'medium' ? '🟡' : '🔵';
|
|
396
|
+
md += `### ${emoji} ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${missions.length})\n\n`;
|
|
397
|
+
|
|
398
|
+
for (const mission of missions) {
|
|
399
|
+
const checkbox = mission.status === 'completed' ? '[x]' : '[ ]';
|
|
400
|
+
md += `- ${checkbox} **${mission.id}**: ${mission.title}\n`;
|
|
401
|
+
if (mission.file) {
|
|
402
|
+
md += ` - File: \`${mission.file}\`\n`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
md += '\n';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
md += `---
|
|
409
|
+
|
|
410
|
+
## How to Use
|
|
411
|
+
|
|
412
|
+
1. **Review missions**: Read each mission file in \`.vibecheck/missions/\`
|
|
413
|
+
2. **Copy prompts**: Use the prompt in each mission file with your AI assistant
|
|
414
|
+
3. **Verify fixes**: Run \`vibecheck scan\` after each fix
|
|
415
|
+
4. **Track progress**: Update mission status in this file
|
|
416
|
+
|
|
417
|
+
## Commands
|
|
418
|
+
|
|
419
|
+
\`\`\`bash
|
|
420
|
+
# Re-scan after fixes
|
|
421
|
+
vibecheck scan
|
|
422
|
+
|
|
423
|
+
# Check progress
|
|
424
|
+
vibecheck checkpoint
|
|
425
|
+
|
|
426
|
+
# Final verification
|
|
427
|
+
vibecheck ship
|
|
428
|
+
\`\`\`
|
|
429
|
+
`;
|
|
430
|
+
|
|
431
|
+
return md;
|
|
432
|
+
}
|
|
433
|
+
|
|
405
434
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
406
435
|
// MAIN SCAN FUNCTION - ROUTE INTEGRITY SYSTEM
|
|
407
436
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -426,8 +455,8 @@ async function runScan(args) {
|
|
|
426
455
|
}
|
|
427
456
|
// Network error - fall back to free tier only (SECURITY: never grant paid features offline)
|
|
428
457
|
if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND' || err.name === 'NetworkError') {
|
|
429
|
-
console.warn(` ${
|
|
430
|
-
console.warn(` ${
|
|
458
|
+
console.warn(` ${colors.warning}⚠${ansi.reset} API unavailable, running in ${colors.success}FREE${ansi.reset} tier mode`);
|
|
459
|
+
console.warn(` ${ansi.dim}Paid features require API connection. Continuing with free features only.${ansi.reset}\n`);
|
|
431
460
|
// Continue with free tier features only - scan command is free tier
|
|
432
461
|
} else {
|
|
433
462
|
throw err; // Re-throw unexpected errors
|
|
@@ -464,19 +493,22 @@ async function runScan(args) {
|
|
|
464
493
|
if (layers.reality) layerNames.push('Reality');
|
|
465
494
|
if (layers.realitySniff) layerNames.push('Reality Sniff');
|
|
466
495
|
|
|
467
|
-
console.log(` ${
|
|
468
|
-
console.log(` ${
|
|
469
|
-
console.log(` ${
|
|
496
|
+
console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${projectName}${ansi.reset}`);
|
|
497
|
+
console.log(` ${ansi.dim}Path:${ansi.reset} ${projectPath}`);
|
|
498
|
+
console.log(` ${ansi.dim}Layers:${ansi.reset} ${colors.accent}${layerNames.join(' → ')}${ansi.reset}`);
|
|
470
499
|
console.log();
|
|
471
500
|
|
|
472
501
|
// Reality layer requires URL
|
|
473
502
|
if (opts.reality && !opts.baseUrl) {
|
|
474
|
-
console.log(` ${
|
|
475
|
-
console.log(` ${
|
|
503
|
+
console.log(` ${colors.warning}⚠${ansi.reset} ${ansi.bold}Reality layer requires --url${ansi.reset}`);
|
|
504
|
+
console.log(` ${ansi.dim}Example: vibecheck scan --reality --url http://localhost:3000${ansi.reset}`);
|
|
476
505
|
console.log();
|
|
477
506
|
return 1;
|
|
478
507
|
}
|
|
479
508
|
|
|
509
|
+
// Initialize spinner outside try block for error handling
|
|
510
|
+
let spinner = null;
|
|
511
|
+
|
|
480
512
|
try {
|
|
481
513
|
// Import systems - try TypeScript compiled first, fallback to JS runtime
|
|
482
514
|
let scanRouteIntegrity;
|
|
@@ -539,7 +571,7 @@ async function runScan(args) {
|
|
|
539
571
|
}
|
|
540
572
|
|
|
541
573
|
// Try to import new unified output system (may not be compiled yet)
|
|
542
|
-
let buildVerdictOutput, normalizeFinding, formatStandardOutput,
|
|
574
|
+
let buildVerdictOutput, normalizeFinding, formatStandardOutput, formatScanOutputFromUnified, getExitCodeFromUnified, CacheManager;
|
|
543
575
|
let useUnifiedOutput = false;
|
|
544
576
|
|
|
545
577
|
try {
|
|
@@ -549,8 +581,8 @@ async function runScan(args) {
|
|
|
549
581
|
formatStandardOutput = outputContract.formatStandardOutput;
|
|
550
582
|
|
|
551
583
|
const unifiedOutput = require('./lib/unified-output');
|
|
552
|
-
|
|
553
|
-
|
|
584
|
+
formatScanOutputFromUnified = unifiedOutput.formatScanOutput;
|
|
585
|
+
getExitCodeFromUnified = unifiedOutput.getExitCode;
|
|
554
586
|
|
|
555
587
|
const cacheModule = require('../../dist/lib/cli/cache-manager');
|
|
556
588
|
CacheManager = cacheModule.CacheManager;
|
|
@@ -558,7 +590,7 @@ async function runScan(args) {
|
|
|
558
590
|
} catch (error) {
|
|
559
591
|
// Fallback to old system if new one not available
|
|
560
592
|
if (opts.verbose) {
|
|
561
|
-
console.warn('Unified output system not available, using
|
|
593
|
+
console.warn('Unified output system not available, using enhanced format');
|
|
562
594
|
}
|
|
563
595
|
useUnifiedOutput = false;
|
|
564
596
|
}
|
|
@@ -594,17 +626,18 @@ async function runScan(args) {
|
|
|
594
626
|
return getExitCode(verdict);
|
|
595
627
|
}
|
|
596
628
|
|
|
597
|
-
console.log(formatScanOutput({ verdict, findings: cachedResult.findings }, { verbose: opts.verbose
|
|
629
|
+
console.log(formatScanOutput({ verdict, findings: cachedResult.findings, cached }, { verbose: opts.verbose }));
|
|
598
630
|
return getExitCode(verdict);
|
|
599
631
|
}
|
|
600
632
|
}
|
|
601
633
|
}
|
|
602
634
|
|
|
603
|
-
// Start scanning with spinner
|
|
635
|
+
// Start scanning with enhanced spinner
|
|
604
636
|
const timings = { discovery: 0, analysis: 0, verification: 0, detection: 0, total: 0 };
|
|
605
637
|
timings.discovery = Date.now();
|
|
606
638
|
|
|
607
|
-
|
|
639
|
+
spinner = new Spinner({ color: colors.primary });
|
|
640
|
+
spinner.start('Analyzing codebase...');
|
|
608
641
|
|
|
609
642
|
const result = await scanRouteIntegrity({
|
|
610
643
|
projectPath,
|
|
@@ -612,8 +645,8 @@ async function runScan(args) {
|
|
|
612
645
|
baseUrl: opts.baseUrl,
|
|
613
646
|
verbose: opts.verbose,
|
|
614
647
|
onProgress: opts.verbose ? (phase, progress) => {
|
|
615
|
-
|
|
616
|
-
if (progress < 100)
|
|
648
|
+
spinner.succeed(`${phase}: ${Math.round(progress)}%`);
|
|
649
|
+
if (progress < 100) spinner.start(`Running ${phase}...`);
|
|
617
650
|
} : undefined,
|
|
618
651
|
});
|
|
619
652
|
|
|
@@ -689,24 +722,24 @@ async function runScan(args) {
|
|
|
689
722
|
}
|
|
690
723
|
|
|
691
724
|
timings.detection = Date.now() - detectionStart;
|
|
692
|
-
|
|
725
|
+
spinner.succeed(`Detection complete (${detectionFindings.length} findings)`);
|
|
693
726
|
} catch (detectionError) {
|
|
694
727
|
// Detection engines not compiled yet - continue without them
|
|
695
728
|
if (opts.verbose) {
|
|
696
|
-
console.log(` ${
|
|
729
|
+
console.log(` ${ansi.dim}Detection engines not available: ${detectionError.message}${ansi.reset}`);
|
|
697
730
|
}
|
|
698
|
-
|
|
731
|
+
spinner.warn('Detection skipped (not compiled)');
|
|
699
732
|
}
|
|
700
733
|
|
|
701
734
|
timings.verification = Date.now() - timings.analysis - timings.discovery;
|
|
702
735
|
timings.total = Date.now() - startTime;
|
|
703
736
|
|
|
704
|
-
|
|
737
|
+
spinner.succeed('Analysis complete');
|
|
705
738
|
|
|
706
739
|
const { report, outputPaths } = result;
|
|
707
740
|
|
|
708
|
-
// Use new unified output if available, otherwise fallback to
|
|
709
|
-
if (useUnifiedOutput && buildVerdictOutput && normalizeFinding) {
|
|
741
|
+
// Use new unified output if available, otherwise fallback to enhanced format
|
|
742
|
+
if (useUnifiedOutput && buildVerdictOutput && normalizeFinding && formatScanOutputFromUnified) {
|
|
710
743
|
// Normalize findings with stable IDs
|
|
711
744
|
const existingIDs = new Set();
|
|
712
745
|
const normalizedFindings = [];
|
|
@@ -765,22 +798,34 @@ async function runScan(args) {
|
|
|
765
798
|
// JSON output mode
|
|
766
799
|
if (opts.json) {
|
|
767
800
|
console.log(JSON.stringify(standardOutput, null, 2));
|
|
768
|
-
return getExitCode(verdict);
|
|
801
|
+
return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
|
|
769
802
|
}
|
|
770
803
|
|
|
771
804
|
// SARIF output mode
|
|
772
805
|
if (opts.sarif) {
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
806
|
+
const sarif = formatSARIF(normalizedFindings, {
|
|
807
|
+
projectPath,
|
|
808
|
+
version: require('../../package.json').version || '1.0.0'
|
|
809
|
+
});
|
|
810
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
811
|
+
return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
|
|
776
812
|
}
|
|
777
813
|
|
|
778
814
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
779
|
-
//
|
|
815
|
+
// ENHANCED OUTPUT
|
|
780
816
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
781
817
|
|
|
782
|
-
// Use
|
|
783
|
-
|
|
818
|
+
// Use enhanced output formatter (from scan-output.js)
|
|
819
|
+
const resultForOutput = {
|
|
820
|
+
verdict,
|
|
821
|
+
findings: normalizedFindings,
|
|
822
|
+
layers: report.layers || [],
|
|
823
|
+
coverage: report.coverageMap,
|
|
824
|
+
breakdown: report.score?.breakdown,
|
|
825
|
+
timings,
|
|
826
|
+
cached,
|
|
827
|
+
};
|
|
828
|
+
console.log(formatScanOutput(resultForOutput, { verbose: opts.verbose }));
|
|
784
829
|
|
|
785
830
|
// Additional details if verbose
|
|
786
831
|
if (opts.verbose) {
|
|
@@ -797,6 +842,66 @@ async function runScan(args) {
|
|
|
797
842
|
}
|
|
798
843
|
}
|
|
799
844
|
|
|
845
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
846
|
+
// SAVE RESULTS
|
|
847
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
848
|
+
|
|
849
|
+
if (opts.save) {
|
|
850
|
+
const resultsDir = path.join(projectPath, '.vibecheck', 'results');
|
|
851
|
+
if (!fs.existsSync(resultsDir)) {
|
|
852
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Save latest.json
|
|
856
|
+
const latestPath = path.join(resultsDir, 'latest.json');
|
|
857
|
+
fs.writeFileSync(latestPath, JSON.stringify(standardOutput, null, 2));
|
|
858
|
+
|
|
859
|
+
// Save timestamped copy
|
|
860
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
861
|
+
const historyDir = path.join(resultsDir, 'history');
|
|
862
|
+
if (!fs.existsSync(historyDir)) {
|
|
863
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
864
|
+
}
|
|
865
|
+
fs.writeFileSync(path.join(historyDir, `scan-${timestamp}.json`), JSON.stringify(standardOutput, null, 2));
|
|
866
|
+
|
|
867
|
+
if (!opts.json) {
|
|
868
|
+
console.log(`\n ${ansi.dim}Results saved to: ${latestPath}${ansi.reset}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
873
|
+
// AUTOFIX MODE - Generate missions
|
|
874
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
875
|
+
|
|
876
|
+
if (opts.autofix && normalizedFindings.length > 0) {
|
|
877
|
+
// Check entitlement
|
|
878
|
+
const entitlementsV2 = require("./lib/entitlements-v2");
|
|
879
|
+
const access = await entitlementsV2.enforce("scan.autofix", {
|
|
880
|
+
projectPath,
|
|
881
|
+
silent: false,
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
if (!access.allowed) {
|
|
885
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.bold}--autofix requires STARTER plan${ansi.reset}`);
|
|
886
|
+
console.log(` ${ansi.dim}Upgrade at: https://vibecheckai.dev/pricing${ansi.reset}`);
|
|
887
|
+
console.log(` ${ansi.dim}Scan results saved. Run 'vibecheck fix' for manual mission generation.${ansi.reset}\n`);
|
|
888
|
+
} else {
|
|
889
|
+
console.log(`\n ${colors.accent}${icons.lightning}${ansi.reset} ${ansi.bold}Generating AI missions...${ansi.reset}\n`);
|
|
890
|
+
|
|
891
|
+
const { missions, summary } = await generateMissions(normalizedFindings, projectPath, opts);
|
|
892
|
+
|
|
893
|
+
console.log(` ${colors.success}✓${ansi.reset} Generated ${missions.length} missions`);
|
|
894
|
+
console.log(` ${ansi.dim}Critical: ${summary.bySeverity.critical}${ansi.reset}`);
|
|
895
|
+
console.log(` ${ansi.dim}High: ${summary.bySeverity.high}${ansi.reset}`);
|
|
896
|
+
console.log(` ${ansi.dim}Medium: ${summary.bySeverity.medium}${ansi.reset}`);
|
|
897
|
+
console.log(` ${ansi.dim}Low: ${summary.bySeverity.low}${ansi.reset}`);
|
|
898
|
+
console.log();
|
|
899
|
+
console.log(` ${ansi.dim}Missions saved to: .vibecheck/missions/${ansi.reset}`);
|
|
900
|
+
console.log(` ${ansi.dim}Open MISSIONS.md for prompts to use with your AI assistant.${ansi.reset}`);
|
|
901
|
+
console.log();
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
800
905
|
// Emit audit event for scan complete
|
|
801
906
|
emitScanComplete(projectPath, verdict.verdict === 'PASS' ? 'success' : 'failure', {
|
|
802
907
|
score: report.score?.overall || (verdict.verdict === 'PASS' ? 100 : 50),
|
|
@@ -805,7 +910,7 @@ async function runScan(args) {
|
|
|
805
910
|
durationMs: timings.total,
|
|
806
911
|
});
|
|
807
912
|
|
|
808
|
-
return getExitCode(verdict);
|
|
913
|
+
return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
|
|
809
914
|
} else {
|
|
810
915
|
// Legacy fallback output when unified output system isn't available
|
|
811
916
|
const findings = [...(report.shipBlockers || []), ...detectionFindings];
|
|
@@ -814,40 +919,32 @@ async function runScan(args) {
|
|
|
814
919
|
|
|
815
920
|
const verdict = criticalCount > 0 ? 'BLOCK' : warningCount > 0 ? 'WARN' : 'SHIP';
|
|
816
921
|
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
} else {
|
|
826
|
-
console.log(` ${c.bgRed}${c.white}${c.bold} ✗ BLOCK ${c.reset} ${c.red}Issues must be fixed before shipping${c.reset}`);
|
|
827
|
-
}
|
|
922
|
+
// Use enhanced output formatter for legacy fallback
|
|
923
|
+
const severityCounts = {
|
|
924
|
+
critical: criticalCount,
|
|
925
|
+
high: 0,
|
|
926
|
+
medium: warningCount,
|
|
927
|
+
low: findings.length - criticalCount - warningCount,
|
|
928
|
+
};
|
|
929
|
+
const score = calculateScore(severityCounts);
|
|
828
930
|
|
|
829
|
-
|
|
830
|
-
|
|
931
|
+
const result = {
|
|
932
|
+
verdict: { verdict, score },
|
|
933
|
+
findings: findings.map(f => ({
|
|
934
|
+
severity: f.severity === 'critical' || f.severity === 'BLOCK' ? 'critical' :
|
|
935
|
+
f.severity === 'warning' || f.severity === 'WARN' ? 'medium' : 'low',
|
|
936
|
+
category: f.category || 'ROUTE',
|
|
937
|
+
title: f.title || f.message,
|
|
938
|
+
message: f.message || f.title,
|
|
939
|
+
file: f.file,
|
|
940
|
+
line: f.line,
|
|
941
|
+
fix: f.fixSuggestion,
|
|
942
|
+
})),
|
|
943
|
+
layers: [],
|
|
944
|
+
timings,
|
|
945
|
+
};
|
|
831
946
|
|
|
832
|
-
|
|
833
|
-
console.log(` ${c.bold}Findings (${findings.length})${c.reset}`);
|
|
834
|
-
console.log();
|
|
835
|
-
|
|
836
|
-
for (const finding of findings.slice(0, 10)) {
|
|
837
|
-
const severityIcon = finding.severity === 'critical' || finding.severity === 'BLOCK'
|
|
838
|
-
? `${c.red}✗${c.reset}`
|
|
839
|
-
: `${c.yellow}⚠${c.reset}`;
|
|
840
|
-
console.log(` ${severityIcon} ${finding.title || finding.message}`);
|
|
841
|
-
if (finding.file) {
|
|
842
|
-
console.log(` ${c.dim}${finding.file}${finding.line ? `:${finding.line}` : ''}${c.reset}`);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (findings.length > 10) {
|
|
847
|
-
console.log(` ${c.dim}... and ${findings.length - 10} more findings${c.reset}`);
|
|
848
|
-
}
|
|
849
|
-
console.log();
|
|
850
|
-
}
|
|
947
|
+
console.log(formatScanOutput(result, { verbose: opts.verbose }));
|
|
851
948
|
|
|
852
949
|
// Emit audit event
|
|
853
950
|
emitScanComplete(projectPath, verdict === 'SHIP' ? 'success' : 'failure', {
|
|
@@ -860,10 +957,11 @@ async function runScan(args) {
|
|
|
860
957
|
}
|
|
861
958
|
|
|
862
959
|
} catch (error) {
|
|
863
|
-
|
|
960
|
+
if (spinner) {
|
|
961
|
+
spinner.fail(`Scan failed: ${error.message}`);
|
|
962
|
+
}
|
|
864
963
|
|
|
865
|
-
// Use
|
|
866
|
-
const { printError, EXIT_CODES } = require('./lib/unified-output');
|
|
964
|
+
// Use enhanced error handling
|
|
867
965
|
const exitCode = printError(error, 'Scan');
|
|
868
966
|
|
|
869
967
|
// Emit audit event for scan error
|