@vibecheckai/cli 3.5.0 → 3.5.2

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 (224) hide show
  1. package/bin/registry.js +214 -237
  2. package/bin/runners/cli-utils.js +33 -2
  3. package/bin/runners/context/analyzer.js +52 -1
  4. package/bin/runners/context/generators/cursor.js +2 -49
  5. package/bin/runners/context/git-context.js +3 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/analysis-core.js +25 -5
  8. package/bin/runners/lib/analyzers.js +431 -481
  9. package/bin/runners/lib/default-config.js +127 -0
  10. package/bin/runners/lib/doctor/modules/security.js +3 -1
  11. package/bin/runners/lib/engine/ast-cache.js +210 -0
  12. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  13. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  14. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  15. package/bin/runners/lib/engine/env-extractor.js +207 -0
  16. package/bin/runners/lib/engine/express-extractor.js +208 -0
  17. package/bin/runners/lib/engine/extractors.js +849 -0
  18. package/bin/runners/lib/engine/index.js +207 -0
  19. package/bin/runners/lib/engine/repo-index.js +514 -0
  20. package/bin/runners/lib/engine/types.js +124 -0
  21. package/bin/runners/lib/engines/accessibility-engine.js +18 -218
  22. package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
  23. package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
  24. package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
  25. package/bin/runners/lib/engines/mock-data-engine.js +10 -53
  26. package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
  27. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
  28. package/bin/runners/lib/engines/type-aware-engine.js +39 -263
  29. package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
  30. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  31. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  32. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  33. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  34. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  35. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  36. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  37. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
  38. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  39. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  40. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  41. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  42. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  43. package/bin/runners/lib/entitlements-v2.js +73 -97
  44. package/bin/runners/lib/error-handler.js +44 -3
  45. package/bin/runners/lib/error-messages.js +289 -0
  46. package/bin/runners/lib/evidence-pack.js +7 -1
  47. package/bin/runners/lib/finding-id.js +69 -0
  48. package/bin/runners/lib/finding-sorter.js +89 -0
  49. package/bin/runners/lib/html-proof-report.js +700 -350
  50. package/bin/runners/lib/missions/plan.js +6 -46
  51. package/bin/runners/lib/missions/templates.js +0 -232
  52. package/bin/runners/lib/next-action.js +560 -0
  53. package/bin/runners/lib/prerequisites.js +149 -0
  54. package/bin/runners/lib/route-detection.js +137 -68
  55. package/bin/runners/lib/scan-output.js +91 -76
  56. package/bin/runners/lib/scan-runner.js +135 -0
  57. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  58. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  59. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  60. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  61. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  62. package/bin/runners/lib/schemas/validator.js +27 -0
  63. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  64. package/bin/runners/lib/ship-output-enterprise.js +23 -23
  65. package/bin/runners/lib/ship-output.js +75 -31
  66. package/bin/runners/lib/terminal-ui.js +6 -113
  67. package/bin/runners/lib/truth.js +351 -10
  68. package/bin/runners/lib/unified-cli-output.js +430 -603
  69. package/bin/runners/lib/unified-output.js +13 -9
  70. package/bin/runners/runAIAgent.js +10 -5
  71. package/bin/runners/runAgent.js +0 -3
  72. package/bin/runners/runAllowlist.js +389 -0
  73. package/bin/runners/runApprove.js +0 -33
  74. package/bin/runners/runAuth.js +73 -45
  75. package/bin/runners/runCheckpoint.js +51 -11
  76. package/bin/runners/runClassify.js +85 -21
  77. package/bin/runners/runContext.js +0 -3
  78. package/bin/runners/runDoctor.js +41 -28
  79. package/bin/runners/runEvidencePack.js +362 -0
  80. package/bin/runners/runFirewall.js +0 -3
  81. package/bin/runners/runFirewallHook.js +0 -3
  82. package/bin/runners/runFix.js +66 -76
  83. package/bin/runners/runGuard.js +18 -411
  84. package/bin/runners/runInit.js +113 -30
  85. package/bin/runners/runLabs.js +424 -0
  86. package/bin/runners/runMcp.js +19 -25
  87. package/bin/runners/runPolish.js +64 -240
  88. package/bin/runners/runPromptFirewall.js +12 -5
  89. package/bin/runners/runProve.js +57 -22
  90. package/bin/runners/runQuickstart.js +531 -0
  91. package/bin/runners/runReality.js +59 -68
  92. package/bin/runners/runReport.js +38 -33
  93. package/bin/runners/runRuntime.js +8 -5
  94. package/bin/runners/runScan.js +1413 -190
  95. package/bin/runners/runShip.js +113 -719
  96. package/bin/runners/runTruth.js +0 -3
  97. package/bin/runners/runValidate.js +13 -9
  98. package/bin/runners/runWatch.js +23 -14
  99. package/bin/scan.js +6 -1
  100. package/bin/vibecheck.js +204 -185
  101. package/mcp-server/deprecation-middleware.js +282 -0
  102. package/mcp-server/handlers/index.ts +15 -0
  103. package/mcp-server/handlers/tool-handler.ts +554 -0
  104. package/mcp-server/index-v1.js +698 -0
  105. package/mcp-server/index.js +210 -238
  106. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  107. package/mcp-server/lib/error-envelope.js +138 -0
  108. package/mcp-server/lib/executor.ts +499 -0
  109. package/mcp-server/lib/index.ts +19 -0
  110. package/mcp-server/lib/rate-limiter.js +166 -0
  111. package/mcp-server/lib/sandbox.test.ts +519 -0
  112. package/mcp-server/lib/sandbox.ts +395 -0
  113. package/mcp-server/lib/types.ts +267 -0
  114. package/mcp-server/package.json +12 -3
  115. package/mcp-server/registry/tool-registry.js +794 -0
  116. package/mcp-server/registry/tools.json +605 -0
  117. package/mcp-server/registry.test.ts +334 -0
  118. package/mcp-server/tests/tier-gating.test.js +297 -0
  119. package/mcp-server/tier-auth.js +378 -45
  120. package/mcp-server/tools-v3.js +353 -442
  121. package/mcp-server/tsconfig.json +37 -0
  122. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  123. package/package.json +1 -1
  124. package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
  125. package/bin/runners/lib/audit-logger.js +0 -532
  126. package/bin/runners/lib/authority/authorities/architecture.js +0 -364
  127. package/bin/runners/lib/authority/authorities/compliance.js +0 -341
  128. package/bin/runners/lib/authority/authorities/human.js +0 -343
  129. package/bin/runners/lib/authority/authorities/quality.js +0 -420
  130. package/bin/runners/lib/authority/authorities/security.js +0 -228
  131. package/bin/runners/lib/authority/index.js +0 -293
  132. package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
  133. package/bin/runners/lib/cli-charts.js +0 -368
  134. package/bin/runners/lib/cli-config-display.js +0 -405
  135. package/bin/runners/lib/cli-demo.js +0 -275
  136. package/bin/runners/lib/cli-errors.js +0 -438
  137. package/bin/runners/lib/cli-help-formatter.js +0 -439
  138. package/bin/runners/lib/cli-interactive-menu.js +0 -509
  139. package/bin/runners/lib/cli-prompts.js +0 -441
  140. package/bin/runners/lib/cli-scan-cards.js +0 -362
  141. package/bin/runners/lib/compliance-reporter.js +0 -710
  142. package/bin/runners/lib/conductor/index.js +0 -671
  143. package/bin/runners/lib/easy/README.md +0 -123
  144. package/bin/runners/lib/easy/index.js +0 -140
  145. package/bin/runners/lib/easy/interactive-wizard.js +0 -788
  146. package/bin/runners/lib/easy/one-click-firewall.js +0 -564
  147. package/bin/runners/lib/easy/zero-config-reality.js +0 -714
  148. package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
  149. package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
  150. package/bin/runners/lib/engines/confidence-scoring.js +0 -276
  151. package/bin/runners/lib/engines/context-detection.js +0 -264
  152. package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
  153. package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
  154. package/bin/runners/lib/engines/env-variables-engine.js +0 -458
  155. package/bin/runners/lib/engines/error-handling-engine.js +0 -437
  156. package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
  157. package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
  158. package/bin/runners/lib/engines/framework-detection.js +0 -508
  159. package/bin/runners/lib/engines/import-order-engine.js +0 -429
  160. package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
  161. package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
  162. package/bin/runners/lib/engines/orchestrator.js +0 -334
  163. package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
  164. package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
  165. package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
  166. package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
  167. package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
  168. package/bin/runners/lib/enhanced-features/index.js +0 -305
  169. package/bin/runners/lib/enhanced-output.js +0 -631
  170. package/bin/runners/lib/enterprise.js +0 -300
  171. package/bin/runners/lib/firewall/command-validator.js +0 -351
  172. package/bin/runners/lib/firewall/config.js +0 -341
  173. package/bin/runners/lib/firewall/content-validator.js +0 -519
  174. package/bin/runners/lib/firewall/index.js +0 -101
  175. package/bin/runners/lib/firewall/path-validator.js +0 -256
  176. package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
  177. package/bin/runners/lib/mcp-utils.js +0 -425
  178. package/bin/runners/lib/output/index.js +0 -1022
  179. package/bin/runners/lib/policy-engine.js +0 -652
  180. package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
  181. package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
  182. package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
  183. package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
  184. package/bin/runners/lib/polish/autofix/index.js +0 -200
  185. package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
  186. package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
  187. package/bin/runners/lib/polish/backend-checks.js +0 -148
  188. package/bin/runners/lib/polish/documentation-checks.js +0 -111
  189. package/bin/runners/lib/polish/frontend-checks.js +0 -168
  190. package/bin/runners/lib/polish/index.js +0 -71
  191. package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
  192. package/bin/runners/lib/polish/library-detection.js +0 -175
  193. package/bin/runners/lib/polish/performance-checks.js +0 -100
  194. package/bin/runners/lib/polish/security-checks.js +0 -148
  195. package/bin/runners/lib/polish/utils.js +0 -203
  196. package/bin/runners/lib/prompt-builder.js +0 -540
  197. package/bin/runners/lib/proof-certificate.js +0 -634
  198. package/bin/runners/lib/reality/accessibility-audit.js +0 -946
  199. package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
  200. package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
  201. package/bin/runners/lib/reality/performance-tracker.js +0 -1077
  202. package/bin/runners/lib/reality/scenario-generator.js +0 -1404
  203. package/bin/runners/lib/reality/visual-regression.js +0 -852
  204. package/bin/runners/lib/reality-profiler.js +0 -717
  205. package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
  206. package/bin/runners/lib/review/ai-code-review.js +0 -832
  207. package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
  208. package/bin/runners/lib/sbom-generator.js +0 -641
  209. package/bin/runners/lib/scan-output-enhanced.js +0 -512
  210. package/bin/runners/lib/security/owasp-scanner.js +0 -939
  211. package/bin/runners/lib/validators/contract-validator.js +0 -283
  212. package/bin/runners/lib/validators/dead-export-detector.js +0 -279
  213. package/bin/runners/lib/validators/dep-audit.js +0 -245
  214. package/bin/runners/lib/validators/env-validator.js +0 -319
  215. package/bin/runners/lib/validators/index.js +0 -120
  216. package/bin/runners/lib/validators/license-checker.js +0 -252
  217. package/bin/runners/lib/validators/route-validator.js +0 -290
  218. package/bin/runners/runAuthority.js +0 -528
  219. package/bin/runners/runConductor.js +0 -772
  220. package/bin/runners/runContainer.js +0 -366
  221. package/bin/runners/runEasy.js +0 -410
  222. package/bin/runners/runIaC.js +0 -372
  223. package/bin/runners/runVibe.js +0 -791
  224. package/mcp-server/tools.js +0 -495
@@ -1,46 +1,60 @@
1
1
  /**
2
- * vibecheck Scan - Quick Code Analysis (FREE)
2
+ * vibecheck Scan - Route Integrity & Code Analysis
3
3
  *
4
- * Fast scan running only 5 essential engines:
5
- * - hardcoded-secrets-engine (security)
6
- * - console-logs-engine (code hygiene)
7
- * - ai-hallucination-engine (vibecheck core)
8
- * - type-aware-engine (type safety)
9
- * - error-handling-engine (reliability)
4
+ * The ultimate scanner combining:
5
+ * - Route integrity (dead links, orphan routes, coverage)
6
+ * - Security analysis (secrets, auth, vulnerabilities)
7
+ * - Code quality (mocks, placeholders, hygiene)
10
8
  *
11
- * Target: < 3 seconds on medium project (100-500 files)
12
- *
13
- * For comprehensive analysis (17+ engines + validators), use: vibecheck ship [PRO]
9
+ * Modes:
10
+ * - vibecheck scan: Layer 1 (AST) - Fast static analysis
11
+ * - vibecheck scan --truth: Layer 1+2 (+ build manifests) - CI/ship
12
+ * - vibecheck scan --reality --url <url>: Layer 1+2+3 (+ Playwright) - Full proof
14
13
  */
15
14
 
16
15
  const path = require("path");
17
16
  const fs = require("fs");
18
17
  const { withErrorHandling, createUserError } = require("./lib/error-handler");
19
- const { enforceLimit, trackUsage, getCurrentTier } = require("./lib/entitlements-v2");
18
+ const { enforceLimit, trackUsage } = require("./lib/entitlements");
20
19
  const { emitScanStart, emitScanComplete } = require("./lib/audit-bridge");
21
20
  const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
21
+ const {
22
+ createScan,
23
+ updateScanProgress,
24
+ submitScanResults,
25
+ reportScanError,
26
+ isApiAvailable
27
+ } = require("./lib/api-client");
22
28
  const { EXIT, verdictToExitCode } = require("./lib/exit-codes");
23
29
 
24
- // Import orchestrator for engine management
25
- const { runScanEngines, SCAN_ENGINES } = require("./lib/engines/orchestrator");
26
-
27
30
  // ═══════════════════════════════════════════════════════════════════════════════
28
- // TERMINAL UI
31
+ // ENHANCED TERMINAL UI & OUTPUT MODULES
29
32
  // ═══════════════════════════════════════════════════════════════════════════════
30
33
 
31
34
  const {
32
35
  ansi,
33
36
  colors,
34
37
  Spinner,
38
+ PhaseProgress,
39
+ renderBanner,
40
+ renderSection,
35
41
  formatDuration,
36
42
  } = require("./lib/terminal-ui");
37
43
 
38
44
  const {
45
+ formatScanOutput,
39
46
  formatSARIF,
40
47
  getExitCode,
48
+ printError,
49
+ EXIT_CODES,
41
50
  calculateScore,
42
51
  } = require("./lib/scan-output");
43
52
 
53
+ const {
54
+ enrichFindings,
55
+ saveBaseline,
56
+ } = require("./lib/fingerprint");
57
+
44
58
  const BANNER = `
45
59
  ${ansi.rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
46
60
  ${ansi.rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
@@ -50,7 +64,7 @@ ${ansi.rgb(120, 120, 255)} ╚████╔╝ ██║██████
50
64
  ${ansi.rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
51
65
 
52
66
  ${ansi.dim} ┌─────────────────────────────────────────────────────────────────────┐${ansi.reset}
53
- ${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}Quick Scan${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}5 Essential Engines${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}< 3 seconds${ansi.reset} ${ansi.dim}│${ansi.reset}
67
+ ${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}Route Integrity${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}Security${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}Quality${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(100, 100, 100)}Ship with Confidence${ansi.reset} ${ansi.dim}│${ansi.reset}
54
68
  ${ansi.dim} └─────────────────────────────────────────────────────────────────────┘${ansi.reset}
55
69
  `;
56
70
 
@@ -58,32 +72,94 @@ function printBanner() {
58
72
  console.log(BANNER);
59
73
  }
60
74
 
75
+ // Legacy compatibility functions - now use enhanced modules
76
+ function printCoverageMap(coverageMap) {
77
+ const { renderCoverageMap } = require("./lib/scan-output");
78
+ console.log(renderCoverageMap(coverageMap));
79
+ }
80
+
81
+ function printBreakdown(breakdown) {
82
+ const { renderBreakdown } = require("./lib/scan-output");
83
+ console.log(renderBreakdown(breakdown));
84
+ }
85
+
86
+ function printBlockers(blockers) {
87
+ const { renderBlockers } = require("./lib/scan-output");
88
+ console.log(renderBlockers(blockers));
89
+ }
90
+
91
+ function printLayers(layers) {
92
+ const { renderLayers } = require("./lib/scan-output");
93
+ console.log(renderLayers(layers));
94
+ }
95
+
61
96
  // ═══════════════════════════════════════════════════════════════════════════════
62
97
  // ARGS PARSER
63
98
  // ═══════════════════════════════════════════════════════════════════════════════
64
99
 
65
100
  function parseArgs(args) {
101
+ // Parse global flags first
66
102
  const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
67
103
 
68
104
  const opts = {
69
105
  path: globalFlags.path || process.cwd(),
106
+ truth: false,
107
+ reality: false,
108
+ realitySniff: false,
109
+ baseUrl: null,
70
110
  json: globalFlags.json || false,
71
111
  sarif: false,
72
112
  verbose: globalFlags.verbose || false,
73
113
  help: globalFlags.help || false,
74
- save: true,
114
+ autofix: false,
115
+ save: true, // Always save results by default
75
116
  noBanner: globalFlags.noBanner || false,
76
117
  ci: globalFlags.ci || false,
77
118
  quiet: globalFlags.quiet || false,
119
+ // Baseline tracking (fingerprints)
120
+ baseline: true, // Compare against baseline by default
121
+ updateBaseline: false, // --update-baseline to save current findings as baseline
122
+ // Allowlist subcommand
123
+ allowlist: null, // null = not using allowlist, or 'list' | 'add' | 'remove' | 'check'
124
+ allowlistId: null,
125
+ allowlistPattern: null,
126
+ allowlistReason: null,
127
+ allowlistScope: 'global',
128
+ allowlistFile: null,
129
+ allowlistExpires: null,
78
130
  };
79
131
 
132
+ // Parse command-specific args from cleanArgs
80
133
  for (let i = 0; i < cleanArgs.length; i++) {
81
134
  const arg = cleanArgs[i];
82
135
 
83
- if (arg === '--sarif') opts.sarif = true;
136
+ if (arg === '--truth' || arg === '-t') opts.truth = true;
137
+ else if (arg === '--reality' || arg === '-r') { opts.reality = true; opts.truth = true; }
138
+ else if (arg === '--reality-sniff' || arg === '--sniff') opts.realitySniff = true;
139
+ else if (arg === '--url' || arg === '-u') { opts.baseUrl = cleanArgs[++i]; opts.reality = true; opts.truth = true; }
140
+ else if (arg === '--sarif') opts.sarif = true;
141
+ else if (arg === '--autofix' || arg === '--fix' || arg === '-f') opts.autofix = true;
84
142
  else if (arg === '--no-save') opts.save = false;
143
+ else if (arg === '--no-baseline') opts.baseline = false;
144
+ else if (arg === '--update-baseline' || arg === '--set-baseline') opts.updateBaseline = true;
85
145
  else if (arg === '--path' || arg === '-p') opts.path = cleanArgs[++i] || process.cwd();
86
146
  else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
147
+ // Allowlist subcommand support
148
+ else if (arg === '--allowlist') {
149
+ const nextArg = cleanArgs[i + 1];
150
+ if (nextArg && !nextArg.startsWith('-')) {
151
+ opts.allowlist = nextArg;
152
+ i++;
153
+ } else {
154
+ opts.allowlist = 'list'; // Default to list
155
+ }
156
+ }
157
+ else if (arg === '--id' && opts.allowlist) opts.allowlistId = cleanArgs[++i];
158
+ else if (arg === '--pattern' && opts.allowlist) opts.allowlistPattern = cleanArgs[++i];
159
+ else if (arg === '--reason' && opts.allowlist) opts.allowlistReason = cleanArgs[++i];
160
+ else if (arg === '--scope' && opts.allowlist) opts.allowlistScope = cleanArgs[++i];
161
+ else if (arg === '--file' && opts.allowlist) opts.allowlistFile = cleanArgs[++i];
162
+ else if (arg === '--expires' && opts.allowlist) opts.allowlistExpires = cleanArgs[++i];
87
163
  else if (!arg.startsWith('-')) opts.path = path.resolve(arg);
88
164
  }
89
165
 
@@ -97,41 +173,70 @@ function printHelp(showBanner = true) {
97
173
  console.log(`
98
174
  ${ansi.bold}USAGE${ansi.reset}
99
175
  ${colors.accent}vibecheck scan${ansi.reset} [path] [options]
176
+
177
+ ${ansi.dim}Aliases: s, check${ansi.reset}
178
+
179
+ The core analysis engine. Scans your codebase for route integrity issues,
180
+ security vulnerabilities, code quality problems, and more.
181
+
182
+ ${ansi.bold}SCAN LAYERS${ansi.reset}
183
+ ${colors.accent}(default)${ansi.reset} Layer 1: AST static analysis ${ansi.dim}(fast, ~2s)${ansi.reset}
184
+ ${colors.accent}--truth, -t${ansi.reset} Layer 1+2: + build manifest verification ${ansi.dim}(CI/ship)${ansi.reset}
185
+ ${colors.accent}--reality, -r${ansi.reset} Layer 1+2+3: + Playwright runtime proof ${ansi.dim}[PRO]${ansi.reset}
186
+ ${colors.accent}--reality-sniff${ansi.reset} Include Reality Sniff AI artifact detection
187
+
188
+ ${ansi.bold}FIX MODE${ansi.reset} ${ansi.cyan}[STARTER]${ansi.reset}
189
+ ${colors.accent}--autofix, -f${ansi.reset} Generate AI fix missions for detected issues
190
+
191
+ ${ansi.bold}BASELINE TRACKING${ansi.reset}
192
+ ${colors.accent}--baseline${ansi.reset} Compare against previous scan ${ansi.dim}(default: on)${ansi.reset}
193
+ ${colors.accent}--no-baseline${ansi.reset} Skip baseline comparison
194
+ ${colors.accent}--update-baseline${ansi.reset} Save current findings as new baseline
100
195
 
101
- ${ansi.bold}QUICK SCAN${ansi.reset} - Runs 5 essential engines in < 3 seconds:
102
- • hardcoded-secrets-engine ${ansi.dim}(security)${ansi.reset}
103
- • console-logs-engine ${ansi.dim}(code hygiene)${ansi.reset}
104
- • ai-hallucination-engine ${ansi.dim}(vibecheck core)${ansi.reset}
105
- • type-aware-engine ${ansi.dim}(type safety)${ansi.reset}
106
- • error-handling-engine ${ansi.dim}(reliability)${ansi.reset}
196
+ ${ansi.bold}ALLOWLIST (suppress false positives)${ansi.reset}
197
+ ${colors.accent}--allowlist list${ansi.reset} Show all suppressed findings
198
+ ${colors.accent}--allowlist add --id <id> --reason "..."${ansi.reset}
199
+ ${colors.accent}--allowlist remove --id <id>${ansi.reset} Remove suppression
200
+ ${colors.accent}--allowlist check --id <id>${ansi.reset} Check if suppressed
107
201
 
108
202
  ${ansi.bold}OUTPUT OPTIONS${ansi.reset}
109
- ${colors.accent}--json${ansi.reset} Output as JSON
110
- ${colors.accent}--sarif${ansi.reset} SARIF format (GitHub code scanning)
203
+ ${colors.accent}--json${ansi.reset} Output as JSON ${ansi.dim}(machine-readable)${ansi.reset}
204
+ ${colors.accent}--sarif${ansi.reset} SARIF format ${ansi.dim}(GitHub code scanning)${ansi.reset}
111
205
  ${colors.accent}--no-save${ansi.reset} Don't save results to .vibecheck/results/
112
206
  ${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
113
207
 
114
208
  ${ansi.bold}GLOBAL OPTIONS${ansi.reset}
115
209
  ${colors.accent}--path, -p <dir>${ansi.reset} Run in specified directory
116
210
  ${colors.accent}--quiet, -q${ansi.reset} Suppress non-essential output
117
- ${colors.accent}--ci${ansi.reset} CI mode (quiet + no-banner)
211
+ ${colors.accent}--ci${ansi.reset} CI mode ${ansi.dim}(quiet + no-banner)${ansi.reset}
118
212
  ${colors.accent}--help, -h${ansi.reset} Show this help
119
213
 
120
214
  ${ansi.bold}💡 EXAMPLES${ansi.reset}
121
215
 
122
- ${ansi.dim}# Quick scan (default)${ansi.reset}
216
+ ${ansi.dim}# Quick scan (most common)${ansi.reset}
123
217
  vibecheck scan
124
218
 
125
- ${ansi.dim}# Quick scan with JSON output${ansi.reset}
126
- vibecheck scan --json
219
+ ${ansi.dim}# Scan with AI fix missions${ansi.reset}
220
+ vibecheck scan --autofix
127
221
 
128
- ${ansi.dim}# Scan a specific directory${ansi.reset}
129
- vibecheck scan ./my-project
222
+ ${ansi.dim}# Suppress a false positive${ansi.reset}
223
+ vibecheck scan --allowlist add --id R_DEAD_abc123 --reason "Feature toggle"
130
224
 
131
- ${ansi.bold}🚀 COMPREHENSIVE ANALYSIS${ansi.reset}
225
+ ${ansi.dim}# CI pipeline (JSON output, strict)${ansi.reset}
226
+ vibecheck scan --ci --json > results.json
132
227
 
133
- For 17+ engines plus ship-only validators, use:
134
- ${colors.accent}vibecheck ship${ansi.reset} ${ansi.magenta}[PRO]${ansi.reset}
228
+ ${ansi.dim}# Full reality proof (requires running app)${ansi.reset}
229
+ vibecheck scan --reality --url http://localhost:3000
230
+
231
+ ${ansi.bold}📄 OUTPUT${ansi.reset}
232
+ Results: .vibecheck/results/latest.json
233
+ Missions: .vibecheck/missions/ ${ansi.dim}(with --autofix)${ansi.reset}
234
+ History: .vibecheck/results/history/
235
+
236
+ ${ansi.bold}🔗 RELATED COMMANDS${ansi.reset}
237
+ ${colors.accent}vibecheck ship${ansi.reset} Get final SHIP/WARN/BLOCK verdict
238
+ ${colors.accent}vibecheck fix${ansi.reset} Apply AI-generated fixes ${ansi.cyan}[STARTER]${ansi.reset}
239
+ ${colors.accent}vibecheck prove${ansi.reset} Full proof pipeline with evidence ${ansi.magenta}[PRO]${ansi.reset}
135
240
 
136
241
  ${ansi.dim}─────────────────────────────────────────────────────────────${ansi.reset}
137
242
  ${ansi.dim}Documentation: https://docs.vibecheckai.dev/cli/scan${ansi.reset}
@@ -139,7 +244,444 @@ function printHelp(showBanner = true) {
139
244
  }
140
245
 
141
246
  // ═══════════════════════════════════════════════════════════════════════════════
142
- // MAIN SCAN FUNCTION
247
+ // ALLOWLIST MANAGEMENT (integrated from runAllowlist)
248
+ // ═══════════════════════════════════════════════════════════════════════════════
249
+
250
+ async function handleAllowlistCommand(opts) {
251
+ const root = opts.path || process.cwd();
252
+
253
+ // Load evidence-pack module for allowlist functions
254
+ let evidencePack;
255
+ try {
256
+ evidencePack = require("./lib/evidence-pack");
257
+ } catch (e) {
258
+ console.error(`${colors.error}✗${ansi.reset} Failed to load allowlist module: ${e.message}`);
259
+ return 1;
260
+ }
261
+
262
+ const action = opts.allowlist;
263
+
264
+ try {
265
+ switch (action) {
266
+ case "list": {
267
+ const allowlist = evidencePack.loadAllowlist(root);
268
+
269
+ if (opts.json) {
270
+ console.log(JSON.stringify(allowlist, null, 2));
271
+ return 0;
272
+ }
273
+
274
+ if (!opts.quiet) {
275
+ console.log(`\n 📋 ${ansi.bold}Allowlist Entries${ansi.reset}\n`);
276
+ }
277
+
278
+ if (!allowlist.entries || allowlist.entries.length === 0) {
279
+ console.log(` ${ansi.dim}No entries in allowlist.${ansi.reset}\n`);
280
+ console.log(` ${ansi.dim}Add entries with: vibecheck scan --allowlist add --id <id> --reason "..."${ansi.reset}\n`);
281
+ return 0;
282
+ }
283
+
284
+ for (const entry of allowlist.entries) {
285
+ const expireStr = entry.expiresAt
286
+ ? `${ansi.dim}expires ${new Date(entry.expiresAt).toLocaleDateString()}${ansi.reset}`
287
+ : '';
288
+ const scopeStr = entry.scope !== 'global'
289
+ ? `${colors.accent}[${entry.scope}]${ansi.reset} `
290
+ : '';
291
+
292
+ console.log(` ${colors.success}✓${ansi.reset} ${ansi.bold}${entry.id}${ansi.reset} ${scopeStr}${expireStr}`);
293
+
294
+ if (entry.findingId) {
295
+ console.log(` ${ansi.dim}Finding:${ansi.reset} ${entry.findingId}`);
296
+ }
297
+ if (entry.pattern) {
298
+ console.log(` ${ansi.dim}Pattern:${ansi.reset} ${entry.pattern}`);
299
+ }
300
+ console.log(` ${ansi.dim}Reason:${ansi.reset} ${entry.reason || 'No reason provided'}`);
301
+ console.log(` ${ansi.dim}Added:${ansi.reset} ${entry.addedAt} by ${entry.addedBy || 'user'}`);
302
+ console.log();
303
+ }
304
+
305
+ console.log(` ${ansi.dim}Total: ${allowlist.entries.length} entries${ansi.reset}\n`);
306
+ return 0;
307
+ }
308
+
309
+ case "add": {
310
+ if (!opts.allowlistId && !opts.allowlistPattern) {
311
+ console.error(`\n ${colors.error}✗${ansi.reset} Either --id or --pattern is required\n`);
312
+ return 1;
313
+ }
314
+
315
+ if (!opts.allowlistReason) {
316
+ console.error(`\n ${colors.error}✗${ansi.reset} --reason is required\n`);
317
+ return 1;
318
+ }
319
+
320
+ const entry = {
321
+ findingId: opts.allowlistId,
322
+ pattern: opts.allowlistPattern,
323
+ reason: opts.allowlistReason,
324
+ scope: opts.allowlistScope,
325
+ addedBy: 'cli'
326
+ };
327
+
328
+ if (opts.allowlistFile) entry.file = opts.allowlistFile;
329
+ if (opts.allowlistExpires) {
330
+ const days = parseInt(opts.allowlistExpires, 10);
331
+ entry.expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
332
+ }
333
+
334
+ const added = evidencePack.addToAllowlist(root, entry);
335
+
336
+ if (opts.json) {
337
+ console.log(JSON.stringify({ added }, null, 2));
338
+ return 0;
339
+ }
340
+
341
+ console.log(`\n ${colors.success}+${ansi.reset} Added allowlist entry: ${ansi.bold}${added.id}${ansi.reset}\n`);
342
+ return 0;
343
+ }
344
+
345
+ case "remove": {
346
+ if (!opts.allowlistId) {
347
+ console.error(`\n ${colors.error}✗${ansi.reset} --id is required for remove\n`);
348
+ return 1;
349
+ }
350
+
351
+ const allowlist = evidencePack.loadAllowlist(root);
352
+ const before = allowlist.entries.length;
353
+ allowlist.entries = allowlist.entries.filter(e => e.id !== opts.allowlistId && e.findingId !== opts.allowlistId);
354
+ const removed = before - allowlist.entries.length;
355
+
356
+ if (removed > 0) {
357
+ evidencePack.saveAllowlist(root, allowlist);
358
+ }
359
+
360
+ if (opts.json) {
361
+ console.log(JSON.stringify({ removed, remaining: allowlist.entries.length }, null, 2));
362
+ return 0;
363
+ }
364
+
365
+ if (removed > 0) {
366
+ console.log(`\n ${colors.success}-${ansi.reset} Removed ${removed} entry from allowlist\n`);
367
+ } else {
368
+ console.log(`\n ${colors.warning}⚠${ansi.reset} Entry not found: ${opts.allowlistId}\n`);
369
+ }
370
+ return 0;
371
+ }
372
+
373
+ case "check": {
374
+ if (!opts.allowlistId) {
375
+ console.error(`\n ${colors.error}✗${ansi.reset} --id is required for check\n`);
376
+ return 1;
377
+ }
378
+
379
+ const allowlist = evidencePack.loadAllowlist(root);
380
+ const result = evidencePack.isAllowlisted({ id: opts.allowlistId }, allowlist);
381
+
382
+ if (opts.json) {
383
+ console.log(JSON.stringify(result, null, 2));
384
+ return result.allowed ? 0 : 1;
385
+ }
386
+
387
+ if (result.allowed) {
388
+ console.log(`\n ${colors.success}✓${ansi.reset} ${ansi.bold}Allowlisted${ansi.reset}`);
389
+ console.log(` ${ansi.dim}Reason:${ansi.reset} ${result.reason}`);
390
+ console.log(` ${ansi.dim}Entry:${ansi.reset} ${result.entry?.id}\n`);
391
+ } else {
392
+ console.log(`\n ${colors.warning}⚠${ansi.reset} ${ansi.bold}Not allowlisted${ansi.reset}\n`);
393
+ }
394
+ return result.allowed ? 0 : 1;
395
+ }
396
+
397
+ default:
398
+ console.error(`\n ${colors.error}✗${ansi.reset} Unknown allowlist action: ${action}\n`);
399
+ console.log(` ${ansi.dim}Available: list, add, remove, check${ansi.reset}\n`);
400
+ return 1;
401
+ }
402
+ } catch (error) {
403
+ if (opts.json) {
404
+ console.log(JSON.stringify({ error: error.message }, null, 2));
405
+ } else {
406
+ console.error(`\n ${colors.error}✗${ansi.reset} ${error.message}\n`);
407
+ }
408
+ return 1;
409
+ }
410
+ }
411
+
412
+ // ═══════════════════════════════════════════════════════════════════════════════
413
+ // AUTOFIX & MISSION GENERATION
414
+ // ═══════════════════════════════════════════════════════════════════════════════
415
+
416
+ /**
417
+ * Normalize severity values to standard format
418
+ */
419
+ function normalizeSeverity(severity) {
420
+ if (!severity) return 'medium';
421
+ const sev = String(severity).toLowerCase();
422
+ if (sev === 'block' || sev === 'critical') return 'critical';
423
+ if (sev === 'high') return 'high';
424
+ if (sev === 'warn' || sev === 'warning' || sev === 'medium') return 'medium';
425
+ if (sev === 'info' || sev === 'low') return 'low';
426
+ return 'medium'; // Default fallback
427
+ }
428
+
429
+ async function generateMissions(findings, projectPath, opts) {
430
+ const missionsDir = path.join(projectPath, '.vibecheck', 'missions');
431
+
432
+ // Ensure missions directory exists
433
+ if (!fs.existsSync(missionsDir)) {
434
+ fs.mkdirSync(missionsDir, { recursive: true });
435
+ }
436
+
437
+ const missions = [];
438
+ const missionIndex = [];
439
+
440
+ for (let i = 0; i < findings.length; i++) {
441
+ const finding = findings[i];
442
+ const missionId = `mission-${String(i + 1).padStart(3, '0')}`;
443
+ const normalizedSeverity = normalizeSeverity(finding.severity);
444
+
445
+ // Generate AI-ready mission prompt
446
+ const mission = {
447
+ id: missionId,
448
+ createdAt: new Date().toISOString(),
449
+ finding: {
450
+ id: finding.id || `finding-${i + 1}`,
451
+ severity: normalizedSeverity,
452
+ originalSeverity: finding.severity, // Keep original for reference
453
+ category: finding.category,
454
+ title: finding.title || finding.message,
455
+ file: finding.file,
456
+ line: finding.line,
457
+ },
458
+ prompt: generateMissionPrompt(finding),
459
+ constraints: generateConstraints(finding),
460
+ verification: generateVerificationSteps(finding),
461
+ status: 'pending',
462
+ };
463
+
464
+ missions.push(mission);
465
+ missionIndex.push({
466
+ id: missionId,
467
+ severity: normalizedSeverity,
468
+ title: finding.title || finding.message,
469
+ file: finding.file,
470
+ status: 'pending',
471
+ });
472
+
473
+ // Save individual mission JSON
474
+ const missionPath = path.join(missionsDir, `${missionId}.json`);
475
+ fs.writeFileSync(missionPath, JSON.stringify(mission, null, 2));
476
+ }
477
+
478
+ // Generate MISSIONS.md index
479
+ const missionsMarkdown = generateMissionsMarkdown(missionIndex, projectPath);
480
+ fs.writeFileSync(path.join(missionsDir, 'MISSIONS.md'), missionsMarkdown);
481
+
482
+ // Save missions summary
483
+ const summary = {
484
+ generatedAt: new Date().toISOString(),
485
+ totalMissions: missions.length,
486
+ bySeverity: {
487
+ critical: missionIndex.filter(m => m.severity === 'critical').length,
488
+ high: missionIndex.filter(m => m.severity === 'high').length,
489
+ medium: missionIndex.filter(m => m.severity === 'medium').length,
490
+ low: missionIndex.filter(m => m.severity === 'low').length,
491
+ },
492
+ missions: missionIndex,
493
+ };
494
+ fs.writeFileSync(path.join(missionsDir, 'summary.json'), JSON.stringify(summary, null, 2));
495
+
496
+ return { missions, summary };
497
+ }
498
+
499
+ function generateMissionPrompt(finding) {
500
+ const file = finding.file || 'unknown file';
501
+ const line = finding.line ? ` at line ${finding.line}` : '';
502
+ const title = finding.title || finding.message || 'Unknown issue';
503
+ const category = finding.category || 'general';
504
+
505
+ let prompt = `## Mission: Fix ${title}
506
+
507
+ **File:** \`${file}\`${line}
508
+ **Category:** ${category}
509
+ **Severity:** ${finding.severity || 'medium'}
510
+
511
+ ### Problem
512
+ ${finding.message || title}
513
+
514
+ ### Context
515
+ `;
516
+
517
+ // Add category-specific context
518
+ if (category === 'ROUTE' || category === 'routes') {
519
+ prompt += `This is a route integrity issue. The route may be:
520
+ - Referenced but not defined
521
+ - Defined but not reachable
522
+ - Missing proper error handling
523
+ `;
524
+ } else if (category === 'ENV' || category === 'env') {
525
+ prompt += `This is an environment variable issue. The variable may be:
526
+ - Used but not defined in .env
527
+ - Missing from .env.example
528
+ - Not documented
529
+ `;
530
+ } else if (category === 'AUTH' || category === 'auth' || category === 'security') {
531
+ prompt += `This is a security/authentication issue. Check for:
532
+ - Proper authentication middleware
533
+ - Authorization checks
534
+ - Secure defaults
535
+ `;
536
+ } else if (category === 'MOCK' || category === 'mock') {
537
+ prompt += `This is a mock/placeholder issue. The code may contain:
538
+ - TODO comments that need implementation
539
+ - Placeholder data that should be real
540
+ - Fake success responses
541
+ `;
542
+ }
543
+
544
+ prompt += `
545
+ ### Requirements
546
+ 1. Fix the issue while maintaining existing functionality
547
+ 2. Follow the project's coding style and patterns
548
+ 3. Add appropriate error handling
549
+ 4. Update tests if they exist
550
+
551
+ ### Verification
552
+ After making changes:
553
+ 1. Run \`vibecheck scan\` to verify the issue is resolved
554
+ 2. Run any existing tests for the affected file
555
+ 3. Manually verify the functionality if applicable
556
+ `;
557
+
558
+ if (finding.fix || finding.fixSuggestion) {
559
+ prompt += `
560
+ ### Suggested Fix
561
+ ${finding.fix || finding.fixSuggestion}
562
+ `;
563
+ }
564
+
565
+ return prompt;
566
+ }
567
+
568
+ function generateConstraints(finding) {
569
+ const constraints = [
570
+ 'Do not break existing functionality',
571
+ 'Follow existing code patterns in the project',
572
+ 'Add error handling where appropriate',
573
+ ];
574
+
575
+ if (finding.category === 'security' || finding.category === 'AUTH') {
576
+ constraints.push('Ensure no security regressions');
577
+ constraints.push('Follow OWASP security guidelines');
578
+ }
579
+
580
+ if (finding.category === 'ROUTE' || finding.category === 'routes') {
581
+ constraints.push('Maintain API backwards compatibility');
582
+ constraints.push('Update route documentation if changed');
583
+ }
584
+
585
+ return constraints;
586
+ }
587
+
588
+ function generateVerificationSteps(finding) {
589
+ const steps = [
590
+ 'Run `vibecheck scan` and confirm this issue no longer appears',
591
+ 'Run `vibecheck checkpoint` to ensure no regressions',
592
+ ];
593
+
594
+ if (finding.file) {
595
+ steps.push(`Review changes in \`${finding.file}\``);
596
+ }
597
+
598
+ if (finding.category === 'ROUTE' || finding.category === 'routes') {
599
+ steps.push('Test the affected route(s) manually or with automated tests');
600
+ }
601
+
602
+ if (finding.category === 'ENV' || finding.category === 'env') {
603
+ steps.push('Verify environment variable is properly documented');
604
+ steps.push('Check .env.example is updated');
605
+ }
606
+
607
+ return steps;
608
+ }
609
+
610
+ function generateMissionsMarkdown(missionIndex, projectPath) {
611
+ const projectName = path.basename(projectPath);
612
+ const now = new Date().toISOString();
613
+
614
+ let md = `# Vibecheck Missions
615
+
616
+ > Generated: ${now}
617
+ > Project: ${projectName}
618
+
619
+ ## Summary
620
+
621
+ | Severity | Count |
622
+ |----------|-------|
623
+ | Critical | ${missionIndex.filter(m => m.severity === 'critical').length} |
624
+ | High | ${missionIndex.filter(m => m.severity === 'high').length} |
625
+ | Medium | ${missionIndex.filter(m => m.severity === 'medium').length} |
626
+ | Low | ${missionIndex.filter(m => m.severity === 'low').length} |
627
+ | **Total** | **${missionIndex.length}** |
628
+
629
+ ## Missions
630
+
631
+ `;
632
+
633
+ // Group by severity
634
+ const bySeverity = {
635
+ critical: missionIndex.filter(m => m.severity === 'critical'),
636
+ high: missionIndex.filter(m => m.severity === 'high'),
637
+ medium: missionIndex.filter(m => m.severity === 'medium'),
638
+ low: missionIndex.filter(m => m.severity === 'low'),
639
+ };
640
+
641
+ for (const [severity, missions] of Object.entries(bySeverity)) {
642
+ if (missions.length === 0) continue;
643
+
644
+ const emoji = severity === 'critical' ? '🔴' : severity === 'high' ? '🟠' : severity === 'medium' ? '🟡' : '🔵';
645
+ md += `### ${emoji} ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${missions.length})\n\n`;
646
+
647
+ for (const mission of missions) {
648
+ const checkbox = mission.status === 'completed' ? '[x]' : '[ ]';
649
+ md += `- ${checkbox} **${mission.id}**: ${mission.title}\n`;
650
+ if (mission.file) {
651
+ md += ` - File: \`${mission.file}\`\n`;
652
+ }
653
+ }
654
+ md += '\n';
655
+ }
656
+
657
+ md += `---
658
+
659
+ ## How to Use
660
+
661
+ 1. **Review missions**: Read each mission file in \`.vibecheck/missions/\`
662
+ 2. **Copy prompts**: Use the prompt in each mission file with your AI assistant
663
+ 3. **Verify fixes**: Run \`vibecheck scan\` after each fix
664
+ 4. **Track progress**: Update mission status in this file
665
+
666
+ ## Commands
667
+
668
+ \`\`\`bash
669
+ # Re-scan after fixes
670
+ vibecheck scan
671
+
672
+ # Check progress
673
+ vibecheck checkpoint
674
+
675
+ # Final verification
676
+ vibecheck ship
677
+ \`\`\`
678
+ `;
679
+
680
+ return md;
681
+ }
682
+
683
+ // ═══════════════════════════════════════════════════════════════════════════════
684
+ // MAIN SCAN FUNCTION - ROUTE INTEGRITY SYSTEM
143
685
  // ═══════════════════════════════════════════════════════════════════════════════
144
686
 
145
687
  async function runScan(args) {
@@ -151,248 +693,929 @@ async function runScan(args) {
151
693
  return 0;
152
694
  }
153
695
 
154
- // Entitlement check (scan is FREE)
696
+ // CRITICAL: Check for JSON/SARIF output FIRST - skip all banners/output if these flags are set
697
+ if (opts.json || opts.sarif) {
698
+ process.env.NO_COLOR = '1';
699
+ // Skip all banner/output until we output JSON/SARIF
700
+ }
701
+
702
+ // Handle --allowlist subcommand
703
+ if (opts.allowlist) {
704
+ return await handleAllowlistCommand(opts);
705
+ }
706
+
707
+ // Entitlement check (graceful offline handling)
155
708
  try {
156
709
  await enforceLimit('scans');
157
710
  await trackUsage('scans');
158
711
  } catch (err) {
159
712
  if (err.code === 'LIMIT_EXCEEDED') {
160
- console.error(err.upgradePrompt || err.message);
713
+ if (!opts.json && !opts.sarif) {
714
+ console.error(err.upgradePrompt || err.message);
715
+ }
161
716
  return 1;
162
717
  }
163
- // Network errors - continue with free tier
164
- if (!['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'].includes(err.code) && err.name !== 'NetworkError') {
165
- throw err;
718
+ // Network error - fall back to free tier only (SECURITY: never grant paid features offline)
719
+ if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND' || err.name === 'NetworkError') {
720
+ if (!opts.json && !opts.sarif) {
721
+ console.warn(` ${colors.warning}⚠${ansi.reset} API unavailable, running in ${colors.success}FREE${ansi.reset} tier mode`);
722
+ console.warn(` ${ansi.dim}Paid features require API connection. Continuing with free features only.${ansi.reset}\n`);
723
+ }
724
+ // Continue with free tier features only - scan command is free tier
725
+ } else {
726
+ throw err; // Re-throw unexpected errors
166
727
  }
167
728
  }
168
729
 
169
- // Print banner
170
- if (shouldShowBanner(opts)) {
730
+ // Print banner (respects --no-banner, VIBECHECK_NO_BANNER, --ci, --quiet, --json, --sarif)
731
+ if (shouldShowBanner(opts) && !opts.json && !opts.sarif) {
171
732
  printBanner();
172
733
  }
173
734
 
174
735
  const projectPath = path.resolve(opts.path);
175
736
  const startTime = Date.now();
176
- const projectName = path.basename(projectPath);
177
737
 
178
- // Emit audit event
738
+ // Emit audit event for scan start
179
739
  emitScanStart(projectPath, args);
740
+ const projectName = path.basename(projectPath);
741
+
742
+ // Initialize API integration
743
+ let apiScan = null;
744
+ let apiConnected = false;
745
+
746
+ // Try to connect to API for dashboard integration
747
+ try {
748
+ apiConnected = await isApiAvailable();
749
+ if (apiConnected) {
750
+ // Create scan record in dashboard
751
+ apiScan = await createScan({
752
+ localPath: projectPath,
753
+ branch: opts.branch || 'main',
754
+ enableLLM: opts.llm || false,
755
+ });
756
+ if (!opts.json && !opts.sarif) {
757
+ console.log(`${colors.info}📡${ansi.reset} Connected to dashboard (Scan ID: ${apiScan.scanId})`);
758
+ }
759
+ }
760
+ } catch (err) {
761
+ // API connection is optional, continue without it
762
+ if (!opts.json && !opts.sarif) {
763
+ console.log(`${colors.dim}ℹ${ansi.reset} Dashboard integration unavailable`);
764
+ }
765
+ }
180
766
 
181
767
  // Validate project path
182
768
  if (!fs.existsSync(projectPath)) {
183
769
  throw createUserError(`Project path does not exist: ${projectPath}`, "ValidationError");
184
770
  }
185
771
 
186
- // Print scan info
187
- if (!opts.json && !opts.quiet) {
772
+ // Determine layers
773
+ const layers = {
774
+ ast: true,
775
+ truth: opts.truth,
776
+ reality: opts.reality,
777
+ realitySniff: opts.realitySniff,
778
+ };
779
+
780
+ // Print scan info (skip if JSON/SARIF mode)
781
+ if (!opts.json && !opts.sarif) {
782
+ const layerNames = [];
783
+ if (layers.ast) layerNames.push('AST');
784
+ if (layers.truth) layerNames.push('Truth');
785
+ if (layers.reality) layerNames.push('Reality');
786
+ if (layers.realitySniff) layerNames.push('Reality Sniff');
787
+
188
788
  console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${projectName}${ansi.reset}`);
189
789
  console.log(` ${ansi.dim}Path:${ansi.reset} ${projectPath}`);
190
- console.log(` ${ansi.dim}Mode:${ansi.reset} ${colors.accent}Quick Scan${ansi.reset} ${ansi.dim}(${SCAN_ENGINES.length} engines)${ansi.reset}`);
790
+ console.log(` ${ansi.dim}Layers:${ansi.reset} ${colors.accent}${layerNames.join(' → ')}${ansi.reset}`);
191
791
  console.log();
192
792
  }
193
793
 
794
+ // Reality layer requires URL
795
+ if (opts.reality && !opts.baseUrl) {
796
+ if (!opts.json && !opts.sarif) {
797
+ console.log(` ${colors.warning}⚠${ansi.reset} ${ansi.bold}Reality layer requires --url${ansi.reset}`);
798
+ console.log(` ${ansi.dim}Example: vibecheck scan --reality --url http://localhost:3000${ansi.reset}`);
799
+ console.log();
800
+ }
801
+ return EXIT.USER_ERROR;
802
+ }
803
+
804
+ // Initialize spinner outside try block for error handling
194
805
  let spinner = null;
195
806
 
196
807
  try {
197
- // Start spinner
198
- if (!opts.json && !opts.quiet) {
808
+ // Import systems - try TypeScript compiled first, fallback to JS runtime
809
+ let scanRouteIntegrity;
810
+ let useFallbackScanner = false;
811
+
812
+ try {
813
+ scanRouteIntegrity = require('../../dist/lib/route-integrity').scanRouteIntegrity;
814
+ } catch (e) {
815
+ // Fallback to JS-based scanner using truth.js and analyzers.js
816
+ useFallbackScanner = true;
817
+ const { buildTruthpackSmart } = require('./lib/truth');
818
+ const {
819
+ findMissingRoutes,
820
+ findEnvGaps,
821
+ findFakeSuccess,
822
+ findGhostAuth,
823
+ findMockData,
824
+ findTodoFixme,
825
+ findConsoleLogs,
826
+ findHardcodedSecrets,
827
+ findDeadCode,
828
+ findDeprecatedApis,
829
+ findEmptyCatch,
830
+ findUnsafeRegex,
831
+ findSecurityVulnerabilities,
832
+ findPerformanceIssues,
833
+ findCodeQualityIssues,
834
+ findCrossFileIssues,
835
+ findTypeSafetyIssues,
836
+ findAccessibilityIssues,
837
+ findAPIConsistencyIssues,
838
+ // NEW: AI Hallucination Detectors
839
+ findOptimisticNoRollback,
840
+ findSilentCatch,
841
+ findMethodMismatch,
842
+ findDeadUI,
843
+ clearFileCache, // V3: Memory management
844
+ } = require('./lib/analyzers');
845
+
846
+ scanRouteIntegrity = async function({ projectPath, layers, baseUrl, verbose }) {
847
+ // Build truthpack for route analysis
848
+ const truthpack = await buildTruthpackSmart({ repoRoot: projectPath });
849
+
850
+ // Run ALL analyzers for comprehensive scanning
851
+ const findings = [];
852
+
853
+ // Core analyzers (route integrity, env, auth)
854
+ findings.push(...findMissingRoutes(truthpack));
855
+ findings.push(...findMethodMismatch(truthpack));
856
+ findings.push(...findEnvGaps(truthpack));
857
+ findings.push(...findFakeSuccess(projectPath));
858
+ findings.push(...findOptimisticNoRollback(projectPath));
859
+ findings.push(...findSilentCatch(projectPath));
860
+ findings.push(...findDeadUI(projectPath));
861
+ findings.push(...findGhostAuth(truthpack, projectPath));
862
+
863
+ // Code quality analyzers (MOCK, TODO, console.log, etc.)
864
+ findings.push(...findMockData(projectPath));
865
+ findings.push(...findTodoFixme(projectPath));
866
+ findings.push(...findConsoleLogs(projectPath));
867
+ findings.push(...findHardcodedSecrets(projectPath));
868
+ findings.push(...findDeadCode(projectPath));
869
+ findings.push(...findDeprecatedApis(projectPath));
870
+ findings.push(...findEmptyCatch(projectPath));
871
+ findings.push(...findUnsafeRegex(projectPath));
872
+
873
+ // Enhanced analyzers (Security, Performance, Code Quality)
874
+ findings.push(...findSecurityVulnerabilities(projectPath));
875
+ findings.push(...findPerformanceIssues(projectPath));
876
+ findings.push(...findCodeQualityIssues(projectPath));
877
+
878
+ // V3: Clear file cache and AST cache to prevent memory leaks in large monorepos
879
+ clearFileCache();
880
+ const engines = require("./lib/engines/vibecheck-engines");
881
+ if (engines.clearASTCache) {
882
+ engines.clearASTCache();
883
+ }
884
+
885
+ // Convert to scan format matching TypeScript scanner output
886
+ const shipBlockers = findings.map((f, i) => ({
887
+ id: f.id || `finding-${i}`,
888
+ ruleId: f.category,
889
+ category: f.category,
890
+ severity: f.severity === 'BLOCK' ? 'critical' : f.severity === 'WARN' ? 'warning' : 'info',
891
+ title: f.title,
892
+ message: f.title,
893
+ description: f.why,
894
+ file: f.evidence?.[0]?.file || '',
895
+ line: parseInt(f.evidence?.[0]?.lines?.split('-')[0]) || 1,
896
+ evidence: f.evidence || [],
897
+ fixHints: f.fixHints || [],
898
+ autofixAvailable: false,
899
+ verdict: f.severity === 'BLOCK' ? 'FAIL' : 'WARN',
900
+ }));
901
+
902
+ // Return structure matching TypeScript scanner
903
+ return {
904
+ report: {
905
+ shipBlockers,
906
+ realitySniffFindings: [],
907
+ routeMap: truthpack.routes,
908
+ summary: {
909
+ total: shipBlockers.length,
910
+ critical: shipBlockers.filter(f => f.severity === 'critical').length,
911
+ warning: shipBlockers.filter(f => f.severity === 'warning').length,
912
+ },
913
+ verdict: shipBlockers.some(f => f.severity === 'critical') ? 'BLOCK' :
914
+ shipBlockers.some(f => f.severity === 'warning') ? 'WARN' : 'SHIP'
915
+ },
916
+ outputPaths: {},
917
+ truthpack
918
+ };
919
+ };
920
+ }
921
+
922
+ // Try to import new unified output system (may not be compiled yet)
923
+ let buildVerdictOutput, normalizeFinding, formatStandardOutput, formatScanOutputFromUnified, getExitCodeFromUnified, CacheManager;
924
+ let useUnifiedOutput = false;
925
+
926
+ try {
927
+ const outputContract = require('../../dist/lib/cli/output-contract');
928
+ buildVerdictOutput = outputContract.buildVerdictOutput;
929
+ normalizeFinding = outputContract.normalizeFinding;
930
+ formatStandardOutput = outputContract.formatStandardOutput;
931
+
932
+ const unifiedOutput = require('./lib/unified-output');
933
+ formatScanOutputFromUnified = unifiedOutput.formatScanOutput;
934
+ getExitCodeFromUnified = unifiedOutput.getExitCode;
935
+
936
+ const cacheModule = require('../../dist/lib/cli/cache-manager');
937
+ CacheManager = cacheModule.CacheManager;
938
+ useUnifiedOutput = true;
939
+ } catch (error) {
940
+ // Fallback to old system if new one not available
941
+ if (opts.verbose) {
942
+ console.warn('Unified output system not available, using enhanced format');
943
+ }
944
+ useUnifiedOutput = false;
945
+ }
946
+
947
+ // Initialize cache if available
948
+ let cache = null;
949
+ let cached = false;
950
+ let cachedResult = null;
951
+
952
+ if (CacheManager) {
953
+ cache = new CacheManager(projectPath);
954
+ const cacheKey = 'scan';
955
+
956
+ // Compute project hash for caching
957
+ const sourceFiles = await findSourceFiles(projectPath);
958
+ const projectHash = await cache.computeProjectHash(sourceFiles, { layers, baseUrl: opts.baseUrl });
959
+
960
+ // Check cache
961
+ if (!opts.verbose) {
962
+ cachedResult = await cache.get(cacheKey, projectHash);
963
+ if (cachedResult && buildVerdictOutput) {
964
+ cached = true;
965
+ // Use cached result
966
+ const verdict = buildVerdictOutput(cachedResult.findings, cachedResult.timings, true);
967
+ const output = formatStandardOutput(verdict, cachedResult.findings, cachedResult.scanId, projectPath, {
968
+ version: require('../../package.json').version || '1.0.0',
969
+ nodeVersion: process.version,
970
+ platform: process.platform,
971
+ });
972
+
973
+ if (opts.json) {
974
+ console.log(JSON.stringify(output, null, 2));
975
+ return getExitCode(verdict);
976
+ }
977
+
978
+ console.log(formatScanOutput({ verdict, findings: cachedResult.findings, cached }, { verbose: opts.verbose }));
979
+ return getExitCode(verdict);
980
+ }
981
+ }
982
+ }
983
+
984
+ // Start scanning with enhanced spinner
985
+ const timings = { discovery: 0, analysis: 0, verification: 0, detection: 0, total: 0 };
986
+ timings.discovery = Date.now();
987
+
988
+ // Only show spinner if not in JSON/SARIF mode
989
+ if (!opts.json && !opts.sarif) {
199
990
  spinner = new Spinner({ color: colors.primary });
200
- spinner.start(`Running quick scan...`);
991
+ spinner.start('Analyzing codebase...');
201
992
  }
202
993
 
203
- // Run the 5 quick scan engines via orchestrator
204
- const scanResult = await runScanEngines(projectPath, {
205
- maxFiles: 500,
206
- onProgress: opts.verbose ? (phase, pct) => {
207
- if (spinner) spinner.text = `${phase}: ${pct}%`;
994
+ const result = await scanRouteIntegrity({
995
+ projectPath,
996
+ layers,
997
+ baseUrl: opts.baseUrl,
998
+ verbose: opts.verbose,
999
+ onProgress: opts.verbose ? (phase, progress) => {
1000
+ spinner.succeed(`${phase}: ${Math.round(progress)}%`);
1001
+ if (progress < 100) spinner.start(`Running ${phase}...`);
208
1002
  } : undefined,
209
1003
  });
1004
+
1005
+ timings.analysis = Date.now() - timings.discovery;
1006
+
1007
+ // Run new detection engines (Dead UI, Billing Bypass, Fake Success)
1008
+ let detectionFindings = [];
1009
+ try {
1010
+ if (spinner) spinner.start('Running detection engines...');
1011
+ const detectionStart = Date.now();
1012
+
1013
+ // Dynamic import for TypeScript detection engines
1014
+ const { DeadUIDetector } = require('../../dist/engines/detection/dead-ui-detector');
1015
+ const { BillingBypassDetector } = require('../../dist/engines/detection/billing-bypass-detector');
1016
+ const { FakeSuccessDetector } = require('../../dist/engines/detection/fake-success-detector');
1017
+
1018
+ // Load ignore patterns from .vibecheckignore + defaults
1019
+ const { loadIgnorePatterns } = require('./lib/agent-firewall/utils/ignore-checker');
1020
+ const ignorePatterns = loadIgnorePatterns(projectPath);
1021
+ const exclude = [
1022
+ // Default excludes
1023
+ 'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '_archive',
1024
+ // From .vibecheckignore
1025
+ 'mcp-server', 'bin', 'packages/cli', 'tests', '__tests__', 'examples',
1026
+ 'templates', 'docs', '.guardrail', '.cursor', '.vibecheck',
1027
+ // Test files
1028
+ '*.test.ts', '*.test.tsx', '*.spec.ts', '*.spec.tsx', '*.test.js', '*.spec.js',
1029
+ // Type definitions
1030
+ '*.d.ts', '*.d.ts.map',
1031
+ ];
1032
+
1033
+ // Run detectors in parallel
1034
+ const [deadUIResult, billingResult, fakeSuccessResult] = await Promise.all([
1035
+ new DeadUIDetector(projectPath).scan({ exclude }),
1036
+ new BillingBypassDetector(projectPath).scan({ exclude }),
1037
+ new FakeSuccessDetector(projectPath).scan({ exclude }),
1038
+ ]);
1039
+
1040
+ // Convert to normalized findings format
1041
+ for (const finding of deadUIResult.findings) {
1042
+ detectionFindings.push({
1043
+ id: finding.id,
1044
+ ruleId: finding.type,
1045
+ category: 'DEAD_UI',
1046
+ severity: finding.severity,
1047
+ title: finding.message,
1048
+ description: finding.suggestion,
1049
+ file: finding.file,
1050
+ line: finding.line,
1051
+ evidence: finding.evidence,
1052
+ autofixAvailable: false,
1053
+ verdict: 'FAIL',
1054
+ });
1055
+ }
1056
+
1057
+ for (const finding of billingResult.findings) {
1058
+ detectionFindings.push({
1059
+ id: finding.id,
1060
+ ruleId: finding.type,
1061
+ category: 'BILLING',
1062
+ severity: finding.severity,
1063
+ title: finding.message,
1064
+ description: finding.suggestion,
1065
+ file: finding.file,
1066
+ line: finding.line,
1067
+ evidence: finding.evidence,
1068
+ autofixAvailable: false,
1069
+ verdict: 'FAIL',
1070
+ });
1071
+ }
1072
+
1073
+ for (const finding of fakeSuccessResult.findings) {
1074
+ detectionFindings.push({
1075
+ id: finding.id,
1076
+ ruleId: finding.type,
1077
+ category: 'FAKE_SUCCESS',
1078
+ severity: finding.severity,
1079
+ title: finding.message,
1080
+ description: finding.suggestion,
1081
+ file: finding.file,
1082
+ line: finding.line,
1083
+ evidence: finding.evidence,
1084
+ autofixAvailable: false,
1085
+ verdict: 'FAIL',
1086
+ });
1087
+ }
1088
+
1089
+ timings.detection = Date.now() - detectionStart;
1090
+ if (spinner && !opts.json && !opts.sarif) {
1091
+ spinner.succeed(`Detection complete (${detectionFindings.length} findings)`);
1092
+ }
1093
+ } catch (detectionError) {
1094
+ // Detection engines not compiled yet - continue without them
1095
+ if (opts.verbose && !opts.json && !opts.sarif) {
1096
+ console.log(` ${ansi.dim}Detection engines not available: ${detectionError.message}${ansi.reset}`);
1097
+ }
1098
+ if (spinner && !opts.json && !opts.sarif) {
1099
+ spinner.warn('Detection skipped (not compiled)');
1100
+ }
1101
+ }
210
1102
 
211
- const duration = Date.now() - startTime;
1103
+ timings.verification = Date.now() - timings.analysis - timings.discovery;
1104
+ timings.total = Date.now() - startTime;
212
1105
 
213
- if (spinner) {
214
- spinner.succeed(`Quick scan complete (${scanResult.findings.length} findings in ${formatDuration(duration)})`);
1106
+ if (spinner && !opts.json && !opts.sarif) {
1107
+ spinner.succeed('Analysis complete');
215
1108
  }
216
1109
 
217
- // Normalize findings
218
- const findings = scanResult.findings.map((f, i) => ({
219
- id: f.id || `finding-${i}`,
220
- ruleId: f.type || f.category,
221
- category: f.category || 'CodeQuality',
222
- severity: normalizeSeverity(f.severity),
223
- title: f.title || f.message,
224
- message: f.message || f.title,
225
- file: f.file || '',
226
- line: f.line || 1,
227
- confidence: f.confidence || 'medium',
228
- engine: f.engine,
229
- fixHint: f.fixHint,
230
- }));
231
-
232
- // Calculate verdict
233
- const blockers = findings.filter(f => f.severity === 'critical');
234
- const warnings = findings.filter(f => f.severity === 'warning');
235
- const verdict = blockers.length > 0 ? 'BLOCK' : warnings.length > 0 ? 'WARN' : 'PASS';
236
- const score = calculateScore({
237
- critical: blockers.length,
238
- high: 0,
239
- medium: warnings.length,
240
- low: findings.length - blockers.length - warnings.length
241
- });
1110
+ const { report, outputPaths } = result;
242
1111
 
243
- // ═══════════════════════════════════════════════════════════════════════════
244
- // OUTPUT
245
- // ═══════════════════════════════════════════════════════════════════════════
246
-
247
- // JSON output
1112
+ // Use new unified output if available, otherwise fallback to enhanced format
1113
+ if (useUnifiedOutput && buildVerdictOutput && normalizeFinding && formatScanOutputFromUnified) {
1114
+ // Normalize findings with stable IDs
1115
+ const existingIDs = new Set();
1116
+ const normalizedFindings = [];
1117
+
1118
+ // Normalize route integrity findings
1119
+ if (report.shipBlockers) {
1120
+ for (let i = 0; i < report.shipBlockers.length; i++) {
1121
+ const blocker = report.shipBlockers[i];
1122
+ const category = blocker.category || 'ROUTE';
1123
+ const normalized = normalizeFinding(blocker, category, i, existingIDs);
1124
+ normalizedFindings.push(normalized);
1125
+ }
1126
+ }
1127
+
1128
+ // Normalize Reality Sniff findings if present
1129
+ if (report.realitySniffFindings) {
1130
+ for (let i = 0; i < report.realitySniffFindings.length; i++) {
1131
+ const finding = report.realitySniffFindings[i];
1132
+ const category = finding.ruleId?.startsWith('auth') ? 'AUTH' : 'REALITY';
1133
+ const normalized = normalizeFinding(finding, category, normalizedFindings.length, existingIDs);
1134
+ normalizedFindings.push(normalized);
1135
+ }
1136
+ }
1137
+
1138
+ // Add detection engine findings (Dead UI, Billing, Fake Success)
1139
+ for (const finding of detectionFindings) {
1140
+ normalizedFindings.push(finding);
1141
+ }
1142
+
1143
+ // Build verdict
1144
+ const verdict = buildVerdictOutput(normalizedFindings, timings, false);
1145
+ const scanId = `scan_${Date.now()}`;
1146
+
1147
+ // Cache result
1148
+ if (cache) {
1149
+ const sourceFiles = await findSourceFiles(projectPath);
1150
+ const projectHash = await cache.computeProjectHash(sourceFiles, { layers, baseUrl: opts.baseUrl });
1151
+ await cache.set('scan', projectHash, {
1152
+ findings: normalizedFindings,
1153
+ timings,
1154
+ scanId,
1155
+ }, {
1156
+ filesScanned: sourceFiles.length,
1157
+ findings: normalizedFindings.length,
1158
+ duration: timings.total,
1159
+ });
1160
+ }
1161
+
1162
+ // Build standard output
1163
+ const standardOutput = formatStandardOutput(verdict, normalizedFindings, scanId, projectPath, {
1164
+ version: require('../../package.json').version || '1.0.0',
1165
+ nodeVersion: process.version,
1166
+ platform: process.platform,
1167
+ });
1168
+
1169
+ // JSON output mode - CRITICAL: Output ONLY JSON, no banners/colors
248
1170
  if (opts.json) {
1171
+ // Ensure pure JSON output with no ANSI codes or banners
1172
+ process.env.NO_COLOR = '1';
1173
+ const calculatedExitCode = getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
249
1174
  const jsonOutput = {
250
- version: "1.0.0",
251
- command: "scan",
252
- mode: "quick",
253
- engines: SCAN_ENGINES,
254
- verdict,
255
- score,
1175
+ success: calculatedExitCode === 0 || calculatedExitCode === 1, // 0 or 1 are success (SHIP/WARN)
1176
+ verdict: verdict?.verdict || 'UNKNOWN',
1177
+ score: report.score?.overall || (verdict?.verdict === 'PASS' ? 100 : 50),
1178
+ findings: normalizedFindings || [],
256
1179
  summary: {
257
- total: findings.length,
258
- blockers: blockers.length,
259
- warnings: warnings.length,
260
- filesScanned: scanResult.stats.filesScanned,
261
- duration,
1180
+ total: normalizedFindings?.length || 0,
1181
+ blockers: verdict?.summary?.blockers || 0,
1182
+ warnings: verdict?.summary?.warnings || 0,
1183
+ info: verdict?.summary?.info || 0,
262
1184
  },
263
- findings: findings.map(f => ({
264
- id: f.id,
265
- category: f.category,
266
- severity: f.severity,
267
- title: f.title,
268
- message: f.message,
269
- file: f.file,
270
- line: f.line,
271
- engine: f.engine,
272
- })),
273
- timestamp: new Date().toISOString(),
1185
+ stats: {
1186
+ filesScanned: report.stats?.filesScanned || 0,
1187
+ linesScanned: report.stats?.linesScanned || 0,
1188
+ durationMs: timings.total || 0,
1189
+ },
1190
+ exitCode: calculatedExitCode
274
1191
  };
275
1192
  console.log(JSON.stringify(jsonOutput, null, 2));
276
- return verdictToExitCode(verdict);
1193
+ // CRITICAL: Exit immediately, don't output anything else
1194
+ process.exit(calculatedExitCode);
1195
+ return calculatedExitCode;
277
1196
  }
278
-
279
- // SARIF output
1197
+
1198
+ // SARIF output mode - CRITICAL: Output ONLY valid SARIF, no banners/colors
280
1199
  if (opts.sarif) {
281
- const sarif = formatSARIF(findings, {
1200
+ process.env.NO_COLOR = '1';
1201
+ const sarif = formatSARIF(normalizedFindings, {
282
1202
  projectPath,
283
1203
  version: require('../../package.json').version || '1.0.0'
284
1204
  });
285
- console.log(JSON.stringify(sarif, null, 2));
286
- return verdictToExitCode(verdict);
1205
+ // formatSARIF returns a JSON string, so output it directly
1206
+ console.log(sarif);
1207
+ const exitCode = getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
1208
+ // CRITICAL: Exit immediately, don't output anything else
1209
+ process.exit(exitCode);
1210
+ return exitCode;
287
1211
  }
1212
+
1213
+ // ═══════════════════════════════════════════════════════════════════════════
1214
+ // FINGERPRINTING & BASELINE COMPARISON
1215
+ // ═══════════════════════════════════════════════════════════════════════════
288
1216
 
289
- // Human-readable output
290
- console.log();
291
-
292
- // Verdict
293
- const verdictColor = verdict === 'PASS' ? colors.success : verdict === 'WARN' ? colors.warning : colors.error;
294
- const verdictIcon = verdict === 'PASS' ? '✓' : verdict === 'WARN' ? '⚠' : '✗';
295
-
296
- console.log(` ${verdictColor}${ansi.bold}${verdictIcon} ${verdict}${ansi.reset} ${ansi.dim}Score: ${score}/100${ansi.reset}`);
297
- console.log(` ${ansi.dim}${findings.length} findings (${blockers.length} blockers, ${warnings.length} warnings)${ansi.reset}`);
298
- console.log(` ${ansi.dim}${scanResult.stats.filesScanned} files scanned in ${formatDuration(duration)}${ansi.reset}`);
299
- console.log();
300
-
301
- // Show blockers
302
- if (blockers.length > 0) {
303
- console.log(` ${colors.error}${ansi.bold}Blockers (${blockers.length})${ansi.reset}`);
304
- for (const f of blockers.slice(0, 5)) {
305
- console.log(` ${colors.error}●${ansi.reset} ${f.title}`);
306
- if (f.file) console.log(` ${ansi.dim}${f.file}:${f.line}${ansi.reset}`);
1217
+ let diff = null;
1218
+ if (opts.baseline) {
1219
+ try {
1220
+ const enrichResult = enrichFindings(normalizedFindings, projectPath, true);
1221
+ diff = enrichResult.diff;
1222
+
1223
+ // Update findings with fingerprints and status
1224
+ for (let i = 0; i < normalizedFindings.length; i++) {
1225
+ if (enrichResult.findings[i]) {
1226
+ normalizedFindings[i].fingerprint = enrichResult.findings[i].fingerprint;
1227
+ normalizedFindings[i].status = enrichResult.findings[i].status;
1228
+ normalizedFindings[i].firstSeen = enrichResult.findings[i].firstSeen;
1229
+ }
1230
+ }
1231
+ } catch (fpError) {
1232
+ if (opts.verbose) {
1233
+ console.warn(` ${ansi.dim}Fingerprinting skipped: ${fpError.message}${ansi.reset}`);
1234
+ }
307
1235
  }
308
- if (blockers.length > 5) {
309
- console.log(` ${ansi.dim}... and ${blockers.length - 5} more${ansi.reset}`);
310
- }
311
- console.log();
312
1236
  }
313
1237
 
314
- // Show warnings (verbose only)
315
- if (warnings.length > 0 && opts.verbose) {
316
- console.log(` ${colors.warning}${ansi.bold}Warnings (${warnings.length})${ansi.reset}`);
317
- for (const f of warnings.slice(0, 5)) {
318
- console.log(` ${colors.warning}◐${ansi.reset} ${f.title}`);
319
- if (f.file) console.log(` ${ansi.dim}${f.file}:${f.line}${ansi.reset}`);
320
- }
321
- if (warnings.length > 5) {
322
- console.log(` ${ansi.dim}... and ${warnings.length - 5} more${ansi.reset}`);
1238
+ // Update baseline if requested
1239
+ if (opts.updateBaseline) {
1240
+ try {
1241
+ saveBaseline(projectPath, normalizedFindings, {
1242
+ verdict: verdict?.verdict,
1243
+ scanTime: new Date().toISOString(),
1244
+ });
1245
+ if (!opts.json && !opts.quiet) {
1246
+ console.log(` ${colors.success}✓${ansi.reset} Baseline updated with ${normalizedFindings.length} findings`);
1247
+ }
1248
+ } catch (blError) {
1249
+ if (opts.verbose) {
1250
+ console.warn(` ${ansi.dim}Baseline save failed: ${blError.message}${ansi.reset}`);
1251
+ }
323
1252
  }
324
- console.log();
325
1253
  }
326
-
327
- // Pro upsell
328
- if (!opts.quiet) {
329
- console.log(` ${ansi.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ansi.reset}`);
330
- console.log(` ${ansi.dim}Quick scan ran ${SCAN_ENGINES.length} engines.${ansi.reset}`);
331
- console.log(` ${colors.accent}For comprehensive analysis (17+ engines + validators):${ansi.reset}`);
332
- console.log(` ${colors.success}vibecheck ship${ansi.reset} ${ansi.dim}[PRO]${ansi.reset}`);
1254
+
1255
+ // ═══════════════════════════════════════════════════════════════════════════
1256
+ // ENHANCED OUTPUT
1257
+ // ═══════════════════════════════════════════════════════════════════════════
1258
+
1259
+ // Use enhanced output formatter (from scan-output.js) - ONLY if not JSON/SARIF mode
1260
+ if (!opts.json && !opts.sarif) {
1261
+ const resultForOutput = {
1262
+ verdict,
1263
+ findings: normalizedFindings,
1264
+ layers: report.layers || [],
1265
+ coverage: report.coverageMap,
1266
+ breakdown: report.score?.breakdown,
1267
+ timings,
1268
+ cached,
1269
+ diff, // Include diff for display
1270
+ };
1271
+ console.log(formatScanOutput(resultForOutput, { verbose: opts.verbose }));
1272
+ }
1273
+
1274
+ // Additional details if verbose
1275
+ if (opts.verbose) {
1276
+ printBreakdown(report.score.breakdown);
1277
+ printCoverageMap(report.coverageMap);
1278
+ printLayers(report.layers);
1279
+
1280
+ printSection('REPORTS', '📄');
333
1281
  console.log();
1282
+ console.log(` ${c.cyan}${outputPaths.md}${c.reset}`);
1283
+ console.log(` ${c.dim}${outputPaths.json}${c.reset}`);
1284
+ if (outputPaths.sarif) {
1285
+ console.log(` ${c.dim}${outputPaths.sarif}${c.reset}`);
1286
+ }
334
1287
  }
335
-
336
- // Emit completion
337
- emitScanComplete(projectPath, verdict === 'PASS' ? 'success' : 'failure', {
338
- score,
339
- issueCount: findings.length,
340
- durationMs: duration,
341
- });
342
1288
 
343
- // Save results
1289
+ // ═══════════════════════════════════════════════════════════════════════════
1290
+ // SAVE RESULTS
1291
+ // ═══════════════════════════════════════════════════════════════════════════
1292
+
344
1293
  if (opts.save) {
345
1294
  const resultsDir = path.join(projectPath, '.vibecheck', 'results');
346
1295
  if (!fs.existsSync(resultsDir)) {
347
1296
  fs.mkdirSync(resultsDir, { recursive: true });
348
1297
  }
349
1298
 
350
- fs.writeFileSync(
351
- path.join(resultsDir, 'latest.json'),
352
- JSON.stringify({
353
- verdict,
354
- score,
355
- findings,
356
- timestamp: new Date().toISOString(),
357
- mode: 'quick',
358
- engines: SCAN_ENGINES,
359
- stats: scanResult.stats,
360
- }, null, 2)
361
- );
1299
+ // Save latest.json
1300
+ const latestPath = path.join(resultsDir, 'latest.json');
1301
+ fs.writeFileSync(latestPath, JSON.stringify(standardOutput, null, 2));
1302
+
1303
+ // Save timestamped copy
1304
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1305
+ const historyDir = path.join(resultsDir, 'history');
1306
+ if (!fs.existsSync(historyDir)) {
1307
+ fs.mkdirSync(historyDir, { recursive: true });
1308
+ }
1309
+ fs.writeFileSync(path.join(historyDir, `scan-${timestamp}.json`), JSON.stringify(standardOutput, null, 2));
1310
+
1311
+ if (!opts.json) {
1312
+ console.log(`\n ${ansi.dim}Results saved to: ${latestPath}${ansi.reset}`);
1313
+ }
362
1314
  }
363
1315
 
364
- return verdictToExitCode(verdict);
1316
+ // ═══════════════════════════════════════════════════════════════════════════
1317
+ // AUTOFIX MODE - Generate missions
1318
+ // ═══════════════════════════════════════════════════════════════════════════
1319
+
1320
+ if (opts.autofix && normalizedFindings.length > 0) {
1321
+ // Check entitlement
1322
+ const entitlementsV2 = require("./lib/entitlements-v2");
1323
+ const access = await entitlementsV2.enforce("scan.autofix", {
1324
+ projectPath,
1325
+ silent: false,
1326
+ });
1327
+
1328
+ if (!access.allowed) {
1329
+ console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.bold}--autofix requires STARTER plan${ansi.reset}`);
1330
+ console.log(` ${ansi.dim}Upgrade at: https://vibecheckai.dev/pricing${ansi.reset}`);
1331
+ console.log(` ${ansi.dim}Scan results saved. Run 'vibecheck fix' for manual mission generation.${ansi.reset}\n`);
1332
+ } else {
1333
+ console.log(`\n ${colors.accent}${icons.lightning}${ansi.reset} ${ansi.bold}Generating AI missions...${ansi.reset}\n`);
1334
+
1335
+ const { missions, summary } = await generateMissions(normalizedFindings, projectPath, opts);
1336
+
1337
+ console.log(` ${colors.success}✓${ansi.reset} Generated ${missions.length} missions`);
1338
+ console.log(` ${ansi.dim}Critical: ${summary.bySeverity.critical}${ansi.reset}`);
1339
+ console.log(` ${ansi.dim}High: ${summary.bySeverity.high}${ansi.reset}`);
1340
+ console.log(` ${ansi.dim}Medium: ${summary.bySeverity.medium}${ansi.reset}`);
1341
+ console.log(` ${ansi.dim}Low: ${summary.bySeverity.low}${ansi.reset}`);
1342
+ console.log();
1343
+ console.log(` ${ansi.dim}Missions saved to: .vibecheck/missions/${ansi.reset}`);
1344
+ console.log(` ${ansi.dim}Open MISSIONS.md for prompts to use with your AI assistant.${ansi.reset}`);
1345
+ console.log();
1346
+ }
1347
+ }
1348
+
1349
+ // Emit audit event for scan complete
1350
+ emitScanComplete(projectPath, verdict.verdict === 'PASS' ? 'success' : 'failure', {
1351
+ score: report.score?.overall || (verdict.verdict === 'PASS' ? 100 : 50),
1352
+ grade: report.score?.grade || (verdict.verdict === 'PASS' ? 'A' : 'F'),
1353
+ issueCount: verdict.summary.blockers,
1354
+ durationMs: timings.total,
1355
+ });
1356
+
1357
+ // Submit results to dashboard if connected
1358
+ if (apiConnected && apiScan) {
1359
+ try {
1360
+ await submitScanResults(apiScan.scanId, {
1361
+ verdict: verdict.verdict,
1362
+ score: report.score?.overall || 0,
1363
+ findings: report.findings || [],
1364
+ filesScanned: report.stats?.filesScanned || 0,
1365
+ linesScanned: report.stats?.linesScanned || 0,
1366
+ durationMs: timings.total,
1367
+ metadata: {
1368
+ layers,
1369
+ profile: opts.profile,
1370
+ version: require('../../package.json').version,
1371
+ },
1372
+ });
1373
+ console.log(`${colors.success}✓${ansi.reset} Results sent to dashboard`);
1374
+ } catch (err) {
1375
+ console.log(`${colors.warning}⚠${ansi.reset} Failed to send results to dashboard: ${err.message}`);
1376
+ }
1377
+ }
1378
+
1379
+ return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
1380
+ } else {
1381
+ // Legacy fallback output when unified output system isn't available
1382
+ const findings = [...(report.shipBlockers || []), ...detectionFindings];
1383
+ const criticalCount = findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length;
1384
+ const warningCount = findings.filter(f => f.severity === 'warning' || f.severity === 'WARN').length;
1385
+
1386
+ const verdict = criticalCount > 0 ? 'BLOCK' : warningCount > 0 ? 'WARN' : 'SHIP';
1387
+
1388
+ // ═══════════════════════════════════════════════════════════════════════════
1389
+ // FINGERPRINTING & BASELINE COMPARISON (Legacy path)
1390
+ // ═══════════════════════════════════════════════════════════════════════════
1391
+
1392
+ const normalizedLegacyFindings = findings.map(f => ({
1393
+ severity: f.severity === 'critical' || f.severity === 'BLOCK' ? 'critical' :
1394
+ f.severity === 'warning' || f.severity === 'WARN' ? 'medium' : 'low',
1395
+ category: f.category || 'ROUTE',
1396
+ title: f.title || f.message,
1397
+ message: f.message || f.title,
1398
+ file: f.file || f.evidence?.[0]?.file,
1399
+ line: f.line || parseInt(f.evidence?.[0]?.lines?.split('-')[0]) || 1,
1400
+ evidence: f.evidence,
1401
+ fix: f.fixSuggestion,
1402
+ }));
1403
+
1404
+ let diff = null;
1405
+ if (opts.baseline) {
1406
+ try {
1407
+ const enrichResult = enrichFindings(normalizedLegacyFindings, projectPath, true);
1408
+ diff = enrichResult.diff;
1409
+
1410
+ // Update findings with fingerprints and status
1411
+ for (let i = 0; i < normalizedLegacyFindings.length; i++) {
1412
+ if (enrichResult.findings[i]) {
1413
+ normalizedLegacyFindings[i].fingerprint = enrichResult.findings[i].fingerprint;
1414
+ normalizedLegacyFindings[i].status = enrichResult.findings[i].status;
1415
+ normalizedLegacyFindings[i].firstSeen = enrichResult.findings[i].firstSeen;
1416
+ }
1417
+ }
1418
+ } catch (fpError) {
1419
+ if (opts.verbose) {
1420
+ console.warn(` ${ansi.dim}Fingerprinting skipped: ${fpError.message}${ansi.reset}`);
1421
+ }
1422
+ }
1423
+ }
1424
+
1425
+ // Update baseline if requested
1426
+ if (opts.updateBaseline) {
1427
+ try {
1428
+ saveBaseline(projectPath, normalizedLegacyFindings, {
1429
+ verdict,
1430
+ scanTime: new Date().toISOString(),
1431
+ });
1432
+ if (!opts.json && !opts.quiet) {
1433
+ console.log(` ${colors.success}✓${ansi.reset} Baseline updated with ${normalizedLegacyFindings.length} findings`);
1434
+ }
1435
+ } catch (blError) {
1436
+ if (opts.verbose) {
1437
+ console.warn(` ${ansi.dim}Baseline save failed: ${blError.message}${ansi.reset}`);
1438
+ }
1439
+ }
1440
+ }
1441
+
1442
+ // JSON output mode - CRITICAL: Output ONLY JSON, no banners/colors
1443
+ if (opts.json) {
1444
+ process.env.NO_COLOR = '1';
1445
+ const jsonOutput = {
1446
+ success: verdict === 'SHIP' || verdict === 'WARN',
1447
+ verdict: verdict,
1448
+ score: verdict === 'SHIP' ? 100 : verdict === 'WARN' ? 70 : 40,
1449
+ findings: normalizedLegacyFindings || [],
1450
+ summary: {
1451
+ total: normalizedLegacyFindings?.length || 0,
1452
+ blockers: criticalCount,
1453
+ warnings: warningCount,
1454
+ info: findings.length - criticalCount - warningCount,
1455
+ },
1456
+ stats: {
1457
+ filesScanned: 0,
1458
+ linesScanned: 0,
1459
+ durationMs: timings.total || 0,
1460
+ },
1461
+ exitCode: verdictToExitCode(verdict)
1462
+ };
1463
+ console.log(JSON.stringify(jsonOutput, null, 2));
1464
+ process.exit(jsonOutput.exitCode);
1465
+ return jsonOutput.exitCode;
1466
+ }
1467
+
1468
+ // SARIF output mode - CRITICAL: Output ONLY valid SARIF, no banners/colors
1469
+ if (opts.sarif) {
1470
+ process.env.NO_COLOR = '1';
1471
+ const sarif = formatSARIF(normalizedLegacyFindings, {
1472
+ projectPath,
1473
+ version: require('../../package.json').version || '1.0.0'
1474
+ });
1475
+ console.log(sarif);
1476
+ const exitCode = verdictToExitCode(verdict);
1477
+ process.exit(exitCode);
1478
+ return exitCode;
1479
+ }
1480
+
1481
+ // Use enhanced output formatter for legacy fallback
1482
+ const severityCounts = {
1483
+ critical: criticalCount,
1484
+ high: 0,
1485
+ medium: warningCount,
1486
+ low: findings.length - criticalCount - warningCount,
1487
+ };
1488
+ const score = calculateScore(severityCounts);
1489
+
1490
+ const result = {
1491
+ verdict: { verdict, score },
1492
+ findings: normalizedLegacyFindings,
1493
+ layers: [],
1494
+ timings,
1495
+ diff,
1496
+ };
1497
+
1498
+ console.log(formatScanOutput(result, { verbose: opts.verbose }));
1499
+
1500
+ // Emit audit event
1501
+ emitScanComplete(projectPath, verdict === 'SHIP' ? 'success' : 'failure', {
1502
+ score: verdict === 'SHIP' ? 100 : verdict === 'WARN' ? 70 : 40,
1503
+ issueCount: criticalCount + warningCount,
1504
+ durationMs: timings.total,
1505
+ });
1506
+
1507
+ // Submit results to dashboard if connected
1508
+ if (apiConnected && apiScan) {
1509
+ try {
1510
+ await submitScanResults(apiScan.scanId, {
1511
+ verdict,
1512
+ score: verdict === 'SHIP' ? 100 : verdict === 'WARN' ? 70 : 40,
1513
+ findings: normalizedLegacyFindings,
1514
+ filesScanned: result.stats?.filesScanned || 0,
1515
+ linesScanned: result.stats?.linesScanned || 0,
1516
+ durationMs: timings.total,
1517
+ metadata: {
1518
+ layers,
1519
+ profile: opts.profile,
1520
+ version: require('../../package.json').version,
1521
+ },
1522
+ });
1523
+ console.log(`${colors.success}✓${ansi.reset} Results sent to dashboard`);
1524
+ } catch (err) {
1525
+ console.log(`${colors.warning}⚠${ansi.reset} Failed to send results to dashboard: ${err.message}`);
1526
+ }
1527
+ }
1528
+
1529
+ return verdictToExitCode(verdict);
1530
+ }
365
1531
 
366
1532
  } catch (error) {
367
- if (spinner) {
1533
+ if (spinner && !opts.json && !opts.sarif) {
368
1534
  spinner.fail(`Scan failed: ${error.message}`);
369
1535
  }
370
1536
 
371
- console.error(`\n ${colors.error}✗${ansi.reset} ${ansi.bold}Error:${ansi.reset} ${error.message}`);
372
- if (opts.verbose) {
373
- console.error(` ${ansi.dim}${error.stack}${ansi.reset}`);
1537
+ // JSON/SARIF error output
1538
+ if (opts.json || opts.sarif) {
1539
+ process.env.NO_COLOR = '1';
1540
+ const errorOutput = {
1541
+ success: false,
1542
+ error: {
1543
+ code: error.code || 'SCAN_ERROR',
1544
+ message: error.message,
1545
+ },
1546
+ exitCode: error.code === 'VALIDATION_ERROR' ? EXIT.USER_ERROR : EXIT.INTERNAL_ERROR
1547
+ };
1548
+ if (opts.json) {
1549
+ console.log(JSON.stringify(errorOutput, null, 2));
1550
+ } else {
1551
+ // SARIF error - return empty SARIF with error in message
1552
+ const sarif = formatSARIF([], { projectPath, version: require('../../package.json').version || '1.0.0' });
1553
+ console.log(sarif);
1554
+ }
1555
+ process.exit(errorOutput.exitCode);
1556
+ return errorOutput.exitCode;
374
1557
  }
375
1558
 
1559
+ // Use enhanced error handling (only for non-JSON/SARIF modes)
1560
+ const exitCode = printError(error, 'Scan');
1561
+
1562
+ // Emit audit event for scan error
376
1563
  emitScanComplete(projectPath, 'error', {
377
1564
  errorCode: error.code || 'SCAN_ERROR',
378
1565
  errorMessage: error.message,
379
1566
  durationMs: Date.now() - startTime,
380
1567
  });
1568
+
1569
+ // Report error to dashboard if connected
1570
+ if (apiConnected && apiScan) {
1571
+ try {
1572
+ await reportScanError(apiScan.scanId, error);
1573
+ if (!opts.json && !opts.sarif) {
1574
+ console.log(`${colors.info}📡${ansi.reset} Error reported to dashboard`);
1575
+ }
1576
+ } catch (err) {
1577
+ if (!opts.json && !opts.sarif) {
1578
+ console.log(`${colors.warning}⚠${ansi.reset} Failed to report error to dashboard: ${err.message}`);
1579
+ }
1580
+ }
1581
+ }
381
1582
 
382
- return EXIT.INTERNAL_ERROR;
1583
+ return exitCode;
383
1584
  }
384
1585
  }
385
1586
 
386
- // Helper to normalize severity
387
- function normalizeSeverity(sev) {
388
- if (!sev) return 'info';
389
- const s = String(sev).toLowerCase();
390
- if (s === 'block' || s === 'critical' || s === 'high') return 'critical';
391
- if (s === 'warn' || s === 'warning' || s === 'medium') return 'warning';
392
- return 'info';
1587
+ // Helper function to find source files for cache hash
1588
+ async function findSourceFiles(projectPath) {
1589
+ const files = [];
1590
+ const fs = require('fs');
1591
+ const path = require('path');
1592
+
1593
+ async function walk(dir) {
1594
+ try {
1595
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
1596
+ for (const entry of entries) {
1597
+ const fullPath = path.join(dir, entry.name);
1598
+ if (entry.isDirectory()) {
1599
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
1600
+ await walk(fullPath);
1601
+ }
1602
+ } else if (entry.isFile()) {
1603
+ const ext = path.extname(entry.name).toLowerCase();
1604
+ if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
1605
+ files.push(fullPath);
1606
+ }
1607
+ }
1608
+ }
1609
+ } catch {
1610
+ // Skip inaccessible directories
1611
+ }
1612
+ }
1613
+
1614
+ await walk(projectPath);
1615
+ return files;
393
1616
  }
394
1617
 
395
- // Export
1618
+ // Export with error handling wrapper
396
1619
  module.exports = {
397
1620
  runScan: withErrorHandling(runScan, "Scan failed"),
398
1621
  };