@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.
Files changed (47) hide show
  1. package/README.md +60 -33
  2. package/bin/registry.js +319 -34
  3. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  4. package/bin/runners/REPORT_AUDIT.md +64 -0
  5. package/bin/runners/lib/entitlements-v2.js +97 -28
  6. package/bin/runners/lib/entitlements.js +3 -6
  7. package/bin/runners/lib/init-wizard.js +1 -1
  8. package/bin/runners/lib/report-engine.js +459 -280
  9. package/bin/runners/lib/report-html.js +1154 -1423
  10. package/bin/runners/lib/report-output.js +187 -0
  11. package/bin/runners/lib/report-templates.js +848 -850
  12. package/bin/runners/lib/scan-output.js +545 -0
  13. package/bin/runners/lib/server-usage.js +0 -12
  14. package/bin/runners/lib/ship-output.js +641 -0
  15. package/bin/runners/lib/status-output.js +253 -0
  16. package/bin/runners/lib/terminal-ui.js +853 -0
  17. package/bin/runners/runCheckpoint.js +502 -0
  18. package/bin/runners/runContracts.js +105 -0
  19. package/bin/runners/runExport.js +93 -0
  20. package/bin/runners/runFix.js +31 -24
  21. package/bin/runners/runInit.js +377 -112
  22. package/bin/runners/runInstall.js +1 -5
  23. package/bin/runners/runLabs.js +3 -3
  24. package/bin/runners/runPolish.js +2452 -0
  25. package/bin/runners/runProve.js +2 -2
  26. package/bin/runners/runReport.js +251 -200
  27. package/bin/runners/runRuntime.js +110 -0
  28. package/bin/runners/runScan.js +477 -379
  29. package/bin/runners/runSecurity.js +92 -0
  30. package/bin/runners/runShip.js +137 -207
  31. package/bin/runners/runStatus.js +16 -68
  32. package/bin/runners/utils.js +5 -5
  33. package/bin/vibecheck.js +25 -11
  34. package/mcp-server/index.js +150 -18
  35. package/mcp-server/package.json +2 -2
  36. package/mcp-server/premium-tools.js +13 -13
  37. package/mcp-server/tier-auth.js +292 -27
  38. package/mcp-server/vibecheck-tools.js +9 -9
  39. package/package.json +1 -1
  40. package/bin/runners/runClaimVerifier.js +0 -483
  41. package/bin/runners/runContextCompiler.js +0 -385
  42. package/bin/runners/runGate.js +0 -17
  43. package/bin/runners/runInitGha.js +0 -164
  44. package/bin/runners/runInteractive.js +0 -388
  45. package/bin/runners/runMdc.js +0 -204
  46. package/bin/runners/runMissionGenerator.js +0 -282
  47. package/bin/runners/runTruthpack.js +0 -636
@@ -1,63 +1,93 @@
1
1
  /**
2
- * vibecheck report - World-Class Professional Reports
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
- const entitlements = require("./lib/entitlements-v2");
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 engine modules
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
- // Fallback to basic templates if enhanced modules unavailable
30
- reportEngine = null;
31
- reportHtml = null;
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
- // ANSI color codes
35
- const c = {
36
- reset: "\x1b[0m",
37
- bold: "\x1b[1m",
38
- dim: "\x1b[2m",
39
- red: "\x1b[31m",
40
- green: "\x1b[32m",
41
- yellow: "\x1b[33m",
42
- blue: "\x1b[34m",
43
- magenta: "\x1b[35m",
44
- cyan: "\x1b[36m",
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: "executive",
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
- ${c.bold}${c.cyan}vibecheck report${c.reset} — World-Class Professional Reports
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: Check format access
135
+ // TIER ENFORCEMENT
155
136
  const format = opts.format.toLowerCase();
156
- const limits = entitlements.getLimits(await entitlements.getTier({ projectPath }));
137
+ const tier = await entitlements.getTier({ projectPath });
138
+ const limits = entitlements.getLimits(tier);
157
139
  const allowedFormats = limits.reportFormats || ["html", "md"];
158
-
159
- // Check if requested format is allowed
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${c.yellow}Tip:${c.reset} HTML and MD formats are available on FREE tier`);
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,33 @@ async function runReport(args) {
178
172
  silent: false,
179
173
  });
180
174
  if (!access.allowed) {
181
- console.log(`\n${c.yellow}Tip:${c.reset} Executive and technical reports are available on lower tiers`);
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
- console.log(`\n${c.bold}${c.cyan}📄 Generating ${opts.type} report...${c.reset}\n`);
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 from multiple possible sources
191
+ // Load ship results
189
192
  let shipResults = loadShipResults(projectPath, outputDir);
190
193
 
191
194
  if (!shipResults) {
192
- console.log(`${c.yellow}⚠${c.reset} No scan results found. Using demo data for preview.\n`);
193
- console.log(`${c.dim} Tip: Run 'vibecheck ship' first for real results.${c.reset}\n`);
195
+ spinner.warn("No scan results found - using demo data");
196
+ console.log(` ${ansi.dim}Tip: Run 'vibecheck ship' first for real results.${ansi.reset}\n`);
194
197
  shipResults = getDemoData();
195
198
  }
196
199
 
197
- // Build comprehensive report data using the new engine
200
+ // Build comprehensive report data
201
+ spinner.update("Processing findings");
198
202
  let reportData;
199
203
  if (reportEngine) {
200
204
  reportData = reportEngine.buildReportData(shipResults, {
@@ -203,56 +207,73 @@ async function runReport(args) {
203
207
  includeTrends: opts.includeTrends,
204
208
  });
205
209
  } else {
206
- // Fallback to basic report data
207
210
  reportData = buildBasicReportData(shipResults, projectName);
208
211
  }
209
212
 
210
- // Generate report content based on format
213
+ // Generate report content
214
+ spinner.update(`Rendering ${format.toUpperCase()} output`);
211
215
  let reportContent = "";
212
- let fileExtension = opts.format;
216
+ let fileExtension = format;
213
217
 
214
- switch (opts.format) {
215
- case "html":
216
- reportContent = generateHTMLReport(reportData, opts);
217
- break;
218
- case "md":
219
- case "markdown":
220
- reportContent = generateMarkdownReport(reportData, opts);
221
- fileExtension = "md";
222
- break;
223
- case "json":
224
- reportContent = generateJSONReport(reportData, opts);
225
- break;
226
- case "sarif":
227
- reportContent = generateSARIFReport(reportData, opts);
228
- break;
229
- case "csv":
230
- reportContent = generateCSVReport(reportData, opts);
231
- break;
232
- default:
233
- console.error(`${c.red}✗${c.reset} Unknown format: ${opts.format}`);
234
- console.log(`${c.dim} Supported: html, md, json, sarif, csv${c.reset}`);
235
- return 1;
218
+ try {
219
+ switch (format) {
220
+ case "html":
221
+ reportContent = generateHTMLReport(reportData, opts);
222
+ break;
223
+ case "md":
224
+ case "markdown":
225
+ reportContent = generateMarkdownReport(reportData, opts);
226
+ fileExtension = "md";
227
+ break;
228
+ case "json":
229
+ reportContent = generateJSONReport(reportData, opts);
230
+ break;
231
+ case "sarif":
232
+ reportContent = generateSARIFReport(reportData, opts);
233
+ break;
234
+ case "csv":
235
+ reportContent = generateCSVReport(reportData, opts);
236
+ break;
237
+ case "pdf":
238
+ // PDF requires HTML first, then conversion
239
+ reportContent = await generatePDFReport(reportData, opts, outputDir);
240
+ break;
241
+ default:
242
+ spinner.fail(`Unknown format: ${format}`);
243
+ console.log(` ${ansi.dim}Supported: html, md, json, sarif, csv, pdf${ansi.reset}`);
244
+ return 1;
245
+ }
246
+ } catch (err) {
247
+ spinner.fail(`Failed to generate report: ${err.message}`);
248
+ return 1;
236
249
  }
237
250
 
238
251
  // Determine output path
239
252
  const outputFileName = opts.output || path.join(outputDir, `report.${fileExtension}`);
253
+ const outputDirPath = path.dirname(outputFileName);
240
254
 
241
255
  // Ensure output directory exists
242
- const outputDirPath = path.dirname(outputFileName);
243
256
  if (!fs.existsSync(outputDirPath)) {
244
257
  fs.mkdirSync(outputDirPath, { recursive: true });
245
258
  }
246
259
 
247
260
  // Write report
261
+ spinner.update("Writing report");
248
262
  fs.writeFileSync(outputFileName, reportContent);
249
263
 
250
264
  // Save report history for trend analysis
251
265
  saveReportHistory(outputDir, reportData);
252
266
 
253
- // Print success message
254
- console.log(`${c.green}✓${c.reset} Report generated successfully!\n`);
255
- printReportSummary(reportData, opts, outputFileName);
267
+ spinner.succeed("Report generated");
268
+ console.log("");
269
+
270
+ // Print success message using design system
271
+ console.log(renderSuccess(outputFileName, opts.format));
272
+
273
+ // Open in browser if requested
274
+ if (opts.open && format === "html") {
275
+ openInBrowser(outputFileName);
276
+ }
256
277
 
257
278
  return 0;
258
279
  }
@@ -262,7 +283,6 @@ async function runReport(args) {
262
283
  // ============================================================================
263
284
 
264
285
  function loadShipResults(projectPath, outputDir) {
265
- // Try multiple sources for ship results
266
286
  const sources = [
267
287
  path.join(outputDir, "ship_report.json"),
268
288
  path.join(outputDir, "report.json"),
@@ -286,7 +306,7 @@ function loadShipResults(projectPath, outputDir) {
286
306
  path.join(outputDir, "reality", "last_reality.json"),
287
307
  path.join(outputDir, "reality_report.json"),
288
308
  ];
289
-
309
+
290
310
  for (const src of realitySources) {
291
311
  if (fs.existsSync(src)) {
292
312
  try {
@@ -311,29 +331,21 @@ function getDemoData() {
311
331
  score: 72,
312
332
  verdict: "WARN",
313
333
  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" },
334
+ { 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" },
335
+ { 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" },
336
+ { 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" },
337
+ { 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" },
338
+ { id: "CFG001", severity: "INFO", type: "config", message: "Hardcoded timeout value", title: "Hardcoded Configuration", file: "src/utils/http.ts", line: 23, fix: "Move to config" },
339
+ { 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
340
  ],
341
+ categoryScores: { security: 65, auth: 50, billing: 90, quality: 75 },
321
342
  truthpack: {
322
343
  routes: { server: Array(12).fill({}) },
323
344
  env: { vars: Array(8).fill("") },
324
- auth: { nextMiddleware: [{}] },
325
- billing: { webhooks: [{}] },
326
345
  },
327
346
  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],
347
+ coverage: { clientCallsMapped: 87, uiActionsVerified: 95, authRoutes: 100 },
335
348
  latencyP95: 412,
336
- latencySparkline: [120, 150, 180, 200, 220, 280, 320, 380, 400, 412],
337
349
  brokenFlows: [
338
350
  {
339
351
  title: "User cannot complete checkout flow",
@@ -346,21 +358,6 @@ function getDemoData() {
346
358
  { type: "error", label: "500 Internal Server Error" },
347
359
  ],
348
360
  },
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
361
  ],
365
362
  },
366
363
  };
@@ -369,7 +366,7 @@ function getDemoData() {
369
366
  function buildBasicReportData(shipResults, projectName) {
370
367
  const findings = shipResults?.findings || [];
371
368
  const reality = shipResults?.reality || null;
372
-
369
+
373
370
  return {
374
371
  meta: {
375
372
  projectName,
@@ -387,11 +384,11 @@ function buildBasicReportData(shipResults, projectName) {
387
384
  medium: findings.filter(f => f.severity === "WARN" || f.severity === "medium").length,
388
385
  low: findings.filter(f => f.severity === "low" || f.severity === "INFO").length,
389
386
  },
390
- categoryScores: shipResults?.categoryScores || { security: 80, auth: 70, billing: 90, quality: 60 },
387
+ categoryScores: shipResults?.categoryScores || {},
391
388
  },
392
- findings: findings.map((f, i) => ({ ...f, id: f.id || `F${String(i+1).padStart(3,"0")}` })),
393
- fixEstimates: { humanReadable: "~2h", totalMinutes: 120, bySeverity: {} },
394
- truthpack: { routes: 0, envVars: 0, hasAuth: false, hasBilling: false },
389
+ findings: findings.map((f, i) => ({ ...f, id: f.id || `F${String(i + 1).padStart(3, "0")}` })),
390
+ fixEstimates: { humanReadable: calculateFixTime(findings), totalMinutes: 120 },
391
+ truthpack: shipResults?.truthpack || null,
395
392
  reality: reality,
396
393
  };
397
394
  }
@@ -401,43 +398,51 @@ function buildBasicReportData(shipResults, projectName) {
401
398
  // ============================================================================
402
399
 
403
400
  function generateHTMLReport(reportData, opts) {
404
- // Use world-class HTML generator if available
405
- if (reportHtml && reportHtml.generateWorldClassHTML) {
406
- return reportHtml.generateWorldClassHTML(reportData, opts);
407
- }
408
-
409
- // Fallback to enhanced templates
410
- try {
411
- const templates = require("./lib/report-templates");
412
- switch (opts.type) {
413
- case "technical":
414
- return templates.generateEnhancedTechnicalReport(convertToLegacyFormat(reportData), opts);
415
- case "compliance":
416
- return templates.generateEnhancedComplianceReport(convertToLegacyFormat(reportData), opts);
417
- default:
418
- return templates.generateEnhancedExecutiveReport(convertToLegacyFormat(reportData), opts);
419
- }
420
- } catch {
421
- return generateBasicHTML(reportData, opts);
401
+ // Route to appropriate template based on type
402
+ switch (opts.type) {
403
+ case "executive":
404
+ if (reportTemplates?.generateEnhancedExecutiveReport) {
405
+ return reportTemplates.generateEnhancedExecutiveReport(
406
+ convertToLegacyFormat(reportData),
407
+ opts
408
+ );
409
+ }
410
+ break;
411
+ case "compliance":
412
+ if (reportTemplates?.generateEnhancedComplianceReport) {
413
+ return reportTemplates.generateEnhancedComplianceReport(
414
+ convertToLegacyFormat(reportData),
415
+ opts
416
+ );
417
+ }
418
+ break;
419
+ default:
420
+ // Technical report - use world-class HTML generator
421
+ if (reportHtml?.generateWorldClassHTML) {
422
+ return reportHtml.generateWorldClassHTML(reportData, opts);
423
+ }
422
424
  }
425
+
426
+ // Fallback to basic HTML
427
+ return generateBasicHTML(reportData, opts);
423
428
  }
424
429
 
425
430
  function generateMarkdownReport(reportData, opts) {
426
- if (reportEngine && reportEngine.exportToMarkdown) {
431
+ if (reportEngine?.exportToMarkdown) {
427
432
  return reportEngine.exportToMarkdown(reportData, opts);
428
433
  }
429
434
  return generateBasicMarkdown(reportData, opts);
430
435
  }
431
436
 
432
437
  function generateJSONReport(reportData, opts) {
433
- if (reportEngine && reportEngine.exportToJSON) {
438
+ if (reportEngine?.exportToJSON) {
434
439
  return reportEngine.exportToJSON(reportData);
435
440
  }
436
441
  return JSON.stringify(reportData, null, 2);
437
442
  }
438
443
 
439
444
  function generateSARIFReport(reportData, opts) {
440
- if (reportEngine && reportEngine.exportToSARIF) {
445
+ if (reportEngine?.exportToSARIF) {
441
446
  return JSON.stringify(reportEngine.exportToSARIF(reportData), null, 2);
442
447
  }
443
448
  // Basic SARIF fallback
@@ -456,29 +461,66 @@ function generateSARIFReport(reportData, opts) {
456
461
  }
457
462
 
458
463
  function generateCSVReport(reportData, opts) {
459
- if (reportEngine && reportEngine.exportToCSV) {
464
+ if (reportEngine?.exportToCSV) {
460
465
  return reportEngine.exportToCSV(reportData);
461
466
  }
462
467
  // Basic CSV fallback
463
- const headers = ["ID", "Severity", "Title", "File", "Line"];
468
+ const headers = ["ID", "Severity", "Title", "File", "Line", "Fix"];
464
469
  const rows = reportData.findings.map(f => [
465
470
  f.id || "",
466
471
  f.severity || "",
467
472
  `"${(f.title || f.message || "").replace(/"/g, '""')}"`,
468
473
  f.file || "",
469
474
  f.line || "",
475
+ `"${(f.fix || "").replace(/"/g, '""')}"`,
470
476
  ]);
471
477
  return [headers.join(","), ...rows.map(r => r.join(","))].join("\n");
472
478
  }
473
479
 
480
+ async function generatePDFReport(reportData, opts, outputDir) {
481
+ // Generate HTML first
482
+ const htmlContent = generateHTMLReport(reportData, { ...opts, format: "html" });
483
+ const tempHtmlPath = path.join(outputDir, "temp_report.html");
484
+ fs.writeFileSync(tempHtmlPath, htmlContent);
485
+
486
+ // Try to use puppeteer for PDF generation
487
+ try {
488
+ const puppeteer = require("puppeteer");
489
+ const browser = await puppeteer.launch({ headless: "new" });
490
+ const page = await browser.newPage();
491
+ await page.setContent(htmlContent, { waitUntil: "networkidle0" });
492
+ const pdf = await page.pdf({
493
+ format: "Letter",
494
+ printBackground: true,
495
+ margin: { top: "0.5in", right: "0.5in", bottom: "0.5in", left: "0.5in" },
496
+ });
497
+ await browser.close();
498
+
499
+ // Clean up temp file
500
+ try { fs.unlinkSync(tempHtmlPath); } catch {}
501
+
502
+ return pdf;
503
+ } catch (e) {
504
+ // Clean up temp file
505
+ try { fs.unlinkSync(tempHtmlPath); } catch {}
506
+
507
+ throw new Error(
508
+ `PDF generation requires puppeteer. Install with: npm install puppeteer\n` +
509
+ `Or use --format=html and print to PDF from your browser.`
510
+ );
511
+ }
512
+ }
513
+
474
514
  function convertToLegacyFormat(reportData) {
475
515
  return {
476
516
  projectName: reportData.meta.projectName,
477
517
  generatedAt: reportData.meta.generatedAt,
518
+ reportId: reportData.meta.reportId,
478
519
  score: reportData.summary.score,
479
520
  verdict: reportData.summary.verdict,
480
521
  findings: reportData.findings,
481
522
  categoryScores: reportData.summary.categoryScores,
523
+ reality: reportData.reality,
482
524
  };
483
525
  }
484
526
 
@@ -488,18 +530,18 @@ function convertToLegacyFormat(reportData) {
488
530
 
489
531
  function generateBasicHTML(reportData, opts) {
490
532
  const { meta, summary } = reportData;
491
- const verdictColor = summary.verdict === "SHIP" ? "#22c55e" : summary.verdict === "WARN" ? "#f59e0b" : "#ef4444";
492
-
533
+ const verdictColor = summary.verdict === "SHIP" ? "#10b981" : summary.verdict === "WARN" ? "#f59e0b" : "#ef4444";
534
+
493
535
  return `<!DOCTYPE html>
494
536
  <html lang="en">
495
537
  <head>
496
538
  <meta charset="UTF-8">
497
539
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
498
- <title>Vibecheck Report - ${meta.projectName}</title>
540
+ <title>VibeCheck Report - ${meta.projectName}</title>
499
541
  <style>
500
542
  body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 40px; background: #0f172a; color: #f8fafc; }
501
543
  .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; }
544
+ .score { font-size: 4rem; font-weight: 800; text-align: center; margin: 40px 0; color: ${verdictColor}; }
503
545
  .verdict { display: inline-block; padding: 12px 24px; border-radius: 50px; font-weight: 600; background: ${verdictColor}22; color: ${verdictColor}; }
504
546
  .section { margin: 30px 0; }
505
547
  h2 { color: #94a3b8; font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.1em; }
@@ -518,19 +560,19 @@ function generateBasicHTML(reportData, opts) {
518
560
  <h2>Findings (${summary.totalFindings})</h2>
519
561
  ${reportData.findings.slice(0, 10).map(f => `
520
562
  <div class="finding">
521
- <strong>${f.severity?.toUpperCase()}</strong>: ${f.title || f.message}
563
+ <strong>${(f.severity || 'WARN').toUpperCase()}</strong>: ${f.title || f.message}
522
564
  ${f.file ? `<br><code>${f.file}${f.line ? `:${f.line}` : ""}</code>` : ""}
523
565
  </div>
524
566
  `).join("")}
525
567
  </div>
526
- <div class="footer">Generated by Vibecheck · ${meta.reportId}</div>
568
+ <div class="footer">Generated by VibeCheck · ${meta.reportId}</div>
527
569
  </body>
528
570
  </html>`;
529
571
  }
530
572
 
531
573
  function generateBasicMarkdown(reportData, opts) {
532
574
  const { meta, summary, findings } = reportData;
533
- return `# Vibecheck Report
575
+ return `# VibeCheck Report
534
576
 
535
577
  **Project:** ${meta.projectName}
536
578
  **Generated:** ${new Date(meta.generatedAt).toLocaleString()}
@@ -539,10 +581,10 @@ function generateBasicMarkdown(reportData, opts) {
539
581
 
540
582
  ## Findings (${summary.totalFindings})
541
583
 
542
- ${findings.slice(0, 20).map(f => `- **${f.severity?.toUpperCase()}**: ${f.title || f.message}${f.file ? ` (\`${f.file}\`)` : ""}`).join("\n")}
584
+ ${findings.slice(0, 20).map(f => `- **${(f.severity || 'WARN').toUpperCase()}**: ${f.title || f.message}${f.file ? ` (\`${f.file}\`)` : ""}`).join("\n")}
543
585
 
544
586
  ---
545
- *Generated by Vibecheck · ${meta.reportId}*
587
+ *Generated by VibeCheck · ${meta.reportId}*
546
588
  `;
547
589
  }
548
590
 
@@ -550,16 +592,30 @@ ${findings.slice(0, 20).map(f => `- **${f.severity?.toUpperCase()}**: ${f.title
550
592
  // UTILITIES
551
593
  // ============================================================================
552
594
 
595
+ function calculateFixTime(findings) {
596
+ if (!findings || findings.length === 0) return "0h";
597
+
598
+ const minutes = findings.reduce((total, f) => {
599
+ const sev = (f.severity || "").toLowerCase();
600
+ const time = { block: 60, critical: 60, high: 45, warn: 20, medium: 20, info: 10, low: 10 };
601
+ return total + (time[sev] || 15);
602
+ }, 0);
603
+
604
+ if (minutes < 60) return `${minutes}m`;
605
+ const hours = Math.round(minutes / 60 * 10) / 10;
606
+ return hours > 8 ? `${Math.ceil(hours / 8)}d` : `${hours}h`;
607
+ }
608
+
553
609
  function saveReportHistory(outputDir, reportData) {
554
610
  try {
555
611
  const historyDir = path.join(outputDir, "history");
556
612
  if (!fs.existsSync(historyDir)) {
557
613
  fs.mkdirSync(historyDir, { recursive: true });
558
614
  }
559
-
615
+
560
616
  const timestamp = new Date().toISOString().split("T")[0];
561
617
  const historyFile = path.join(historyDir, `${timestamp}.json`);
562
-
618
+
563
619
  fs.writeFileSync(historyFile, JSON.stringify({
564
620
  meta: reportData.meta,
565
621
  summary: reportData.summary,
@@ -569,16 +625,11 @@ function saveReportHistory(outputDir, reportData) {
569
625
  }
570
626
  }
571
627
 
572
- function printReportSummary(reportData, opts, outputFile) {
573
- const { summary } = reportData;
574
- const verdictColor = summary.verdict === "SHIP" ? c.green : summary.verdict === "WARN" ? c.yellow : c.red;
575
-
576
- console.log(` ${c.dim}Type:${c.reset} ${c.bold}${opts.type}${c.reset}`);
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`);
628
+ function openInBrowser(filePath) {
629
+ const { exec } = require("child_process");
630
+ const cmd = process.platform === "darwin" ? "open" :
631
+ process.platform === "win32" ? "start" : "xdg-open";
632
+ exec(`${cmd} "${filePath}"`);
582
633
  }
583
634
 
584
635
  module.exports = { runReport };