@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.
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 +1157 -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 +299 -202
  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,35 @@ 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
+ 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 using the new engine
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 based on format
215
+ // Generate report content
216
+ spinner.update(`Rendering ${format.toUpperCase()} output`);
211
217
  let reportContent = "";
212
- let fileExtension = opts.format;
218
+ let fileExtension = format;
213
219
 
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;
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
- fs.writeFileSync(outputFileName, reportContent);
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
- // Print success message
254
- console.log(`${c.green}✓${c.reset} Report generated successfully!\n`);
255
- printReportSummary(reportData, opts, outputFileName);
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 || { security: 80, auth: 70, billing: 90, quality: 60 },
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: "~2h", totalMinutes: 120, bySeverity: {} },
394
- truthpack: { routes: 0, envVars: 0, hasAuth: false, hasBilling: false },
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
- // 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);
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 && reportEngine.exportToMarkdown) {
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 && reportEngine.exportToJSON) {
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 && reportEngine.exportToSARIF) {
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 && reportEngine.exportToCSV) {
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" ? "#22c55e" : summary.verdict === "WARN" ? "#f59e0b" : "#ef4444";
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>Vibecheck Report - ${meta.projectName}</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?.toUpperCase()}</strong>: ${f.title || f.message}
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 Vibecheck · ${meta.reportId}</div>
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 `# Vibecheck Report
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?.toUpperCase()}**: ${f.title || f.message}${f.file ? ` (\`${f.file}\`)` : ""}`).join("\n")}
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 Vibecheck · ${meta.reportId}*
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 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`);
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 };