@vibecheckai/cli 3.2.2 → 3.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  6. package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
  7. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
  8. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
  9. package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
  10. package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
  11. package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
  12. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
  14. package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
  15. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  16. package/bin/runners/lib/analyzers.js +606 -325
  17. package/bin/runners/lib/auth-truth.js +193 -193
  18. package/bin/runners/lib/backup.js +62 -62
  19. package/bin/runners/lib/billing.js +107 -107
  20. package/bin/runners/lib/claims.js +118 -118
  21. package/bin/runners/lib/cli-ui.js +540 -540
  22. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  23. package/bin/runners/lib/contracts/env-contract.js +181 -181
  24. package/bin/runners/lib/contracts/external-contract.js +206 -206
  25. package/bin/runners/lib/contracts/guard.js +168 -168
  26. package/bin/runners/lib/contracts/index.js +89 -89
  27. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  28. package/bin/runners/lib/contracts/route-contract.js +199 -199
  29. package/bin/runners/lib/contracts.js +804 -804
  30. package/bin/runners/lib/detect.js +89 -89
  31. package/bin/runners/lib/doctor/autofix.js +254 -254
  32. package/bin/runners/lib/doctor/index.js +37 -37
  33. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  34. package/bin/runners/lib/doctor/modules/index.js +46 -46
  35. package/bin/runners/lib/doctor/modules/network.js +250 -250
  36. package/bin/runners/lib/doctor/modules/project.js +312 -312
  37. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  38. package/bin/runners/lib/doctor/modules/security.js +348 -348
  39. package/bin/runners/lib/doctor/modules/system.js +213 -213
  40. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  41. package/bin/runners/lib/doctor/reporter.js +262 -262
  42. package/bin/runners/lib/doctor/service.js +262 -262
  43. package/bin/runners/lib/doctor/types.js +113 -113
  44. package/bin/runners/lib/doctor/ui.js +263 -263
  45. package/bin/runners/lib/doctor-v2.js +608 -608
  46. package/bin/runners/lib/drift.js +425 -425
  47. package/bin/runners/lib/enforcement.js +72 -72
  48. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  49. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  50. package/bin/runners/lib/engines/ast-cache.js +99 -0
  51. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  52. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  53. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  54. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  55. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  56. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  57. package/bin/runners/lib/engines/file-filter.js +131 -0
  58. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  59. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  60. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  61. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  62. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  63. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  64. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  65. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  66. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  67. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  68. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  69. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  70. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  71. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  72. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  73. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  74. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  75. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  76. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  77. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  78. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  79. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  80. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  81. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  82. package/bin/runners/lib/enterprise-detect.js +603 -603
  83. package/bin/runners/lib/enterprise-init.js +942 -942
  84. package/bin/runners/lib/env-resolver.js +417 -417
  85. package/bin/runners/lib/env-template.js +66 -66
  86. package/bin/runners/lib/env.js +189 -189
  87. package/bin/runners/lib/extractors/client-calls.js +990 -990
  88. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  89. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  90. package/bin/runners/lib/extractors/index.js +363 -363
  91. package/bin/runners/lib/extractors/next-routes.js +524 -524
  92. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  93. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  94. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  95. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  96. package/bin/runners/lib/findings-schema.js +281 -281
  97. package/bin/runners/lib/firewall-prompt.js +50 -50
  98. package/bin/runners/lib/global-flags.js +213 -213
  99. package/bin/runners/lib/graph/graph-builder.js +265 -265
  100. package/bin/runners/lib/graph/html-renderer.js +413 -413
  101. package/bin/runners/lib/graph/index.js +32 -32
  102. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  103. package/bin/runners/lib/graph/static-extractor.js +518 -518
  104. package/bin/runners/lib/html-report.js +650 -650
  105. package/bin/runners/lib/interactive-menu.js +1496 -1496
  106. package/bin/runners/lib/llm.js +75 -75
  107. package/bin/runners/lib/meter.js +61 -61
  108. package/bin/runners/lib/missions/evidence.js +126 -126
  109. package/bin/runners/lib/patch.js +40 -40
  110. package/bin/runners/lib/permissions/auth-model.js +213 -213
  111. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  112. package/bin/runners/lib/permissions/index.js +45 -45
  113. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  114. package/bin/runners/lib/pkgjson.js +28 -28
  115. package/bin/runners/lib/policy.js +295 -295
  116. package/bin/runners/lib/preflight.js +142 -142
  117. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  118. package/bin/runners/lib/reality/index.js +318 -318
  119. package/bin/runners/lib/reality/request-hashing.js +416 -416
  120. package/bin/runners/lib/reality/request-mapper.js +453 -453
  121. package/bin/runners/lib/reality/safety-rails.js +463 -463
  122. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  123. package/bin/runners/lib/reality/toast-detector.js +393 -393
  124. package/bin/runners/lib/reality-findings.js +84 -84
  125. package/bin/runners/lib/receipts.js +179 -179
  126. package/bin/runners/lib/redact.js +29 -29
  127. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  128. package/bin/runners/lib/replay/index.js +263 -263
  129. package/bin/runners/lib/replay/player.js +348 -348
  130. package/bin/runners/lib/replay/recorder.js +331 -331
  131. package/bin/runners/lib/report-output.js +187 -187
  132. package/bin/runners/lib/report.js +135 -135
  133. package/bin/runners/lib/route-detection.js +1140 -1140
  134. package/bin/runners/lib/sandbox/index.js +59 -59
  135. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  136. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  137. package/bin/runners/lib/sandbox/worktree.js +174 -174
  138. package/bin/runners/lib/scan-output.js +525 -190
  139. package/bin/runners/lib/schema-validator.js +350 -350
  140. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  141. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  142. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  143. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  144. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  145. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  146. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  147. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  148. package/bin/runners/lib/schemas/validator.js +438 -438
  149. package/bin/runners/lib/score-history.js +282 -282
  150. package/bin/runners/lib/share-pack.js +239 -239
  151. package/bin/runners/lib/snippets.js +67 -67
  152. package/bin/runners/lib/status-output.js +253 -253
  153. package/bin/runners/lib/terminal-ui.js +351 -271
  154. package/bin/runners/lib/upsell.js +510 -510
  155. package/bin/runners/lib/usage.js +153 -153
  156. package/bin/runners/lib/validate-patch.js +156 -156
  157. package/bin/runners/lib/verdict-engine.js +628 -628
  158. package/bin/runners/reality/engine.js +917 -917
  159. package/bin/runners/reality/flows.js +122 -122
  160. package/bin/runners/reality/report.js +378 -378
  161. package/bin/runners/reality/session.js +193 -193
  162. package/bin/runners/runGuard.js +168 -168
  163. package/bin/runners/runProof.zip +0 -0
  164. package/bin/runners/runProve.js +8 -0
  165. package/bin/runners/runReality.js +14 -0
  166. package/bin/runners/runScan.js +17 -1
  167. package/bin/runners/runTruth.js +15 -3
  168. package/mcp-server/tier-auth.js +4 -4
  169. package/mcp-server/tools/index.js +72 -72
  170. package/package.json +1 -1
@@ -1,190 +1,525 @@
1
- /**
2
- * Enterprise Scan Output - Premium Format
3
- * Features:
4
- * - "SCAN" ASCII Art
5
- * - Fixed alignment tables
6
- * - "Redacted" Upsell section for Scan context
7
- */
8
-
9
- const chalk = require('chalk');
10
-
11
- // ═══════════════════════════════════════════════════════════════════════════════
12
- // CONFIGURATION
13
- // ═══════════════════════════════════════════════════════════════════════════════
14
-
15
- const WIDTH = 76;
16
-
17
- const BOX = {
18
- topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
19
- horizontal: '═', vertical: '║',
20
- teeRight: '╠', teeLeft: '╣',
21
-
22
- // Table Borders (Light)
23
- tTopLeft: '┌', tTopRight: '┐', tBottomLeft: '└', tBottomRight: '┘',
24
- tHorizontal: '─', tVertical: '│',
25
- tTeeTop: '┬', tTeeBottom: '┴', tTee: '┼', tTeeLeft: '├', tTeeRight: '┤'
26
- };
27
-
28
- const LOGO_SCAN = `
29
- █████████ █████████ █████████ █████████
30
- ███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███
31
- ░███ ░░░ ░███ ░░░ ░███ ░███ ░███ ░███
32
- ░░█████████ ░███ ░███████████ ░███ ░███
33
- ░░░░░░░░███░███ ░███░░░░░███ ░███ ░███
34
- ███ ░███░███ ███ ░███ ░███ ░███ ░███
35
- ░░█████████ ░░█████████ ░███ ░███ ░███ ░███
36
- ░░░░░░░░░ ░░░░░░░░░ ░░░░ ░░░░ ░░░░ ░░░░
37
- `;
38
-
39
- // Column widths for the table (Must correspond to math below)
40
- const COL_1 = 10; // Severity
41
- const COL_2 = 13; // Component
42
- const COL_3 = 41; // Message
43
-
44
- // ═══════════════════════════════════════════════════════════════════════════════
45
- // UTILITIES
46
- // ═══════════════════════════════════════════════════════════════════════════════
47
-
48
- function padCenter(str, width) {
49
- const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
50
- const padding = Math.max(0, width - visibleLen);
51
- const left = Math.floor(padding / 2);
52
- const right = padding - left;
53
- return ' '.repeat(left) + str + ' '.repeat(right);
54
- }
55
-
56
- function padRight(str, len) {
57
- const visibleLen = str.length; // Simplified for this usage
58
- const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
59
- return truncated + ' '.repeat(Math.max(0, len - truncated.length));
60
- }
61
-
62
- function padLogoBlock(ascii, width) {
63
- const lines = ascii.trim().split('\n');
64
- const maxContentWidth = Math.max(...lines.map(l => l.length));
65
-
66
- return lines.map(line => {
67
- const solidLine = line + ' '.repeat(maxContentWidth - line.length);
68
- return padCenter(solidLine, width);
69
- });
70
- }
71
-
72
- function renderProgressBar(score, width = 20) {
73
- const filled = Math.round((score / 100) * width);
74
- const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
75
- return bar;
76
- }
77
-
78
- // ═══════════════════════════════════════════════════════════════════════════════
79
- // MAIN FORMATTER
80
- // ═══════════════════════════════════════════════════════════════════════════════
81
-
82
- function formatScanOutput(result) {
83
- const {
84
- score = 0,
85
- findings = [],
86
- duration = 0,
87
- scannedFiles = 0
88
- } = result;
89
-
90
- const hasIssues = findings.length > 0;
91
- const heapMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
92
- const lines = [];
93
-
94
- // 1. OUTER FRAME TOP
95
- lines.push(chalk.gray(BOX.topLeft + BOX.horizontal.repeat(WIDTH - 2) + BOX.topRight));
96
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
97
-
98
- // 2. LOGO (Cyan if clean, Red if issues)
99
- const logoColor = hasIssues ? chalk.red : chalk.cyan;
100
- padLogoBlock(LOGO_SCAN, WIDTH - 2).forEach(line => {
101
- lines.push(chalk.gray(BOX.vertical) + logoColor(line) + chalk.gray(BOX.vertical));
102
- });
103
-
104
- const subTitle = hasIssues ? 'INTEGRITY SCAN • ISSUES DETECTED' : 'INTEGRITY SCAN • CLEAN';
105
- lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.bold.white(subTitle), WIDTH - 2) + chalk.gray(BOX.vertical));
106
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
107
-
108
- // 3. TELEMETRY
109
- lines.push(chalk.gray(BOX.teeRight + BOX.horizontal.repeat(WIDTH - 2) + BOX.teeLeft));
110
- const stats = `📡 TELEMETRY │ ⏱ ${duration}ms │ 📂 ${scannedFiles} Files │ 📦 ${heapMB}MB`;
111
- lines.push(chalk.gray(BOX.vertical) + padCenter(stats, WIDTH - 2) + chalk.gray(BOX.vertical));
112
- lines.push(chalk.gray(BOX.teeRight + BOX.horizontal.repeat(WIDTH - 2) + BOX.teeLeft));
113
-
114
- if (!hasIssues) {
115
- // -- CLEAN STATE --
116
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
117
- lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.green('✅ NO STATIC ISSUES FOUND'), WIDTH - 2) + chalk.gray(BOX.vertical));
118
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
119
- } else {
120
- // -- ISSUES STATE --
121
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
122
- const scoreBar = renderProgressBar(score, 20);
123
- lines.push(chalk.gray(BOX.vertical) + padCenter(`HEALTH SCORE [${scoreBar}] ${score} / 100`, WIDTH + 18) + chalk.gray(BOX.vertical));
124
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
125
-
126
- // 4. FINDINGS TABLE
127
- lines.push(chalk.gray(BOX.vertical) + padCenter('DETECTED VULNERABILITIES (STATIC)', WIDTH - 2) + chalk.gray(BOX.vertical));
128
-
129
- // Table Construction
130
- const C1 = COL_1, C2 = COL_2, C3 = COL_3;
131
- const tTop = ` ${BOX.tTopLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTeeTop}${BOX.tHorizontal.repeat(C2)}${BOX.tTeeTop}${BOX.tHorizontal.repeat(C3)}${BOX.tTopRight} `;
132
- const tMid = ` ${BOX.tTeeLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTee}${BOX.tHorizontal.repeat(C2)}${BOX.tTee}${BOX.tHorizontal.repeat(C3)}${BOX.tTeeRight} `;
133
- const tBot = ` ${BOX.tBottomLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTeeBottom}${BOX.tHorizontal.repeat(C2)}${BOX.tTeeBottom}${BOX.tHorizontal.repeat(C3)}${BOX.tBottomRight} `;
134
-
135
- lines.push(chalk.gray(BOX.vertical) + chalk.gray(tTop) + chalk.gray(BOX.vertical));
136
- const header = ` ${BOX.tVertical}${padRight(' SEVERITY', C1)}${BOX.tVertical}${padRight(' TYPE', C2)}${BOX.tVertical}${padRight(' FINDING', C3)}${BOX.tVertical} `;
137
- lines.push(chalk.gray(BOX.vertical) + chalk.bold(header) + chalk.gray(BOX.vertical));
138
- lines.push(chalk.gray(BOX.vertical) + chalk.gray(tMid) + chalk.gray(BOX.vertical));
139
-
140
- // Rows
141
- const topItems = findings.slice(0, 5);
142
- topItems.forEach(item => {
143
- const severity = item.severity === 'critical' ? chalk.red('🛑 CRIT ') : chalk.yellow('🟡 WARN ');
144
- const type = padRight(' ' + (item.type || 'Unknown'), C2);
145
- const desc = padRight(' ' + (item.message || ''), C3);
146
-
147
- const row = ` ${chalk.gray(BOX.tVertical)}${padRight(severity, C1)}${chalk.gray(BOX.tVertical)}${type}${chalk.gray(BOX.tVertical)}${desc}${chalk.gray(BOX.tVertical)} `;
148
- lines.push(chalk.gray(BOX.vertical) + row + chalk.gray(BOX.vertical));
149
- });
150
-
151
- lines.push(chalk.gray(BOX.vertical) + chalk.gray(tBot) + chalk.gray(BOX.vertical));
152
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
153
- }
154
-
155
- // 5. LOCKED / UPSELL SECTION (Redacted for SCAN context)
156
- lines.push(chalk.gray(BOX.teeRight + BOX.horizontal.repeat(WIDTH - 2) + BOX.teeLeft));
157
-
158
- const lockTitle = chalk.white.bold('🔒 DEEP SCAN ANALYSIS: ') + chalk.gray('UNAVAILABLE');
159
- lines.push(chalk.gray(BOX.vertical) + padCenter(lockTitle, WIDTH + 9) + chalk.gray(BOX.vertical));
160
- lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.gray('─'.repeat(60)), WIDTH + 9) + chalk.gray(BOX.vertical));
161
-
162
- lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.dim('Static analysis is limited. The following runtime vectors'), WIDTH + 9) + chalk.gray(BOX.vertical));
163
- lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.dim('cannot be verified without the Vibecheck Truth Engine.'), WIDTH + 9) + chalk.gray(BOX.vertical));
164
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
165
-
166
- // Redacted Lines
167
- const redacted = [
168
- '░░ [REDACTED] 🔒 Runtime API Schema Validation (Drift Detection)',
169
- '░░ [REDACTED] 🔒 Live Auth Boundary Verification (RBAC)',
170
- '░░ [REDACTED] 🔒 Environment Variable Resolution (Config)'
171
- ];
172
-
173
- redacted.forEach(r => {
174
- lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.dim(r), WIDTH + 9) + chalk.gray(BOX.vertical));
175
- });
176
-
177
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
178
-
179
- // CTA
180
- lines.push(chalk.gray(BOX.vertical) + padCenter(chalk.bold('👉 UNLOCK INSIGHTS:'), WIDTH + 4) + chalk.gray(BOX.vertical));
181
- lines.push(chalk.gray(BOX.vertical) + padCenter('Run ' + chalk.cyan('vibecheck upgrade pro') + ' to enable Deep Scan.', WIDTH + 13) + chalk.gray(BOX.vertical));
182
-
183
- // BOTTOM FRAME
184
- lines.push(chalk.gray(BOX.vertical) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.vertical));
185
- lines.push(chalk.gray(BOX.bottomLeft + BOX.horizontal.repeat(WIDTH - 2) + BOX.bottomRight));
186
-
187
- return lines.join('\n');
188
- }
189
-
190
- module.exports = { formatScanOutput };
1
+ /**
2
+ * Enterprise Scan Output - Premium Format
3
+ * Features:
4
+ * - "SCAN" ASCII Art
5
+ * - Fixed alignment tables
6
+ * - "Redacted" Upsell section for Scan context
7
+ */
8
+
9
+ // Use ANSI codes directly (chalk v5 is ESM-only, this is CommonJS)
10
+ const ESC = '\x1b';
11
+ const chalk = {
12
+ reset: `${ESC}[0m`,
13
+ bold: `${ESC}[1m`,
14
+ dim: `${ESC}[2m`,
15
+ red: `${ESC}[31m`,
16
+ green: `${ESC}[32m`,
17
+ yellow: `${ESC}[33m`,
18
+ cyan: `${ESC}[36m`,
19
+ magenta: `${ESC}[35m`,
20
+ white: `${ESC}[37m`,
21
+ gray: `${ESC}[90m`,
22
+ };
23
+
24
+ // ═══════════════════════════════════════════════════════════════════════════════
25
+ // CONFIGURATION
26
+ // ═══════════════════════════════════════════════════════════════════════════════
27
+
28
+ const WIDTH = 76;
29
+
30
+ const BOX = {
31
+ topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
32
+ horizontal: '═', vertical: '║',
33
+ teeRight: '╠', teeLeft: '╣',
34
+
35
+ // Table Borders (Light)
36
+ tTopLeft: '┌', tTopRight: '┐', tBottomLeft: '└', tBottomRight: '┘',
37
+ tHorizontal: '─', tVertical: '│',
38
+ tTeeTop: '┬', tTeeBottom: '┴', tTee: '┼', tTeeLeft: '├', tTeeRight: '┤'
39
+ };
40
+
41
+ const LOGO_SCAN = `
42
+ █████████ █████████ █████████ █████████
43
+ ███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███
44
+ ░███ ░░░ ░███ ░░░ ░███ ░███ ░███ ░███
45
+ ░░█████████ ░███ ░███████████ ░███ ░███
46
+ ░░░░░░░░███░███ ░███░░░░░███ ░███ ░███
47
+ ███ ░███░███ ███ ░███ ░███ ░███ ░███
48
+ ░░█████████ ░░█████████ ░███ ░███ ░███ ░███
49
+ ░░░░░░░░░ ░░░░░░░░░ ░░░░ ░░░░ ░░░░ ░░░░
50
+ `;
51
+
52
+ // Column widths for the table (Must correspond to math below)
53
+ const COL_1 = 10; // Severity
54
+ const COL_2 = 13; // Component
55
+ const COL_3 = 41; // Message
56
+
57
+ // ═══════════════════════════════════════════════════════════════════════════════
58
+ // UTILITIES
59
+ // ═══════════════════════════════════════════════════════════════════════════════
60
+
61
+ function padCenter(str, width) {
62
+ const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
63
+ const padding = Math.max(0, width - visibleLen);
64
+ const left = Math.floor(padding / 2);
65
+ const right = padding - left;
66
+ return ' '.repeat(left) + str + ' '.repeat(right);
67
+ }
68
+
69
+ function padRight(str, len) {
70
+ const visibleLen = str.length; // Simplified for this usage
71
+ const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
72
+ return truncated + ' '.repeat(Math.max(0, len - truncated.length));
73
+ }
74
+
75
+ function padLogoBlock(ascii, width) {
76
+ const lines = ascii.trim().split('\n');
77
+ const maxContentWidth = Math.max(...lines.map(l => l.length));
78
+
79
+ return lines.map(line => {
80
+ const solidLine = line + ' '.repeat(maxContentWidth - line.length);
81
+ return padCenter(solidLine, width);
82
+ });
83
+ }
84
+
85
+ function renderProgressBar(score, width = 20) {
86
+ const filled = Math.round((score / 100) * width);
87
+ const bar = `${chalk.green}█${chalk.reset}`.repeat(filled) + `${chalk.gray}░${chalk.reset}`.repeat(width - filled);
88
+ return bar;
89
+ }
90
+
91
+ // ═══════════════════════════════════════════════════════════════════════════════
92
+ // MAIN FORMATTER
93
+ // ═══════════════════════════════════════════════════════════════════════════════
94
+
95
+ function formatScanOutput(result, options = {}) {
96
+ // Extract data from various possible structures
97
+ let score = 0;
98
+ let findings = [];
99
+ let duration = 0;
100
+ let scannedFiles = 0;
101
+
102
+ // Helper function to calculate score from findings
103
+ function calculateScoreFromFindings(findings) {
104
+ if (!findings || findings.length === 0) return 100;
105
+
106
+ // Count by severity (handle various severity formats)
107
+ const severityCounts = {
108
+ critical: 0,
109
+ high: 0,
110
+ medium: 0,
111
+ low: 0,
112
+ info: 0,
113
+ };
114
+
115
+ findings.forEach(f => {
116
+ const sev = (f.severity || '').toLowerCase();
117
+ if (sev === 'critical' || sev === 'block') {
118
+ severityCounts.critical++;
119
+ } else if (sev === 'high') {
120
+ severityCounts.high++;
121
+ } else if (sev === 'medium' || sev === 'warn' || sev === 'warning') {
122
+ severityCounts.medium++;
123
+ } else if (sev === 'low') {
124
+ severityCounts.low++;
125
+ } else if (sev === 'info') {
126
+ severityCounts.info++;
127
+ } else {
128
+ // Default unknown severities to medium
129
+ severityCounts.medium++;
130
+ }
131
+ });
132
+
133
+ // Calculate score with proper weights
134
+ // Critical: 25 points each, High: 15, Medium: 5, Low: 2, Info: 0
135
+ const deductions =
136
+ (severityCounts.critical * 25) +
137
+ (severityCounts.high * 15) +
138
+ (severityCounts.medium * 5) +
139
+ (severityCounts.low * 2);
140
+
141
+ // Cap deductions to prevent negative scores, but allow score to go to 0
142
+ const calculatedScore = Math.max(0, 100 - deductions);
143
+
144
+ // If we have findings but score is still 100, something's wrong - recalculate more aggressively
145
+ if (findings.length > 0 && calculatedScore === 100 && deductions === 0) {
146
+ // All findings were info or unknown - still deduct something
147
+ return Math.max(50, 100 - (findings.length * 0.1));
148
+ }
149
+
150
+ return Math.round(calculatedScore);
151
+ }
152
+
153
+ // Handle different input structures
154
+ if (result.verdict) {
155
+ // New unified output structure
156
+ if (typeof result.verdict === 'object') {
157
+ findings = result.findings || [];
158
+
159
+ // Always recalculate score from findings to ensure accuracy
160
+ score = calculateScoreFromFindings(findings);
161
+
162
+ // Only use provided score if it seems reasonable (not 100 when we have findings)
163
+ if (result.verdict.score && findings.length === 0) {
164
+ score = result.verdict.score;
165
+ } else if (result.verdict.score && result.verdict.score < score) {
166
+ // Use the lower (worse) score if provided score is worse
167
+ score = result.verdict.score;
168
+ }
169
+
170
+ duration = result.timings?.total || result.duration || 0;
171
+ scannedFiles = result.timings?.filesScanned || result.scannedFiles || findings.length || 0;
172
+ } else {
173
+ // Legacy structure where verdict is just a string
174
+ findings = result.findings || [];
175
+ score = calculateScoreFromFindings(findings);
176
+
177
+ // Fallback to verdict string if no findings
178
+ if (!score && findings.length === 0) {
179
+ const verdictStr = result.verdict;
180
+ if (verdictStr === 'PASS' || verdictStr === 'SHIP') score = 100;
181
+ else if (verdictStr === 'WARN') score = 70;
182
+ else if (verdictStr === 'FAIL' || verdictStr === 'BLOCK') score = 40;
183
+ }
184
+
185
+ duration = result.timings?.total || result.duration || 0;
186
+ scannedFiles = result.timings?.filesScanned || result.scannedFiles || findings.length || 0;
187
+ }
188
+ } else {
189
+ // Direct structure
190
+ findings = result.findings || [];
191
+ score = calculateScoreFromFindings(findings);
192
+
193
+ // Only use provided score if it seems reasonable
194
+ if (result.score && findings.length === 0) {
195
+ score = result.score;
196
+ } else if (result.score && result.score < score) {
197
+ score = result.score; // Use worse score
198
+ }
199
+
200
+ duration = result.duration || result.timings?.total || 0;
201
+ scannedFiles = result.scannedFiles || result.timings?.filesScanned || 0;
202
+ }
203
+
204
+ const hasIssues = findings.length > 0;
205
+ const heapMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
206
+ const lines = [];
207
+
208
+ // 1. OUTER FRAME TOP
209
+ lines.push(`${chalk.gray}${BOX.topLeft}${BOX.horizontal.repeat(WIDTH - 2)}${BOX.topRight}${chalk.reset}`);
210
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
211
+
212
+ // 2. LOGO (Cyan if clean, Red if issues)
213
+ const logoColorCode = hasIssues ? chalk.red : chalk.cyan;
214
+ padLogoBlock(LOGO_SCAN, WIDTH - 2).forEach(line => {
215
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${logoColorCode}${line}${chalk.reset}${chalk.gray}${BOX.vertical}${chalk.reset}`);
216
+ });
217
+
218
+ const subTitle = hasIssues ? 'INTEGRITY SCAN • ISSUES DETECTED' : 'INTEGRITY SCAN • CLEAN';
219
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.bold}${chalk.white}${subTitle}${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
220
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
221
+
222
+ // 3. TELEMETRY
223
+ lines.push(`${chalk.gray}${BOX.teeRight}${BOX.horizontal.repeat(WIDTH - 2)}${BOX.teeLeft}${chalk.reset}`);
224
+ const stats = `📡 TELEMETRY │ ⏱ ${duration}ms │ 📂 ${scannedFiles} Files │ 📦 ${heapMB}MB`;
225
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(stats, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
226
+ lines.push(`${chalk.gray}${BOX.teeRight}${BOX.horizontal.repeat(WIDTH - 2)}${BOX.teeLeft}${chalk.reset}`);
227
+
228
+ if (!hasIssues) {
229
+ // -- CLEAN STATE --
230
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
231
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.green}✅ NO STATIC ISSUES FOUND${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
232
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
233
+ } else {
234
+ // -- ISSUES STATE --
235
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
236
+ const scoreBar = renderProgressBar(score, 20);
237
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`HEALTH SCORE [${scoreBar}] ${score} / 100`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
238
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
239
+
240
+ // 4. FINDINGS SUMMARY BY CATEGORY
241
+ const categoryCounts = {};
242
+ findings.forEach(f => {
243
+ const cat = f.category || f.type || f.ruleId || 'Other';
244
+ categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
245
+ });
246
+
247
+ const categoryLabels = {
248
+ 'EnvContract': '🔐 Env Vars',
249
+ 'MissingRoute': '🔗 Routes',
250
+ 'GhostAuth': '🛡️ Auth',
251
+ 'FakeSuccess': '⚠️ Fake Success',
252
+ 'MockData': '🎭 Mock Data',
253
+ 'Secrets': '🔑 Secrets',
254
+ 'ConsoleLog': '📝 Console Logs',
255
+ 'TodoFixme': '📋 TODOs',
256
+ };
257
+
258
+ const summaryItems = Object.entries(categoryCounts)
259
+ .sort((a, b) => b[1] - a[1])
260
+ .slice(0, 6)
261
+ .map(([cat, count]) => {
262
+ const label = categoryLabels[cat] || `📌 ${cat}`;
263
+ return `${label}: ${chalk.bold}${count}${chalk.reset}`;
264
+ });
265
+
266
+ if (summaryItems.length > 0) {
267
+ // Split into two lines if too long
268
+ const categoryText = summaryItems.join(' • ');
269
+ const prefix = `${chalk.dim}Findings by Category:${chalk.reset} `;
270
+ const maxLineLength = WIDTH - 10; // Leave some margin
271
+
272
+ if (categoryText.length > maxLineLength - prefix.length) {
273
+ // Split into two lines
274
+ const midPoint = Math.floor(summaryItems.length / 2);
275
+ const line1 = summaryItems.slice(0, midPoint).join(' • ');
276
+ const line2 = summaryItems.slice(midPoint).join(' • ');
277
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${prefix}${line1}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
278
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${' '.repeat(prefix.length)}${line2}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
279
+ } else {
280
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${prefix}${categoryText}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
281
+ }
282
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
283
+ }
284
+
285
+ // 5. FINDINGS TABLE
286
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter('DETECTED VULNERABILITIES (STATIC)', WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
287
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
288
+
289
+ // Table Construction - Fixed alignment with proper spacing
290
+ const C1 = COL_1, C2 = COL_2, C3 = COL_3;
291
+ const tableContentWidth = C1 + C2 + C3 + 7; // 7 = borders (3 vertical + 4 spaces)
292
+ const tableLeftPad = Math.floor((WIDTH - 2 - tableContentWidth) / 2);
293
+ const tablePad = ' '.repeat(Math.max(1, tableLeftPad)); // At least 1 space padding
294
+
295
+ // Table borders without padding (we'll add padding when inserting into frame)
296
+ const tTop = `${BOX.tTopLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTeeTop}${BOX.tHorizontal.repeat(C2)}${BOX.tTeeTop}${BOX.tHorizontal.repeat(C3)}${BOX.tTopRight}`;
297
+ const tMid = `${BOX.tTeeLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTee}${BOX.tHorizontal.repeat(C2)}${BOX.tTee}${BOX.tHorizontal.repeat(C3)}${BOX.tTeeRight}`;
298
+ const tBot = `${BOX.tBottomLeft}${BOX.tHorizontal.repeat(C1)}${BOX.tTeeBottom}${BOX.tHorizontal.repeat(C2)}${BOX.tTeeBottom}${BOX.tHorizontal.repeat(C3)}${BOX.tBottomRight}`;
299
+
300
+ // Calculate right padding to fill the line
301
+ const totalTableWidth = tablePad.length + tableContentWidth;
302
+ const rightPad = WIDTH - 2 - totalTableWidth;
303
+ const rightPadStr = ' '.repeat(Math.max(0, rightPad));
304
+
305
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${tablePad}${chalk.gray}${tTop}${chalk.reset}${rightPadStr}${chalk.gray}${BOX.vertical}${chalk.reset}`);
306
+ const header = `${tablePad}${BOX.tVertical}${padRight(' SEVERITY', C1)}${BOX.tVertical}${padRight(' TYPE', C2)}${BOX.tVertical}${padRight(' FINDING', C3)}${BOX.tVertical}`;
307
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${chalk.bold}${header}${chalk.reset}${rightPadStr}${chalk.gray}${BOX.vertical}${chalk.reset}`);
308
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${tablePad}${chalk.gray}${tMid}${chalk.reset}${rightPadStr}${chalk.gray}${BOX.vertical}${chalk.reset}`);
309
+
310
+ // Rows - map findings to display format (show top 10)
311
+ const topItems = findings.slice(0, 10);
312
+ topItems.forEach(item => {
313
+ // Handle different finding structures
314
+ const severityText = item.severity === 'critical' || item.severity === 'BLOCK' || item.category === 'critical'
315
+ ? '🛑 CRIT '
316
+ : item.severity === 'warning' || item.severity === 'WARN' || item.category === 'warning'
317
+ ? '🟡 WARN '
318
+ : '🟡 WARN ';
319
+
320
+ const severityColor = item.severity === 'critical' || item.severity === 'BLOCK' || item.category === 'critical'
321
+ ? chalk.red
322
+ : chalk.yellow;
323
+
324
+ const severity = `${severityColor}${severityText}${chalk.reset}`;
325
+
326
+ // Map category to readable type
327
+ const categoryMap = {
328
+ 'EnvContract': 'EnvVar',
329
+ 'MissingRoute': 'Route',
330
+ 'GhostAuth': 'Auth',
331
+ 'FakeSuccess': 'FakeSuccess',
332
+ 'MockData': 'MockData',
333
+ 'Secrets': 'Secret',
334
+ 'ConsoleLog': 'Console',
335
+ 'TodoFixme': 'TODO',
336
+ };
337
+ const typeName = categoryMap[item.category] || item.category || item.type || item.ruleId || 'Unknown';
338
+ const type = padRight(' ' + typeName, C2);
339
+
340
+ // Truncate description if too long
341
+ let desc = item.message || item.title || item.description || '';
342
+ if (desc.length > C3 - 1) {
343
+ desc = desc.substring(0, C3 - 4) + '...';
344
+ }
345
+ desc = padRight(' ' + desc, C3);
346
+
347
+ const row = `${tablePad}${chalk.gray}${BOX.tVertical}${chalk.reset}${padRight(severity, C1)}${chalk.gray}${BOX.tVertical}${chalk.reset}${type}${chalk.gray}${BOX.tVertical}${chalk.reset}${desc}${chalk.gray}${BOX.tVertical}${chalk.reset}`;
348
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${row}${rightPadStr}${chalk.gray}${BOX.vertical}${chalk.reset}`);
349
+ });
350
+
351
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${tablePad}${chalk.gray}${tBot}${chalk.reset}${rightPadStr}${chalk.gray}${BOX.vertical}${chalk.reset}`);
352
+
353
+ // Show count if more findings exist
354
+ if (findings.length > 10) {
355
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.dim}... and ${findings.length - 10} more findings${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
356
+ }
357
+
358
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
359
+ }
360
+
361
+ // 6. UPSELL SECTION - Autofix & Mission Packs
362
+ lines.push(`${chalk.gray}${BOX.teeRight}${BOX.horizontal.repeat(WIDTH - 2)}${BOX.teeLeft}${chalk.reset}`);
363
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
364
+
365
+ // Autofix Upsell
366
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.bold}${chalk.cyan}🚀 AUTO-FIX AVAILABLE${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
367
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.dim}Fix ${findings.length} issues automatically with AI-powered autofix${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
368
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`Run ${chalk.cyan}vibecheck scan --autofix${chalk.reset} to apply fixes`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
369
+
370
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
371
+
372
+ // Mission Packs Upsell
373
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.bold}${chalk.magenta}📦 MISSION PACKS${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
374
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.dim}Get AI-generated fix plans grouped by feature area${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
375
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`Run ${chalk.cyan}vibecheck fix --packs${chalk.reset} to generate mission packs`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
376
+
377
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
378
+
379
+ // Upgrade CTA
380
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${padCenter(`${chalk.bold}★ Upgrade for unlimited scans + auto-fix${chalk.reset} → ${chalk.cyan}https://vibecheckai.dev${chalk.reset}`, WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
381
+
382
+ // BOTTOM FRAME
383
+ lines.push(`${chalk.gray}${BOX.vertical}${chalk.reset}${' '.repeat(WIDTH - 2)}${chalk.gray}${BOX.vertical}${chalk.reset}`);
384
+ lines.push(`${chalk.gray}${BOX.bottomLeft}${BOX.horizontal.repeat(WIDTH - 2)}${BOX.bottomRight}${chalk.reset}`);
385
+
386
+ return lines.join('\n');
387
+ }
388
+
389
+ // ═══════════════════════════════════════════════════════════════════════════════
390
+ // ADDITIONAL EXPORTS (for compatibility with runScan.js)
391
+ // ═══════════════════════════════════════════════════════════════════════════════
392
+
393
+ // Exit codes
394
+ const EXIT_CODES = {
395
+ SHIP: 0,
396
+ WARN: 1,
397
+ BLOCK: 2,
398
+ ERROR: 1,
399
+ MISCONFIG: 3,
400
+ INTERNAL: 1,
401
+ };
402
+
403
+ // Severity weights for scoring
404
+ const SEVERITY_WEIGHTS = {
405
+ critical: 25,
406
+ high: 15,
407
+ medium: 5,
408
+ low: 1,
409
+ info: 0,
410
+ };
411
+
412
+ /**
413
+ * Calculate overall score from severity counts
414
+ */
415
+ function calculateScore(severityCounts, totalFindings = 0) {
416
+ if (totalFindings === 0) return 100;
417
+
418
+ const deductions =
419
+ (severityCounts.critical || 0) * SEVERITY_WEIGHTS.critical +
420
+ (severityCounts.high || 0) * SEVERITY_WEIGHTS.high +
421
+ (severityCounts.medium || 0) * SEVERITY_WEIGHTS.medium +
422
+ (severityCounts.low || 0) * SEVERITY_WEIGHTS.low;
423
+
424
+ // Cap deductions at 100
425
+ const score = Math.max(0, 100 - deductions);
426
+ return Math.round(score);
427
+ }
428
+
429
+ /**
430
+ * Get exit code from verdict
431
+ */
432
+ function getExitCode(verdict) {
433
+ if (!verdict) return EXIT_CODES.BLOCK;
434
+ const v = typeof verdict === 'object' ? verdict.verdict : verdict;
435
+ if (v === 'SHIP' || v === 'PASS') return EXIT_CODES.SHIP;
436
+ if (v === 'WARN') return EXIT_CODES.WARN;
437
+ return EXIT_CODES.BLOCK;
438
+ }
439
+
440
+ /**
441
+ * Print error message
442
+ */
443
+ function printError(error, context = '') {
444
+ const isConfig = error.message && (
445
+ error.message.includes('config') ||
446
+ error.message.includes('missing') ||
447
+ error.message.includes('not found')
448
+ );
449
+ const exitCode = isConfig ? EXIT_CODES.MISCONFIG : EXIT_CODES.INTERNAL;
450
+
451
+ console.error(`${chalk.red}✗ Error${chalk.reset}`);
452
+ if (context) console.error(` ${chalk.dim}${context}${chalk.reset}`);
453
+ console.error(` ${error.message || error}`);
454
+ if (error.stack && process.env.VIBECHECK_DEBUG) {
455
+ console.error(`${chalk.dim}${error.stack}${chalk.reset}`);
456
+ }
457
+
458
+ return exitCode;
459
+ }
460
+
461
+ /**
462
+ * Format SARIF output (placeholder - full implementation in report-engine.js)
463
+ */
464
+ function formatSARIF(findings, options = {}) {
465
+ // Basic SARIF structure - full implementation should be in report-engine.js
466
+ return JSON.stringify({
467
+ version: "2.1.0",
468
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema.json",
469
+ runs: [{
470
+ tool: {
471
+ driver: {
472
+ name: "vibecheck",
473
+ version: options.version || "1.0.0"
474
+ }
475
+ },
476
+ results: findings.map((f, i) => ({
477
+ ruleId: f.ruleId || f.category || `rule-${i}`,
478
+ message: {
479
+ text: f.message || f.title || ""
480
+ },
481
+ level: f.severity === 'critical' ? 'error' : f.severity === 'warning' ? 'warning' : 'note',
482
+ locations: f.file ? [{
483
+ physicalLocation: {
484
+ artifactLocation: {
485
+ uri: f.file
486
+ },
487
+ region: f.line ? {
488
+ startLine: f.line
489
+ } : undefined
490
+ }
491
+ }] : []
492
+ }))
493
+ }]
494
+ }, null, 2);
495
+ }
496
+
497
+ // Placeholder functions for compatibility
498
+ function renderCoverageMap(coverageMap) {
499
+ return ''; // Implement if needed
500
+ }
501
+
502
+ function renderBreakdown(breakdown) {
503
+ return ''; // Implement if needed
504
+ }
505
+
506
+ function renderBlockers(blockers) {
507
+ return ''; // Implement if needed
508
+ }
509
+
510
+ function renderLayers(layers) {
511
+ return ''; // Implement if needed
512
+ }
513
+
514
+ module.exports = {
515
+ formatScanOutput,
516
+ calculateScore,
517
+ getExitCode,
518
+ printError,
519
+ formatSARIF,
520
+ EXIT_CODES,
521
+ renderCoverageMap,
522
+ renderBreakdown,
523
+ renderBlockers,
524
+ renderLayers,
525
+ };