@vibecheckai/cli 3.1.2 → 3.1.5
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 +1157 -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 +299 -202
- 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/runReport.js
CHANGED
|
@@ -1,63 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vibecheck report - World-Class
|
|
2
|
+
* vibecheck report - World-Class Enterprise Reports
|
|
3
3
|
*
|
|
4
4
|
* TIER ENFORCEMENT:
|
|
5
5
|
* - FREE: HTML, MD formats only
|
|
6
6
|
* - STARTER: + SARIF, CSV formats
|
|
7
|
-
* - PRO: + compliance packs, redaction templates
|
|
7
|
+
* - PRO: + compliance packs, PDF export, redaction templates
|
|
8
8
|
*
|
|
9
9
|
* Enterprise-grade report generation with:
|
|
10
10
|
* - Beautiful interactive HTML with modern design
|
|
11
|
-
* - Multiple export formats (HTML, MD, JSON, SARIF, CSV)
|
|
11
|
+
* - Multiple export formats (HTML, MD, JSON, SARIF, CSV, PDF)
|
|
12
12
|
* - Executive, Technical, Compliance report types
|
|
13
13
|
* - Historical trends and fix time estimates
|
|
14
14
|
* - Print-optimized layouts
|
|
15
|
+
* - White-label customization
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
const path = require("path");
|
|
18
19
|
const fs = require("fs");
|
|
19
20
|
|
|
20
21
|
// Entitlements enforcement
|
|
21
|
-
|
|
22
|
+
let entitlements;
|
|
23
|
+
try {
|
|
24
|
+
entitlements = require("./lib/entitlements-v2");
|
|
25
|
+
} catch {
|
|
26
|
+
// Fallback: allow all features if entitlements not available
|
|
27
|
+
entitlements = {
|
|
28
|
+
getLimits: () => ({ reportFormats: ["html", "md", "json", "sarif", "csv", "pdf"] }),
|
|
29
|
+
getTier: async () => "pro",
|
|
30
|
+
enforce: async () => ({ allowed: true }),
|
|
31
|
+
EXIT_FEATURE_NOT_ALLOWED: 1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
22
34
|
|
|
23
|
-
// Report
|
|
24
|
-
let reportEngine, reportHtml;
|
|
35
|
+
// Report modules
|
|
36
|
+
let reportEngine, reportHtml, reportTemplates;
|
|
25
37
|
try {
|
|
26
38
|
reportEngine = require("./lib/report-engine");
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error("Warning: report-engine not found:", e.message);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
27
43
|
reportHtml = require("./lib/report-html");
|
|
28
44
|
} catch (e) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
console.error("Warning: report-html not found:", e.message);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
reportTemplates = require("./lib/report-templates");
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error("Warning: report-templates not found:", e.message);
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
54
|
+
// ENHANCED TERMINAL UI & OUTPUT MODULES
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
ansi,
|
|
59
|
+
colors,
|
|
60
|
+
icons,
|
|
61
|
+
Spinner,
|
|
62
|
+
renderSection,
|
|
63
|
+
formatDuration,
|
|
64
|
+
} = require("./lib/terminal-ui");
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
BANNER,
|
|
68
|
+
formatHelp,
|
|
69
|
+
renderReportInfo,
|
|
70
|
+
renderSuccess,
|
|
71
|
+
renderError,
|
|
72
|
+
} = require("./lib/report-output");
|
|
46
73
|
|
|
47
74
|
function parseArgs(args) {
|
|
48
75
|
const opts = {
|
|
49
|
-
type: "
|
|
76
|
+
type: "technical",
|
|
50
77
|
format: "html",
|
|
51
78
|
path: ".",
|
|
52
79
|
output: null,
|
|
53
80
|
logo: null,
|
|
54
81
|
company: null,
|
|
55
82
|
theme: "dark",
|
|
83
|
+
framework: "SOC2",
|
|
56
84
|
includeVerify: false,
|
|
57
85
|
includeTrends: false,
|
|
58
86
|
redactPaths: false,
|
|
59
87
|
maxFindings: 50,
|
|
88
|
+
open: false,
|
|
60
89
|
help: false,
|
|
90
|
+
quiet: false,
|
|
61
91
|
};
|
|
62
92
|
|
|
63
93
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -68,6 +98,7 @@ function parseArgs(args) {
|
|
|
68
98
|
if (a === "--logo") opts.logo = args[++i];
|
|
69
99
|
if (a === "--company") opts.company = args[++i];
|
|
70
100
|
if (a === "--theme") opts.theme = args[++i];
|
|
101
|
+
if (a === "--framework") opts.framework = args[++i];
|
|
71
102
|
if (a === "--light") opts.theme = "light";
|
|
72
103
|
if (a === "--dark") opts.theme = "dark";
|
|
73
104
|
if (a === "--include-verify") opts.includeVerify = true;
|
|
@@ -76,6 +107,8 @@ function parseArgs(args) {
|
|
|
76
107
|
if (a === "--max-findings") opts.maxFindings = parseInt(args[++i]) || 50;
|
|
77
108
|
if (a.startsWith("--path=")) opts.path = a.split("=")[1];
|
|
78
109
|
if (a === "--path" || a === "-p") opts.path = args[++i];
|
|
110
|
+
if (a === "--open") opts.open = true;
|
|
111
|
+
if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
79
112
|
if (a === "--help" || a === "-h") opts.help = true;
|
|
80
113
|
}
|
|
81
114
|
|
|
@@ -83,60 +116,8 @@ function parseArgs(args) {
|
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
function printHelp() {
|
|
86
|
-
console.log(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
${c.dim}Generate beautiful, professional reports for stakeholders.${c.reset}
|
|
90
|
-
|
|
91
|
-
${c.bold}USAGE${c.reset}
|
|
92
|
-
vibecheck report Generate executive HTML report
|
|
93
|
-
vibecheck report --format=md Generate Markdown report
|
|
94
|
-
vibecheck report --format=sarif Generate SARIF for security tools
|
|
95
|
-
vibecheck report --type=compliance Generate compliance report
|
|
96
|
-
|
|
97
|
-
${c.bold}REPORT TYPES${c.reset}
|
|
98
|
-
${c.cyan}executive${c.reset} One-page overview for stakeholders (default)
|
|
99
|
-
${c.cyan}technical${c.reset} Detailed findings for developers/CTOs
|
|
100
|
-
${c.cyan}compliance${c.reset} SOC2/HIPAA-ready language for regulated industries
|
|
101
|
-
${c.cyan}trend${c.reset} Historical score analysis over time
|
|
102
|
-
|
|
103
|
-
${c.bold}OUTPUT FORMATS${c.reset}
|
|
104
|
-
${c.green}html${c.reset} Beautiful interactive report (default)
|
|
105
|
-
${c.green}md${c.reset} Markdown for documentation/GitHub
|
|
106
|
-
${c.green}json${c.reset} Machine-readable JSON
|
|
107
|
-
${c.green}sarif${c.reset} SARIF for security tool integration
|
|
108
|
-
${c.green}csv${c.reset} CSV for spreadsheet analysis
|
|
109
|
-
|
|
110
|
-
${c.bold}OPTIONS${c.reset}
|
|
111
|
-
--type, -t <type> Report type: executive, technical, compliance, trend
|
|
112
|
-
--format, -f <format> Output format: html, md, json, sarif, csv
|
|
113
|
-
--output, -o <path> Output file path
|
|
114
|
-
--theme <dark|light> HTML theme (default: dark)
|
|
115
|
-
--company <name> Company name for branding
|
|
116
|
-
--logo <path> Custom logo path/URL
|
|
117
|
-
--include-trends Include historical trend data
|
|
118
|
-
--redact-paths Hide file paths for client reports
|
|
119
|
-
--max-findings <n> Max findings to show (default: 50)
|
|
120
|
-
--path, -p <dir> Project path (default: current directory)
|
|
121
|
-
--help, -h Show this help
|
|
122
|
-
|
|
123
|
-
${c.bold}EXAMPLES${c.reset}
|
|
124
|
-
vibecheck report # Interactive HTML
|
|
125
|
-
vibecheck report --format=md --output=REPORT.md # Markdown file
|
|
126
|
-
vibecheck report --type=compliance --company=Acme # Compliance report
|
|
127
|
-
vibecheck report --format=sarif -o report.sarif # Security tooling
|
|
128
|
-
vibecheck report --theme=light --redact # Client-safe HTML
|
|
129
|
-
vibecheck report --trends # Include history
|
|
130
|
-
|
|
131
|
-
${c.bold}OUTPUT${c.reset}
|
|
132
|
-
HTML reports include:
|
|
133
|
-
• Animated score visualization
|
|
134
|
-
• Interactive severity charts
|
|
135
|
-
• Category breakdown bars
|
|
136
|
-
• Fix time estimates
|
|
137
|
-
• Dark/light mode toggle
|
|
138
|
-
• Print-optimized layout
|
|
139
|
-
`);
|
|
119
|
+
console.log(BANNER);
|
|
120
|
+
console.log(formatHelp());
|
|
140
121
|
}
|
|
141
122
|
|
|
142
123
|
async function runReport(args) {
|
|
@@ -151,26 +132,39 @@ async function runReport(args) {
|
|
|
151
132
|
const outputDir = path.join(projectPath, ".vibecheck");
|
|
152
133
|
const projectName = path.basename(projectPath);
|
|
153
134
|
|
|
154
|
-
// TIER ENFORCEMENT
|
|
135
|
+
// TIER ENFORCEMENT
|
|
155
136
|
const format = opts.format.toLowerCase();
|
|
156
|
-
const
|
|
137
|
+
const tier = await entitlements.getTier({ projectPath });
|
|
138
|
+
const limits = entitlements.getLimits(tier);
|
|
157
139
|
const allowedFormats = limits.reportFormats || ["html", "md"];
|
|
158
|
-
|
|
159
|
-
// Check
|
|
140
|
+
|
|
141
|
+
// Check format access
|
|
160
142
|
if (!allowedFormats.includes(format) && format !== "json") {
|
|
161
|
-
// SARIF/CSV require STARTER+
|
|
162
143
|
if (format === "sarif" || format === "csv") {
|
|
163
144
|
const access = await entitlements.enforce("report.sarif_csv", {
|
|
164
145
|
projectPath,
|
|
165
146
|
silent: false,
|
|
166
147
|
});
|
|
167
148
|
if (!access.allowed) {
|
|
168
|
-
console.log(`\n${
|
|
149
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}HTML and MD formats are available on FREE tier${ansi.reset}`);
|
|
150
|
+
console.log(` ${ansi.dim}Upgrade to STARTER for SARIF/CSV export${ansi.reset}\n`);
|
|
151
|
+
return entitlements.EXIT_FEATURE_NOT_ALLOWED;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (format === "pdf") {
|
|
156
|
+
const access = await entitlements.enforce("report.pdf_export", {
|
|
157
|
+
projectPath,
|
|
158
|
+
silent: false,
|
|
159
|
+
});
|
|
160
|
+
if (!access.allowed) {
|
|
161
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}PDF export requires PRO tier${ansi.reset}`);
|
|
162
|
+
console.log(` ${ansi.dim}HTML reports can be printed to PDF from browser${ansi.reset}\n`);
|
|
169
163
|
return entitlements.EXIT_FEATURE_NOT_ALLOWED;
|
|
170
164
|
}
|
|
171
165
|
}
|
|
172
166
|
}
|
|
173
|
-
|
|
167
|
+
|
|
174
168
|
// Compliance reports require PRO
|
|
175
169
|
if (opts.type === "compliance") {
|
|
176
170
|
const access = await entitlements.enforce("report.compliance_packs", {
|
|
@@ -178,23 +172,35 @@ async function runReport(args) {
|
|
|
178
172
|
silent: false,
|
|
179
173
|
});
|
|
180
174
|
if (!access.allowed) {
|
|
181
|
-
console.log(`\n${
|
|
175
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}Executive and technical reports available on lower tiers${ansi.reset}`);
|
|
182
176
|
return entitlements.EXIT_FEATURE_NOT_ALLOWED;
|
|
183
177
|
}
|
|
184
178
|
}
|
|
185
179
|
|
|
186
|
-
|
|
180
|
+
// Display banner and report info
|
|
181
|
+
if (!opts.quiet) {
|
|
182
|
+
console.log(BANNER);
|
|
183
|
+
console.log(renderReportInfo(opts.type, opts.format, null));
|
|
184
|
+
console.log('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Progress spinner
|
|
188
|
+
const spinner = new Spinner({ color: colors.accent });
|
|
189
|
+
spinner.start(`Generating ${opts.type} report`);
|
|
187
190
|
|
|
188
|
-
// Load ship results
|
|
191
|
+
// Load ship results
|
|
189
192
|
let shipResults = loadShipResults(projectPath, outputDir);
|
|
190
193
|
|
|
191
194
|
if (!shipResults) {
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
spinner.warn("No scan results found - using demo data");
|
|
196
|
+
if (!opts.quiet) {
|
|
197
|
+
console.log(` ${ansi.dim}Tip: Run 'vibecheck ship' first for real results.${ansi.reset}\n`);
|
|
198
|
+
}
|
|
194
199
|
shipResults = getDemoData();
|
|
195
200
|
}
|
|
196
201
|
|
|
197
|
-
// Build comprehensive report data
|
|
202
|
+
// Build comprehensive report data
|
|
203
|
+
spinner.update("Processing findings");
|
|
198
204
|
let reportData;
|
|
199
205
|
if (reportEngine) {
|
|
200
206
|
reportData = reportEngine.buildReportData(shipResults, {
|
|
@@ -203,56 +209,92 @@ async function runReport(args) {
|
|
|
203
209
|
includeTrends: opts.includeTrends,
|
|
204
210
|
});
|
|
205
211
|
} else {
|
|
206
|
-
// Fallback to basic report data
|
|
207
212
|
reportData = buildBasicReportData(shipResults, projectName);
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
// Generate report content
|
|
215
|
+
// Generate report content
|
|
216
|
+
spinner.update(`Rendering ${format.toUpperCase()} output`);
|
|
211
217
|
let reportContent = "";
|
|
212
|
-
let fileExtension =
|
|
218
|
+
let fileExtension = format;
|
|
213
219
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
220
|
+
try {
|
|
221
|
+
switch (format) {
|
|
222
|
+
case "html":
|
|
223
|
+
// Debug: Log which HTML generator will be used
|
|
224
|
+
if (opts.verbose || opts.type === "technical") {
|
|
225
|
+
if (reportHtml && typeof reportHtml.generateWorldClassHTML === 'function') {
|
|
226
|
+
console.log(` ${ansi.dim}Using world-class HTML generator${ansi.reset}`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(` ${ansi.dim}Using basic HTML generator (report-html module not available)${ansi.reset}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
reportContent = generateHTMLReport(reportData, opts);
|
|
232
|
+
break;
|
|
233
|
+
case "md":
|
|
234
|
+
case "markdown":
|
|
235
|
+
reportContent = generateMarkdownReport(reportData, opts);
|
|
236
|
+
fileExtension = "md";
|
|
237
|
+
break;
|
|
238
|
+
case "json":
|
|
239
|
+
reportContent = generateJSONReport(reportData, opts);
|
|
240
|
+
break;
|
|
241
|
+
case "sarif":
|
|
242
|
+
reportContent = generateSARIFReport(reportData, opts);
|
|
243
|
+
break;
|
|
244
|
+
case "csv":
|
|
245
|
+
reportContent = generateCSVReport(reportData, opts);
|
|
246
|
+
break;
|
|
247
|
+
case "pdf":
|
|
248
|
+
// PDF requires HTML first, then conversion
|
|
249
|
+
reportContent = await generatePDFReport(reportData, opts, outputDir);
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
spinner.fail(`Unknown format: ${format}`);
|
|
253
|
+
console.log(` ${ansi.dim}Supported: html, md, json, sarif, csv, pdf${ansi.reset}`);
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
spinner.fail(`Failed to generate report: ${err.message}`);
|
|
258
|
+
return 1;
|
|
236
259
|
}
|
|
237
260
|
|
|
238
261
|
// Determine output path
|
|
239
262
|
const outputFileName = opts.output || path.join(outputDir, `report.${fileExtension}`);
|
|
263
|
+
const outputDirPath = path.dirname(outputFileName);
|
|
240
264
|
|
|
241
265
|
// Ensure output directory exists
|
|
242
|
-
const outputDirPath = path.dirname(outputFileName);
|
|
243
266
|
if (!fs.existsSync(outputDirPath)) {
|
|
244
267
|
fs.mkdirSync(outputDirPath, { recursive: true });
|
|
245
268
|
}
|
|
246
269
|
|
|
247
|
-
// Write report
|
|
248
|
-
|
|
270
|
+
// Write report (always overwrite to ensure fresh content)
|
|
271
|
+
spinner.update("Writing report");
|
|
272
|
+
fs.writeFileSync(outputFileName, reportContent, 'utf8');
|
|
273
|
+
|
|
274
|
+
// If this is the default report.html, also update the timestamp to help with cache busting
|
|
275
|
+
if (!opts.output && fileExtension === 'html') {
|
|
276
|
+
try {
|
|
277
|
+
// Touch the file to update its modification time
|
|
278
|
+
const now = new Date();
|
|
279
|
+
fs.utimesSync(outputFileName, now, now);
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore errors on utimes
|
|
282
|
+
}
|
|
283
|
+
}
|
|
249
284
|
|
|
250
285
|
// Save report history for trend analysis
|
|
251
286
|
saveReportHistory(outputDir, reportData);
|
|
252
287
|
|
|
253
|
-
|
|
254
|
-
console.log(
|
|
255
|
-
|
|
288
|
+
spinner.succeed("Report generated");
|
|
289
|
+
console.log("");
|
|
290
|
+
|
|
291
|
+
// Print success message using design system
|
|
292
|
+
console.log(renderSuccess(outputFileName, opts.format));
|
|
293
|
+
|
|
294
|
+
// Open in browser if requested
|
|
295
|
+
if (opts.open && format === "html") {
|
|
296
|
+
openInBrowser(outputFileName);
|
|
297
|
+
}
|
|
256
298
|
|
|
257
299
|
return 0;
|
|
258
300
|
}
|
|
@@ -262,7 +304,6 @@ async function runReport(args) {
|
|
|
262
304
|
// ============================================================================
|
|
263
305
|
|
|
264
306
|
function loadShipResults(projectPath, outputDir) {
|
|
265
|
-
// Try multiple sources for ship results
|
|
266
307
|
const sources = [
|
|
267
308
|
path.join(outputDir, "ship_report.json"),
|
|
268
309
|
path.join(outputDir, "report.json"),
|
|
@@ -286,7 +327,7 @@ function loadShipResults(projectPath, outputDir) {
|
|
|
286
327
|
path.join(outputDir, "reality", "last_reality.json"),
|
|
287
328
|
path.join(outputDir, "reality_report.json"),
|
|
288
329
|
];
|
|
289
|
-
|
|
330
|
+
|
|
290
331
|
for (const src of realitySources) {
|
|
291
332
|
if (fs.existsSync(src)) {
|
|
292
333
|
try {
|
|
@@ -311,29 +352,21 @@ function getDemoData() {
|
|
|
311
352
|
score: 72,
|
|
312
353
|
verdict: "WARN",
|
|
313
354
|
findings: [
|
|
314
|
-
{ id: "SEC001", severity: "BLOCK", type: "secret", message: "API key exposed in source code", file: "src/config.ts", line: 42, fix: "Move to environment variable" },
|
|
315
|
-
{ id: "AUTH001", severity: "BLOCK", type: "auth", message: "Authentication bypass in admin route", file: "src/routes/admin.ts", line: 15, fix: "Add auth middleware" },
|
|
316
|
-
{ id: "MOCK001", severity: "WARN", type: "mock", message: "Mock data used in production path", file: "src/api/users.ts", line: 88, fix: "Remove mock data fallback" },
|
|
317
|
-
{ id: "ERR001", severity: "WARN", type: "error", message: "Empty catch block in payment flow", file: "src/billing/checkout.ts", line: 156, fix: "Add error handling" },
|
|
318
|
-
{ id: "CFG001", severity: "INFO", type: "config", message: "Hardcoded timeout value", file: "src/utils/http.ts", line: 23, fix: "Move to config" },
|
|
319
|
-
{ id: "DBG001", severity: "INFO", type: "quality", message: "Console.log statement", file: "src/debug.ts", line: 5, fix: "Remove or use logger" },
|
|
355
|
+
{ id: "SEC001", severity: "BLOCK", type: "secret", message: "API key exposed in source code", title: "Exposed API Key", file: "src/config.ts", line: 42, fix: "Move to environment variable" },
|
|
356
|
+
{ id: "AUTH001", severity: "BLOCK", type: "auth", message: "Authentication bypass in admin route", title: "Auth Bypass Vulnerability", file: "src/routes/admin.ts", line: 15, fix: "Add auth middleware" },
|
|
357
|
+
{ id: "MOCK001", severity: "WARN", type: "mock", message: "Mock data used in production path", title: "Mock Data in Production", file: "src/api/users.ts", line: 88, fix: "Remove mock data fallback" },
|
|
358
|
+
{ id: "ERR001", severity: "WARN", type: "error", message: "Empty catch block in payment flow", title: "Swallowed Exception", file: "src/billing/checkout.ts", line: 156, fix: "Add error handling" },
|
|
359
|
+
{ id: "CFG001", severity: "INFO", type: "config", message: "Hardcoded timeout value", title: "Hardcoded Configuration", file: "src/utils/http.ts", line: 23, fix: "Move to config" },
|
|
360
|
+
{ id: "DBG001", severity: "INFO", type: "quality", message: "Console.log statement", title: "Debug Statement", file: "src/debug.ts", line: 5, fix: "Remove or use logger" },
|
|
320
361
|
],
|
|
362
|
+
categoryScores: { security: 65, auth: 50, billing: 90, quality: 75 },
|
|
321
363
|
truthpack: {
|
|
322
364
|
routes: { server: Array(12).fill({}) },
|
|
323
365
|
env: { vars: Array(8).fill("") },
|
|
324
|
-
auth: { nextMiddleware: [{}] },
|
|
325
|
-
billing: { webhooks: [{}] },
|
|
326
366
|
},
|
|
327
367
|
reality: {
|
|
328
|
-
coverage: {
|
|
329
|
-
clientCallsMapped: 87,
|
|
330
|
-
runtimeRequests: 72,
|
|
331
|
-
uiActionsVerified: 95,
|
|
332
|
-
authRoutes: 100,
|
|
333
|
-
},
|
|
334
|
-
requestsOverTime: [3, 5, 2, 8, 12, 6, 4, 9, 15, 7, 3, 10],
|
|
368
|
+
coverage: { clientCallsMapped: 87, uiActionsVerified: 95, authRoutes: 100 },
|
|
335
369
|
latencyP95: 412,
|
|
336
|
-
latencySparkline: [120, 150, 180, 200, 220, 280, 320, 380, 400, 412],
|
|
337
370
|
brokenFlows: [
|
|
338
371
|
{
|
|
339
372
|
title: "User cannot complete checkout flow",
|
|
@@ -346,21 +379,6 @@ function getDemoData() {
|
|
|
346
379
|
{ type: "error", label: "500 Internal Server Error" },
|
|
347
380
|
],
|
|
348
381
|
},
|
|
349
|
-
{
|
|
350
|
-
title: "Password reset email never sent",
|
|
351
|
-
severity: "BLOCK",
|
|
352
|
-
steps: [
|
|
353
|
-
{ type: "ui", label: "Click 'Forgot Password'" },
|
|
354
|
-
{ type: "form", label: "Enter email" },
|
|
355
|
-
{ type: "api", label: "POST /api/auth/reset" },
|
|
356
|
-
{ type: "error", label: "Missing SMTP config" },
|
|
357
|
-
],
|
|
358
|
-
},
|
|
359
|
-
],
|
|
360
|
-
unmappedRequests: [
|
|
361
|
-
{ method: "GET", path: "/api/analytics", count: 3 },
|
|
362
|
-
{ method: "POST", path: "/api/tracking", count: 5 },
|
|
363
|
-
{ method: "GET", path: "/api/health", count: 12 },
|
|
364
382
|
],
|
|
365
383
|
},
|
|
366
384
|
};
|
|
@@ -369,7 +387,7 @@ function getDemoData() {
|
|
|
369
387
|
function buildBasicReportData(shipResults, projectName) {
|
|
370
388
|
const findings = shipResults?.findings || [];
|
|
371
389
|
const reality = shipResults?.reality || null;
|
|
372
|
-
|
|
390
|
+
|
|
373
391
|
return {
|
|
374
392
|
meta: {
|
|
375
393
|
projectName,
|
|
@@ -387,11 +405,11 @@ function buildBasicReportData(shipResults, projectName) {
|
|
|
387
405
|
medium: findings.filter(f => f.severity === "WARN" || f.severity === "medium").length,
|
|
388
406
|
low: findings.filter(f => f.severity === "low" || f.severity === "INFO").length,
|
|
389
407
|
},
|
|
390
|
-
categoryScores: shipResults?.categoryScores || {
|
|
408
|
+
categoryScores: shipResults?.categoryScores || {},
|
|
391
409
|
},
|
|
392
|
-
findings: findings.map((f, i) => ({ ...f, id: f.id || `F${String(i+1).padStart(3,"0")}` })),
|
|
393
|
-
fixEstimates: { humanReadable:
|
|
394
|
-
truthpack:
|
|
410
|
+
findings: findings.map((f, i) => ({ ...f, id: f.id || `F${String(i + 1).padStart(3, "0")}` })),
|
|
411
|
+
fixEstimates: { humanReadable: calculateFixTime(findings), totalMinutes: 120 },
|
|
412
|
+
truthpack: shipResults?.truthpack || null,
|
|
395
413
|
reality: reality,
|
|
396
414
|
};
|
|
397
415
|
}
|
|
@@ -401,43 +419,76 @@ function buildBasicReportData(shipResults, projectName) {
|
|
|
401
419
|
// ============================================================================
|
|
402
420
|
|
|
403
421
|
function generateHTMLReport(reportData, opts) {
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
+
// Route to appropriate template based on type
|
|
423
|
+
switch (opts.type) {
|
|
424
|
+
case "executive":
|
|
425
|
+
if (reportTemplates?.generateEnhancedExecutiveReport) {
|
|
426
|
+
return reportTemplates.generateEnhancedExecutiveReport(
|
|
427
|
+
convertToLegacyFormat(reportData),
|
|
428
|
+
opts
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
// Fall through if template not available
|
|
432
|
+
break;
|
|
433
|
+
case "compliance":
|
|
434
|
+
if (reportTemplates?.generateEnhancedComplianceReport) {
|
|
435
|
+
return reportTemplates.generateEnhancedComplianceReport(
|
|
436
|
+
convertToLegacyFormat(reportData),
|
|
437
|
+
opts
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
// Fall through if template not available
|
|
441
|
+
break;
|
|
442
|
+
default:
|
|
443
|
+
// Technical report - ALWAYS use world-class HTML generator
|
|
444
|
+
if (reportHtml && typeof reportHtml.generateWorldClassHTML === 'function') {
|
|
445
|
+
try {
|
|
446
|
+
// Ensure reportData has required structure
|
|
447
|
+
if (!reportData.meta) {
|
|
448
|
+
reportData.meta = { projectName: 'Unknown', generatedAt: new Date().toISOString(), version: '2.0.0' };
|
|
449
|
+
}
|
|
450
|
+
if (!reportData.summary) {
|
|
451
|
+
reportData.summary = { score: 0, verdict: 'WARN', totalFindings: 0, severityCounts: {} };
|
|
452
|
+
}
|
|
453
|
+
if (!reportData.findings) {
|
|
454
|
+
reportData.findings = [];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return reportHtml.generateWorldClassHTML(reportData, opts);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.error(`\n ${colors.error}${icons.error}${ansi.reset} Error generating world-class HTML: ${err.message}`);
|
|
460
|
+
if (opts.verbose) {
|
|
461
|
+
console.error(err.stack);
|
|
462
|
+
}
|
|
463
|
+
// Fall through to basic HTML only on error
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
console.warn(`\n ${colors.warning}${icons.warning}${ansi.reset} report-html module not available, using basic HTML template`);
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
422
469
|
}
|
|
470
|
+
|
|
471
|
+
// Fallback to basic HTML only if world-class generator failed or not available
|
|
472
|
+
console.warn(` ${ansi.dim}Using basic HTML fallback${ansi.reset}`);
|
|
473
|
+
return generateBasicHTML(reportData, opts);
|
|
423
474
|
}
|
|
424
475
|
|
|
425
476
|
function generateMarkdownReport(reportData, opts) {
|
|
426
|
-
if (reportEngine
|
|
477
|
+
if (reportEngine?.exportToMarkdown) {
|
|
427
478
|
return reportEngine.exportToMarkdown(reportData, opts);
|
|
428
479
|
}
|
|
429
480
|
return generateBasicMarkdown(reportData, opts);
|
|
430
481
|
}
|
|
431
482
|
|
|
432
483
|
function generateJSONReport(reportData, opts) {
|
|
433
|
-
if (reportEngine
|
|
484
|
+
if (reportEngine?.exportToJSON) {
|
|
434
485
|
return reportEngine.exportToJSON(reportData);
|
|
435
486
|
}
|
|
436
487
|
return JSON.stringify(reportData, null, 2);
|
|
437
488
|
}
|
|
438
489
|
|
|
439
490
|
function generateSARIFReport(reportData, opts) {
|
|
440
|
-
if (reportEngine
|
|
491
|
+
if (reportEngine?.exportToSARIF) {
|
|
441
492
|
return JSON.stringify(reportEngine.exportToSARIF(reportData), null, 2);
|
|
442
493
|
}
|
|
443
494
|
// Basic SARIF fallback
|
|
@@ -456,29 +507,66 @@ function generateSARIFReport(reportData, opts) {
|
|
|
456
507
|
}
|
|
457
508
|
|
|
458
509
|
function generateCSVReport(reportData, opts) {
|
|
459
|
-
if (reportEngine
|
|
510
|
+
if (reportEngine?.exportToCSV) {
|
|
460
511
|
return reportEngine.exportToCSV(reportData);
|
|
461
512
|
}
|
|
462
513
|
// Basic CSV fallback
|
|
463
|
-
const headers = ["ID", "Severity", "Title", "File", "Line"];
|
|
514
|
+
const headers = ["ID", "Severity", "Title", "File", "Line", "Fix"];
|
|
464
515
|
const rows = reportData.findings.map(f => [
|
|
465
516
|
f.id || "",
|
|
466
517
|
f.severity || "",
|
|
467
518
|
`"${(f.title || f.message || "").replace(/"/g, '""')}"`,
|
|
468
519
|
f.file || "",
|
|
469
520
|
f.line || "",
|
|
521
|
+
`"${(f.fix || "").replace(/"/g, '""')}"`,
|
|
470
522
|
]);
|
|
471
523
|
return [headers.join(","), ...rows.map(r => r.join(","))].join("\n");
|
|
472
524
|
}
|
|
473
525
|
|
|
526
|
+
async function generatePDFReport(reportData, opts, outputDir) {
|
|
527
|
+
// Generate HTML first
|
|
528
|
+
const htmlContent = generateHTMLReport(reportData, { ...opts, format: "html" });
|
|
529
|
+
const tempHtmlPath = path.join(outputDir, "temp_report.html");
|
|
530
|
+
fs.writeFileSync(tempHtmlPath, htmlContent);
|
|
531
|
+
|
|
532
|
+
// Try to use puppeteer for PDF generation
|
|
533
|
+
try {
|
|
534
|
+
const puppeteer = require("puppeteer");
|
|
535
|
+
const browser = await puppeteer.launch({ headless: "new" });
|
|
536
|
+
const page = await browser.newPage();
|
|
537
|
+
await page.setContent(htmlContent, { waitUntil: "networkidle0" });
|
|
538
|
+
const pdf = await page.pdf({
|
|
539
|
+
format: "Letter",
|
|
540
|
+
printBackground: true,
|
|
541
|
+
margin: { top: "0.5in", right: "0.5in", bottom: "0.5in", left: "0.5in" },
|
|
542
|
+
});
|
|
543
|
+
await browser.close();
|
|
544
|
+
|
|
545
|
+
// Clean up temp file
|
|
546
|
+
try { fs.unlinkSync(tempHtmlPath); } catch {}
|
|
547
|
+
|
|
548
|
+
return pdf;
|
|
549
|
+
} catch (e) {
|
|
550
|
+
// Clean up temp file
|
|
551
|
+
try { fs.unlinkSync(tempHtmlPath); } catch {}
|
|
552
|
+
|
|
553
|
+
throw new Error(
|
|
554
|
+
`PDF generation requires puppeteer. Install with: npm install puppeteer\n` +
|
|
555
|
+
`Or use --format=html and print to PDF from your browser.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
474
560
|
function convertToLegacyFormat(reportData) {
|
|
475
561
|
return {
|
|
476
562
|
projectName: reportData.meta.projectName,
|
|
477
563
|
generatedAt: reportData.meta.generatedAt,
|
|
564
|
+
reportId: reportData.meta.reportId,
|
|
478
565
|
score: reportData.summary.score,
|
|
479
566
|
verdict: reportData.summary.verdict,
|
|
480
567
|
findings: reportData.findings,
|
|
481
568
|
categoryScores: reportData.summary.categoryScores,
|
|
569
|
+
reality: reportData.reality,
|
|
482
570
|
};
|
|
483
571
|
}
|
|
484
572
|
|
|
@@ -488,18 +576,18 @@ function convertToLegacyFormat(reportData) {
|
|
|
488
576
|
|
|
489
577
|
function generateBasicHTML(reportData, opts) {
|
|
490
578
|
const { meta, summary } = reportData;
|
|
491
|
-
const verdictColor = summary.verdict === "SHIP" ? "#
|
|
492
|
-
|
|
579
|
+
const verdictColor = summary.verdict === "SHIP" ? "#10b981" : summary.verdict === "WARN" ? "#f59e0b" : "#ef4444";
|
|
580
|
+
|
|
493
581
|
return `<!DOCTYPE html>
|
|
494
582
|
<html lang="en">
|
|
495
583
|
<head>
|
|
496
584
|
<meta charset="UTF-8">
|
|
497
585
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
498
|
-
<title>
|
|
586
|
+
<title>VibeCheck Report - ${meta.projectName}</title>
|
|
499
587
|
<style>
|
|
500
588
|
body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 40px; background: #0f172a; color: #f8fafc; }
|
|
501
589
|
.header { border-bottom: 2px solid #334155; padding-bottom: 20px; margin-bottom: 30px; }
|
|
502
|
-
.score { font-size: 4rem; font-weight: 800; text-align: center; margin: 40px 0; }
|
|
590
|
+
.score { font-size: 4rem; font-weight: 800; text-align: center; margin: 40px 0; color: ${verdictColor}; }
|
|
503
591
|
.verdict { display: inline-block; padding: 12px 24px; border-radius: 50px; font-weight: 600; background: ${verdictColor}22; color: ${verdictColor}; }
|
|
504
592
|
.section { margin: 30px 0; }
|
|
505
593
|
h2 { color: #94a3b8; font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.1em; }
|
|
@@ -518,19 +606,19 @@ function generateBasicHTML(reportData, opts) {
|
|
|
518
606
|
<h2>Findings (${summary.totalFindings})</h2>
|
|
519
607
|
${reportData.findings.slice(0, 10).map(f => `
|
|
520
608
|
<div class="finding">
|
|
521
|
-
<strong>${f.severity
|
|
609
|
+
<strong>${(f.severity || 'WARN').toUpperCase()}</strong>: ${f.title || f.message}
|
|
522
610
|
${f.file ? `<br><code>${f.file}${f.line ? `:${f.line}` : ""}</code>` : ""}
|
|
523
611
|
</div>
|
|
524
612
|
`).join("")}
|
|
525
613
|
</div>
|
|
526
|
-
<div class="footer">Generated by
|
|
614
|
+
<div class="footer">Generated by VibeCheck · ${meta.reportId}</div>
|
|
527
615
|
</body>
|
|
528
616
|
</html>`;
|
|
529
617
|
}
|
|
530
618
|
|
|
531
619
|
function generateBasicMarkdown(reportData, opts) {
|
|
532
620
|
const { meta, summary, findings } = reportData;
|
|
533
|
-
return `#
|
|
621
|
+
return `# VibeCheck Report
|
|
534
622
|
|
|
535
623
|
**Project:** ${meta.projectName}
|
|
536
624
|
**Generated:** ${new Date(meta.generatedAt).toLocaleString()}
|
|
@@ -539,10 +627,10 @@ function generateBasicMarkdown(reportData, opts) {
|
|
|
539
627
|
|
|
540
628
|
## Findings (${summary.totalFindings})
|
|
541
629
|
|
|
542
|
-
${findings.slice(0, 20).map(f => `- **${f.severity
|
|
630
|
+
${findings.slice(0, 20).map(f => `- **${(f.severity || 'WARN').toUpperCase()}**: ${f.title || f.message}${f.file ? ` (\`${f.file}\`)` : ""}`).join("\n")}
|
|
543
631
|
|
|
544
632
|
---
|
|
545
|
-
*Generated by
|
|
633
|
+
*Generated by VibeCheck · ${meta.reportId}*
|
|
546
634
|
`;
|
|
547
635
|
}
|
|
548
636
|
|
|
@@ -550,16 +638,30 @@ ${findings.slice(0, 20).map(f => `- **${f.severity?.toUpperCase()}**: ${f.title
|
|
|
550
638
|
// UTILITIES
|
|
551
639
|
// ============================================================================
|
|
552
640
|
|
|
641
|
+
function calculateFixTime(findings) {
|
|
642
|
+
if (!findings || findings.length === 0) return "0h";
|
|
643
|
+
|
|
644
|
+
const minutes = findings.reduce((total, f) => {
|
|
645
|
+
const sev = (f.severity || "").toLowerCase();
|
|
646
|
+
const time = { block: 60, critical: 60, high: 45, warn: 20, medium: 20, info: 10, low: 10 };
|
|
647
|
+
return total + (time[sev] || 15);
|
|
648
|
+
}, 0);
|
|
649
|
+
|
|
650
|
+
if (minutes < 60) return `${minutes}m`;
|
|
651
|
+
const hours = Math.round(minutes / 60 * 10) / 10;
|
|
652
|
+
return hours > 8 ? `${Math.ceil(hours / 8)}d` : `${hours}h`;
|
|
653
|
+
}
|
|
654
|
+
|
|
553
655
|
function saveReportHistory(outputDir, reportData) {
|
|
554
656
|
try {
|
|
555
657
|
const historyDir = path.join(outputDir, "history");
|
|
556
658
|
if (!fs.existsSync(historyDir)) {
|
|
557
659
|
fs.mkdirSync(historyDir, { recursive: true });
|
|
558
660
|
}
|
|
559
|
-
|
|
661
|
+
|
|
560
662
|
const timestamp = new Date().toISOString().split("T")[0];
|
|
561
663
|
const historyFile = path.join(historyDir, `${timestamp}.json`);
|
|
562
|
-
|
|
664
|
+
|
|
563
665
|
fs.writeFileSync(historyFile, JSON.stringify({
|
|
564
666
|
meta: reportData.meta,
|
|
565
667
|
summary: reportData.summary,
|
|
@@ -569,16 +671,11 @@ function saveReportHistory(outputDir, reportData) {
|
|
|
569
671
|
}
|
|
570
672
|
}
|
|
571
673
|
|
|
572
|
-
function
|
|
573
|
-
const {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
console.log(` ${c.dim}Format:${c.reset} ${c.bold}${opts.format}${c.reset}`);
|
|
578
|
-
console.log(` ${c.dim}Score:${c.reset} ${verdictColor}${c.bold}${summary.score}${c.reset}/100`);
|
|
579
|
-
console.log(` ${c.dim}Verdict:${c.reset} ${verdictColor}${c.bold}${summary.verdict}${c.reset}`);
|
|
580
|
-
console.log(` ${c.dim}Findings:${c.reset} ${summary.totalFindings}`);
|
|
581
|
-
console.log(`\n ${c.dim}Output:${c.reset} ${c.cyan}${outputFile}${c.reset}\n`);
|
|
674
|
+
function openInBrowser(filePath) {
|
|
675
|
+
const { exec } = require("child_process");
|
|
676
|
+
const cmd = process.platform === "darwin" ? "open" :
|
|
677
|
+
process.platform === "win32" ? "start" : "xdg-open";
|
|
678
|
+
exec(`${cmd} "${filePath}"`);
|
|
582
679
|
}
|
|
583
680
|
|
|
584
681
|
module.exports = { runReport };
|