@vibecheckai/cli 3.3.0 → 3.5.0

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/registry.js +389 -269
  2. package/bin/runners/cli-utils.js +2 -33
  3. package/bin/runners/context/generators/cursor.js +49 -2
  4. package/bin/runners/lib/agent-firewall/learning/learning-engine.js +849 -0
  5. package/bin/runners/lib/analyzers.js +599 -142
  6. package/bin/runners/lib/audit-logger.js +532 -0
  7. package/bin/runners/lib/authority/authorities/architecture.js +364 -0
  8. package/bin/runners/lib/authority/authorities/compliance.js +341 -0
  9. package/bin/runners/lib/authority/authorities/human.js +343 -0
  10. package/bin/runners/lib/authority/authorities/quality.js +420 -0
  11. package/bin/runners/lib/authority/authorities/security.js +228 -0
  12. package/bin/runners/lib/authority/index.js +293 -0
  13. package/bin/runners/lib/authority-badge.js +425 -425
  14. package/bin/runners/lib/bundle/bundle-intelligence.js +846 -0
  15. package/bin/runners/lib/cli-charts.js +368 -0
  16. package/bin/runners/lib/cli-config-display.js +405 -0
  17. package/bin/runners/lib/cli-demo.js +275 -0
  18. package/bin/runners/lib/cli-errors.js +438 -0
  19. package/bin/runners/lib/cli-help-formatter.js +439 -0
  20. package/bin/runners/lib/cli-interactive-menu.js +509 -0
  21. package/bin/runners/lib/cli-prompts.js +441 -0
  22. package/bin/runners/lib/cli-scan-cards.js +362 -0
  23. package/bin/runners/lib/compliance-reporter.js +710 -0
  24. package/bin/runners/lib/conductor/index.js +671 -0
  25. package/bin/runners/lib/easy/README.md +123 -0
  26. package/bin/runners/lib/easy/index.js +140 -0
  27. package/bin/runners/lib/easy/interactive-wizard.js +788 -0
  28. package/bin/runners/lib/easy/one-click-firewall.js +564 -0
  29. package/bin/runners/lib/easy/zero-config-reality.js +714 -0
  30. package/bin/runners/lib/engines/accessibility-engine.js +218 -18
  31. package/bin/runners/lib/engines/api-consistency-engine.js +335 -30
  32. package/bin/runners/lib/engines/async-patterns-engine.js +444 -0
  33. package/bin/runners/lib/engines/bundle-size-engine.js +433 -0
  34. package/bin/runners/lib/engines/confidence-scoring.js +276 -0
  35. package/bin/runners/lib/engines/context-detection.js +264 -0
  36. package/bin/runners/lib/engines/cross-file-analysis-engine.js +292 -27
  37. package/bin/runners/lib/engines/database-patterns-engine.js +429 -0
  38. package/bin/runners/lib/engines/duplicate-code-engine.js +354 -0
  39. package/bin/runners/lib/engines/empty-catch-engine.js +127 -17
  40. package/bin/runners/lib/engines/env-variables-engine.js +458 -0
  41. package/bin/runners/lib/engines/error-handling-engine.js +437 -0
  42. package/bin/runners/lib/engines/false-positive-prevention.js +630 -0
  43. package/bin/runners/lib/engines/framework-adapters/index.js +607 -0
  44. package/bin/runners/lib/engines/framework-detection.js +508 -0
  45. package/bin/runners/lib/engines/import-order-engine.js +429 -0
  46. package/bin/runners/lib/engines/mock-data-engine.js +53 -10
  47. package/bin/runners/lib/engines/naming-conventions-engine.js +544 -0
  48. package/bin/runners/lib/engines/noise-reduction-engine.js +452 -0
  49. package/bin/runners/lib/engines/orchestrator.js +334 -0
  50. package/bin/runners/lib/engines/performance-issues-engine.js +176 -36
  51. package/bin/runners/lib/engines/react-patterns-engine.js +457 -0
  52. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +382 -54
  53. package/bin/runners/lib/engines/type-aware-engine.js +263 -39
  54. package/bin/runners/lib/engines/vibecheck-engines/index.js +122 -13
  55. package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +806 -0
  56. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +373 -73
  57. package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +577 -0
  58. package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +543 -0
  59. package/bin/runners/lib/engines/vibecheck-engines.js +514 -0
  60. package/bin/runners/lib/enhanced-features/index.js +305 -0
  61. package/bin/runners/lib/enhanced-output.js +631 -0
  62. package/bin/runners/lib/enterprise.js +300 -0
  63. package/bin/runners/lib/entitlements-v2.js +161 -478
  64. package/bin/runners/lib/firewall/command-validator.js +351 -0
  65. package/bin/runners/lib/firewall/config.js +341 -0
  66. package/bin/runners/lib/firewall/content-validator.js +519 -0
  67. package/bin/runners/lib/firewall/index.js +101 -0
  68. package/bin/runners/lib/firewall/path-validator.js +256 -0
  69. package/bin/runners/lib/html-proof-report.js +350 -700
  70. package/bin/runners/lib/intelligence/cross-repo-intelligence.js +817 -0
  71. package/bin/runners/lib/mcp-utils.js +425 -0
  72. package/bin/runners/lib/missions/plan.js +46 -6
  73. package/bin/runners/lib/missions/templates.js +232 -0
  74. package/bin/runners/lib/output/index.js +1022 -0
  75. package/bin/runners/lib/policy-engine.js +652 -0
  76. package/bin/runners/lib/polish/autofix/accessibility-fixes.js +333 -0
  77. package/bin/runners/lib/polish/autofix/async-handlers.js +273 -0
  78. package/bin/runners/lib/polish/autofix/dead-code.js +280 -0
  79. package/bin/runners/lib/polish/autofix/imports-optimizer.js +344 -0
  80. package/bin/runners/lib/polish/autofix/index.js +200 -0
  81. package/bin/runners/lib/polish/autofix/remove-consoles.js +209 -0
  82. package/bin/runners/lib/polish/autofix/strengthen-types.js +245 -0
  83. package/bin/runners/lib/polish/backend-checks.js +148 -0
  84. package/bin/runners/lib/polish/documentation-checks.js +111 -0
  85. package/bin/runners/lib/polish/frontend-checks.js +168 -0
  86. package/bin/runners/lib/polish/index.js +71 -0
  87. package/bin/runners/lib/polish/infrastructure-checks.js +131 -0
  88. package/bin/runners/lib/polish/library-detection.js +175 -0
  89. package/bin/runners/lib/polish/performance-checks.js +100 -0
  90. package/bin/runners/lib/polish/security-checks.js +148 -0
  91. package/bin/runners/lib/polish/utils.js +203 -0
  92. package/bin/runners/lib/prompt-builder.js +540 -0
  93. package/bin/runners/lib/proof-certificate.js +634 -0
  94. package/bin/runners/lib/reality/accessibility-audit.js +946 -0
  95. package/bin/runners/lib/reality/api-contract-validator.js +1012 -0
  96. package/bin/runners/lib/reality/chaos-engineering.js +1084 -0
  97. package/bin/runners/lib/reality/performance-tracker.js +1077 -0
  98. package/bin/runners/lib/reality/scenario-generator.js +1404 -0
  99. package/bin/runners/lib/reality/visual-regression.js +852 -0
  100. package/bin/runners/lib/reality-profiler.js +717 -0
  101. package/bin/runners/lib/replay/flight-recorder-viewer.js +1160 -0
  102. package/bin/runners/lib/review/ai-code-review.js +832 -0
  103. package/bin/runners/lib/rules/custom-rule-engine.js +985 -0
  104. package/bin/runners/lib/sbom-generator.js +641 -0
  105. package/bin/runners/lib/scan-output-enhanced.js +512 -0
  106. package/bin/runners/lib/scan-output.js +65 -19
  107. package/bin/runners/lib/security/owasp-scanner.js +939 -0
  108. package/bin/runners/lib/ship-output.js +18 -25
  109. package/bin/runners/lib/terminal-ui.js +113 -1
  110. package/bin/runners/lib/unified-cli-output.js +603 -430
  111. package/bin/runners/lib/upsell.js +90 -338
  112. package/bin/runners/lib/validators/contract-validator.js +283 -0
  113. package/bin/runners/lib/validators/dead-export-detector.js +279 -0
  114. package/bin/runners/lib/validators/dep-audit.js +245 -0
  115. package/bin/runners/lib/validators/env-validator.js +319 -0
  116. package/bin/runners/lib/validators/index.js +120 -0
  117. package/bin/runners/lib/validators/license-checker.js +252 -0
  118. package/bin/runners/lib/validators/route-validator.js +290 -0
  119. package/bin/runners/runAIAgent.js +5 -10
  120. package/bin/runners/runAgent.js +3 -0
  121. package/bin/runners/runApprove.js +1233 -1200
  122. package/bin/runners/runAuth.js +22 -1
  123. package/bin/runners/runAuthority.js +528 -0
  124. package/bin/runners/runCheckpoint.js +4 -24
  125. package/bin/runners/runClassify.js +862 -859
  126. package/bin/runners/runConductor.js +772 -0
  127. package/bin/runners/runContainer.js +366 -0
  128. package/bin/runners/runContext.js +3 -0
  129. package/bin/runners/runDoctor.js +28 -41
  130. package/bin/runners/runEasy.js +410 -0
  131. package/bin/runners/runFirewall.js +3 -0
  132. package/bin/runners/runFirewallHook.js +3 -0
  133. package/bin/runners/runFix.js +76 -66
  134. package/bin/runners/runGuard.js +411 -18
  135. package/bin/runners/runIaC.js +372 -0
  136. package/bin/runners/runInit.js +10 -60
  137. package/bin/runners/runMcp.js +11 -12
  138. package/bin/runners/runPolish.js +240 -64
  139. package/bin/runners/runPromptFirewall.js +5 -12
  140. package/bin/runners/runProve.js +20 -55
  141. package/bin/runners/runReality.js +68 -59
  142. package/bin/runners/runReport.js +31 -5
  143. package/bin/runners/runRuntime.js +5 -8
  144. package/bin/runners/runScan.js +194 -1273
  145. package/bin/runners/runShip.js +695 -47
  146. package/bin/runners/runTruth.js +3 -0
  147. package/bin/runners/runValidate.js +7 -11
  148. package/bin/runners/runVibe.js +791 -0
  149. package/bin/runners/runWatch.js +14 -23
  150. package/bin/vibecheck.js +179 -65
  151. package/mcp-server/index.js +202 -636
  152. package/mcp-server/lib/api-client.cjs +7 -299
  153. package/mcp-server/package.json +1 -1
  154. package/mcp-server/tier-auth.js +175 -574
  155. package/mcp-server/tools-v3.js +800 -505
  156. package/mcp-server/tools.js +495 -0
  157. package/package.json +1 -1
  158. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
  159. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
  160. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
  161. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
  162. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
  163. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
  164. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
  165. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
  166. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
  167. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
  168. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
  169. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
  170. package/mcp-server/index-v1.js +0 -698
@@ -0,0 +1,1160 @@
1
+ /**
2
+ * Interactive Flight Recorder Viewer
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * COMPETITIVE MOAT FEATURE - Visual Replay of Reality Mode Runs
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ *
8
+ * This generates an interactive HTML report that allows users to:
9
+ * - Step through each action in the Reality Mode run
10
+ * - See screenshots at each point
11
+ * - View network requests on a timeline
12
+ * - See DOM changes between steps
13
+ * - Filter by finding type
14
+ * - Jump to specific findings
15
+ * - Export as video
16
+ */
17
+
18
+ "use strict";
19
+
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+
23
+ // ═══════════════════════════════════════════════════════════════════════════════
24
+ // HTML TEMPLATE
25
+ // ═══════════════════════════════════════════════════════════════════════════════
26
+
27
+ const HTML_TEMPLATE = `<!DOCTYPE html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="UTF-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
32
+ <title>Vibecheck Reality Mode - Flight Recorder</title>
33
+ <style>
34
+ :root {
35
+ --bg-primary: #0f0f0f;
36
+ --bg-secondary: #1a1a1a;
37
+ --bg-tertiary: #252525;
38
+ --text-primary: #ffffff;
39
+ --text-secondary: #a0a0a0;
40
+ --text-muted: #666666;
41
+ --accent: #ff6b35;
42
+ --accent-secondary: #ff9f1c;
43
+ --success: #00ff88;
44
+ --warning: #ffcc00;
45
+ --error: #ff4444;
46
+ --info: #00b4d8;
47
+ --border: #333333;
48
+ }
49
+
50
+ * {
51
+ margin: 0;
52
+ padding: 0;
53
+ box-sizing: border-box;
54
+ }
55
+
56
+ body {
57
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
58
+ background: var(--bg-primary);
59
+ color: var(--text-primary);
60
+ line-height: 1.6;
61
+ }
62
+
63
+ /* Header */
64
+ .header {
65
+ background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
66
+ padding: 20px 30px;
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ position: sticky;
71
+ top: 0;
72
+ z-index: 100;
73
+ }
74
+
75
+ .header h1 {
76
+ font-size: 1.5rem;
77
+ font-weight: 700;
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 10px;
81
+ }
82
+
83
+ .header .logo {
84
+ width: 32px;
85
+ height: 32px;
86
+ }
87
+
88
+ .header-stats {
89
+ display: flex;
90
+ gap: 30px;
91
+ }
92
+
93
+ .stat {
94
+ text-align: center;
95
+ }
96
+
97
+ .stat-value {
98
+ font-size: 1.5rem;
99
+ font-weight: 700;
100
+ }
101
+
102
+ .stat-label {
103
+ font-size: 0.75rem;
104
+ opacity: 0.8;
105
+ }
106
+
107
+ /* Main Layout */
108
+ .container {
109
+ display: grid;
110
+ grid-template-columns: 300px 1fr 350px;
111
+ height: calc(100vh - 80px);
112
+ }
113
+
114
+ /* Timeline Panel */
115
+ .timeline-panel {
116
+ background: var(--bg-secondary);
117
+ border-right: 1px solid var(--border);
118
+ overflow-y: auto;
119
+ padding: 20px;
120
+ }
121
+
122
+ .timeline-header {
123
+ margin-bottom: 20px;
124
+ }
125
+
126
+ .timeline-header h2 {
127
+ font-size: 1rem;
128
+ color: var(--text-secondary);
129
+ margin-bottom: 10px;
130
+ }
131
+
132
+ .timeline-filters {
133
+ display: flex;
134
+ gap: 8px;
135
+ flex-wrap: wrap;
136
+ }
137
+
138
+ .filter-btn {
139
+ background: var(--bg-tertiary);
140
+ border: 1px solid var(--border);
141
+ color: var(--text-secondary);
142
+ padding: 4px 12px;
143
+ border-radius: 20px;
144
+ font-size: 0.75rem;
145
+ cursor: pointer;
146
+ transition: all 0.2s;
147
+ }
148
+
149
+ .filter-btn:hover, .filter-btn.active {
150
+ background: var(--accent);
151
+ color: white;
152
+ border-color: var(--accent);
153
+ }
154
+
155
+ .timeline-item {
156
+ padding: 12px 15px;
157
+ margin-bottom: 8px;
158
+ background: var(--bg-tertiary);
159
+ border-radius: 8px;
160
+ cursor: pointer;
161
+ transition: all 0.2s;
162
+ border-left: 3px solid transparent;
163
+ }
164
+
165
+ .timeline-item:hover {
166
+ background: var(--bg-primary);
167
+ }
168
+
169
+ .timeline-item.active {
170
+ background: var(--bg-primary);
171
+ border-left-color: var(--accent);
172
+ }
173
+
174
+ .timeline-item.finding {
175
+ border-left-color: var(--error);
176
+ }
177
+
178
+ .timeline-item.success {
179
+ border-left-color: var(--success);
180
+ }
181
+
182
+ .timeline-time {
183
+ font-size: 0.7rem;
184
+ color: var(--text-muted);
185
+ font-family: monospace;
186
+ }
187
+
188
+ .timeline-action {
189
+ font-weight: 500;
190
+ margin: 4px 0;
191
+ }
192
+
193
+ .timeline-detail {
194
+ font-size: 0.8rem;
195
+ color: var(--text-secondary);
196
+ white-space: nowrap;
197
+ overflow: hidden;
198
+ text-overflow: ellipsis;
199
+ }
200
+
201
+ .timeline-badge {
202
+ display: inline-block;
203
+ padding: 2px 8px;
204
+ border-radius: 4px;
205
+ font-size: 0.65rem;
206
+ font-weight: 600;
207
+ text-transform: uppercase;
208
+ margin-top: 6px;
209
+ }
210
+
211
+ .badge-block { background: var(--error); }
212
+ .badge-warn { background: var(--warning); color: black; }
213
+ .badge-info { background: var(--info); }
214
+
215
+ /* Main Viewer */
216
+ .main-viewer {
217
+ display: flex;
218
+ flex-direction: column;
219
+ background: var(--bg-primary);
220
+ }
221
+
222
+ .viewer-controls {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 15px;
226
+ padding: 15px 20px;
227
+ background: var(--bg-secondary);
228
+ border-bottom: 1px solid var(--border);
229
+ }
230
+
231
+ .control-btn {
232
+ background: var(--bg-tertiary);
233
+ border: 1px solid var(--border);
234
+ color: var(--text-primary);
235
+ width: 40px;
236
+ height: 40px;
237
+ border-radius: 8px;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ cursor: pointer;
242
+ transition: all 0.2s;
243
+ font-size: 1.2rem;
244
+ }
245
+
246
+ .control-btn:hover {
247
+ background: var(--accent);
248
+ border-color: var(--accent);
249
+ }
250
+
251
+ .control-btn:disabled {
252
+ opacity: 0.5;
253
+ cursor: not-allowed;
254
+ }
255
+
256
+ .progress-bar {
257
+ flex: 1;
258
+ height: 6px;
259
+ background: var(--bg-tertiary);
260
+ border-radius: 3px;
261
+ overflow: hidden;
262
+ cursor: pointer;
263
+ }
264
+
265
+ .progress-fill {
266
+ height: 100%;
267
+ background: linear-gradient(90deg, var(--accent), var(--accent-secondary));
268
+ transition: width 0.3s;
269
+ }
270
+
271
+ .step-counter {
272
+ font-family: monospace;
273
+ color: var(--text-secondary);
274
+ font-size: 0.9rem;
275
+ }
276
+
277
+ .playback-speed {
278
+ background: var(--bg-tertiary);
279
+ border: 1px solid var(--border);
280
+ color: var(--text-primary);
281
+ padding: 6px 12px;
282
+ border-radius: 6px;
283
+ font-size: 0.8rem;
284
+ }
285
+
286
+ /* Screenshot Viewer */
287
+ .screenshot-container {
288
+ flex: 1;
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ padding: 20px;
293
+ background: #000;
294
+ position: relative;
295
+ overflow: hidden;
296
+ }
297
+
298
+ .screenshot {
299
+ max-width: 100%;
300
+ max-height: 100%;
301
+ border-radius: 8px;
302
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
303
+ }
304
+
305
+ .screenshot-overlay {
306
+ position: absolute;
307
+ top: 20px;
308
+ left: 20px;
309
+ background: rgba(0,0,0,0.8);
310
+ padding: 10px 15px;
311
+ border-radius: 8px;
312
+ font-size: 0.85rem;
313
+ }
314
+
315
+ .overlay-url {
316
+ color: var(--accent);
317
+ font-family: monospace;
318
+ }
319
+
320
+ /* Network Waterfall */
321
+ .network-bar {
322
+ height: 40px;
323
+ background: var(--bg-secondary);
324
+ border-top: 1px solid var(--border);
325
+ padding: 0 20px;
326
+ display: flex;
327
+ align-items: center;
328
+ gap: 10px;
329
+ overflow-x: auto;
330
+ }
331
+
332
+ .network-item {
333
+ height: 24px;
334
+ min-width: 4px;
335
+ border-radius: 2px;
336
+ position: relative;
337
+ cursor: pointer;
338
+ }
339
+
340
+ .network-item:hover::after {
341
+ content: attr(data-url);
342
+ position: absolute;
343
+ bottom: 100%;
344
+ left: 50%;
345
+ transform: translateX(-50%);
346
+ background: var(--bg-tertiary);
347
+ padding: 4px 8px;
348
+ border-radius: 4px;
349
+ font-size: 0.7rem;
350
+ white-space: nowrap;
351
+ z-index: 10;
352
+ }
353
+
354
+ .network-js { background: #f7df1e; }
355
+ .network-css { background: #264de4; }
356
+ .network-image { background: #22c55e; }
357
+ .network-fetch { background: #06b6d4; }
358
+ .network-other { background: #6b7280; }
359
+
360
+ /* Details Panel */
361
+ .details-panel {
362
+ background: var(--bg-secondary);
363
+ border-left: 1px solid var(--border);
364
+ overflow-y: auto;
365
+ padding: 20px;
366
+ }
367
+
368
+ .details-section {
369
+ margin-bottom: 25px;
370
+ }
371
+
372
+ .details-section h3 {
373
+ font-size: 0.85rem;
374
+ color: var(--text-secondary);
375
+ text-transform: uppercase;
376
+ letter-spacing: 0.5px;
377
+ margin-bottom: 12px;
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 8px;
381
+ }
382
+
383
+ .finding-card {
384
+ background: var(--bg-tertiary);
385
+ border-radius: 8px;
386
+ padding: 15px;
387
+ margin-bottom: 10px;
388
+ border-left: 3px solid var(--error);
389
+ }
390
+
391
+ .finding-card.warn {
392
+ border-left-color: var(--warning);
393
+ }
394
+
395
+ .finding-title {
396
+ font-weight: 600;
397
+ margin-bottom: 8px;
398
+ }
399
+
400
+ .finding-message {
401
+ font-size: 0.85rem;
402
+ color: var(--text-secondary);
403
+ margin-bottom: 10px;
404
+ }
405
+
406
+ .finding-evidence {
407
+ background: var(--bg-primary);
408
+ padding: 10px;
409
+ border-radius: 6px;
410
+ font-family: monospace;
411
+ font-size: 0.75rem;
412
+ overflow-x: auto;
413
+ }
414
+
415
+ /* DOM Diff */
416
+ .dom-diff {
417
+ background: var(--bg-primary);
418
+ border-radius: 8px;
419
+ padding: 15px;
420
+ font-family: monospace;
421
+ font-size: 0.8rem;
422
+ overflow-x: auto;
423
+ }
424
+
425
+ .diff-add {
426
+ background: rgba(34, 197, 94, 0.2);
427
+ color: #22c55e;
428
+ }
429
+
430
+ .diff-remove {
431
+ background: rgba(239, 68, 68, 0.2);
432
+ color: #ef4444;
433
+ }
434
+
435
+ .diff-context {
436
+ color: var(--text-muted);
437
+ }
438
+
439
+ /* Network Details */
440
+ .network-detail {
441
+ background: var(--bg-tertiary);
442
+ border-radius: 8px;
443
+ padding: 12px;
444
+ margin-bottom: 8px;
445
+ }
446
+
447
+ .network-url {
448
+ font-family: monospace;
449
+ font-size: 0.75rem;
450
+ color: var(--accent);
451
+ word-break: break-all;
452
+ margin-bottom: 8px;
453
+ }
454
+
455
+ .network-meta {
456
+ display: flex;
457
+ gap: 15px;
458
+ font-size: 0.75rem;
459
+ color: var(--text-secondary);
460
+ }
461
+
462
+ .network-meta span {
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 4px;
466
+ }
467
+
468
+ .status-ok { color: var(--success); }
469
+ .status-error { color: var(--error); }
470
+
471
+ /* Responsive */
472
+ @media (max-width: 1200px) {
473
+ .container {
474
+ grid-template-columns: 250px 1fr 300px;
475
+ }
476
+ }
477
+
478
+ @media (max-width: 900px) {
479
+ .container {
480
+ grid-template-columns: 1fr;
481
+ }
482
+
483
+ .timeline-panel, .details-panel {
484
+ display: none;
485
+ }
486
+ }
487
+
488
+ /* Animations */
489
+ @keyframes pulse {
490
+ 0%, 100% { opacity: 1; }
491
+ 50% { opacity: 0.5; }
492
+ }
493
+
494
+ .playing .step-counter {
495
+ animation: pulse 1s infinite;
496
+ }
497
+ </style>
498
+ </head>
499
+ <body>
500
+ <header class="header">
501
+ <h1>
502
+ <svg class="logo" viewBox="0 0 32 32" fill="currentColor">
503
+ <circle cx="16" cy="16" r="14" fill="none" stroke="currentColor" stroke-width="2"/>
504
+ <path d="M12 10 L22 16 L12 22 Z"/>
505
+ </svg>
506
+ Flight Recorder
507
+ </h1>
508
+ <div class="header-stats">
509
+ <div class="stat">
510
+ <div class="stat-value" id="total-steps">{{TOTAL_STEPS}}</div>
511
+ <div class="stat-label">Steps</div>
512
+ </div>
513
+ <div class="stat">
514
+ <div class="stat-value" id="findings-count">{{FINDINGS_COUNT}}</div>
515
+ <div class="stat-label">Findings</div>
516
+ </div>
517
+ <div class="stat">
518
+ <div class="stat-value" id="duration">{{DURATION}}</div>
519
+ <div class="stat-label">Duration</div>
520
+ </div>
521
+ </div>
522
+ </header>
523
+
524
+ <div class="container">
525
+ <!-- Timeline Panel -->
526
+ <aside class="timeline-panel">
527
+ <div class="timeline-header">
528
+ <h2>Timeline</h2>
529
+ <div class="timeline-filters">
530
+ <button class="filter-btn active" data-filter="all">All</button>
531
+ <button class="filter-btn" data-filter="finding">Findings</button>
532
+ <button class="filter-btn" data-filter="navigation">Navigation</button>
533
+ <button class="filter-btn" data-filter="click">Clicks</button>
534
+ </div>
535
+ </div>
536
+ <div id="timeline-items">
537
+ <!-- Timeline items will be inserted here -->
538
+ </div>
539
+ </aside>
540
+
541
+ <!-- Main Viewer -->
542
+ <main class="main-viewer">
543
+ <div class="viewer-controls">
544
+ <button class="control-btn" id="prev-btn" title="Previous">⏮</button>
545
+ <button class="control-btn" id="play-btn" title="Play/Pause">▶</button>
546
+ <button class="control-btn" id="next-btn" title="Next">⏭</button>
547
+ <div class="progress-bar" id="progress-bar">
548
+ <div class="progress-fill" id="progress-fill"></div>
549
+ </div>
550
+ <span class="step-counter"><span id="current-step">1</span> / <span id="total-step">{{TOTAL_STEPS}}</span></span>
551
+ <select class="playback-speed" id="speed-select">
552
+ <option value="0.5">0.5x</option>
553
+ <option value="1" selected>1x</option>
554
+ <option value="2">2x</option>
555
+ <option value="4">4x</option>
556
+ </select>
557
+ </div>
558
+
559
+ <div class="screenshot-container">
560
+ <img class="screenshot" id="screenshot" src="" alt="Screenshot">
561
+ <div class="screenshot-overlay">
562
+ <div class="overlay-url" id="current-url">Loading...</div>
563
+ </div>
564
+ </div>
565
+
566
+ <div class="network-bar" id="network-bar">
567
+ <!-- Network items will be inserted here -->
568
+ </div>
569
+ </main>
570
+
571
+ <!-- Details Panel -->
572
+ <aside class="details-panel">
573
+ <div class="details-section">
574
+ <h3>📍 Current Action</h3>
575
+ <div id="action-details">
576
+ <div class="finding-card">
577
+ <div class="finding-title" id="action-type">Loading...</div>
578
+ <div class="finding-message" id="action-detail"></div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+
583
+ <div class="details-section" id="findings-section">
584
+ <h3>🚨 Findings at This Step</h3>
585
+ <div id="step-findings">
586
+ <!-- Findings will be inserted here -->
587
+ </div>
588
+ </div>
589
+
590
+ <div class="details-section">
591
+ <h3>📝 DOM Changes</h3>
592
+ <div class="dom-diff" id="dom-diff">
593
+ <div class="diff-context">No changes detected</div>
594
+ </div>
595
+ </div>
596
+
597
+ <div class="details-section">
598
+ <h3>🌐 Network Requests</h3>
599
+ <div id="network-details">
600
+ <!-- Network details will be inserted here -->
601
+ </div>
602
+ </div>
603
+ </aside>
604
+ </div>
605
+
606
+ <script>
607
+ // Flight Recorder Data (injected by generator)
608
+ const RECORDING_DATA = {{RECORDING_DATA}};
609
+
610
+ // State
611
+ let currentStepIndex = 0;
612
+ let isPlaying = false;
613
+ let playbackSpeed = 1;
614
+ let playbackInterval = null;
615
+
616
+ // DOM Elements
617
+ const elements = {
618
+ timeline: document.getElementById('timeline-items'),
619
+ screenshot: document.getElementById('screenshot'),
620
+ currentUrl: document.getElementById('current-url'),
621
+ currentStep: document.getElementById('current-step'),
622
+ progressFill: document.getElementById('progress-fill'),
623
+ playBtn: document.getElementById('play-btn'),
624
+ prevBtn: document.getElementById('prev-btn'),
625
+ nextBtn: document.getElementById('next-btn'),
626
+ speedSelect: document.getElementById('speed-select'),
627
+ actionType: document.getElementById('action-type'),
628
+ actionDetail: document.getElementById('action-detail'),
629
+ stepFindings: document.getElementById('step-findings'),
630
+ domDiff: document.getElementById('dom-diff'),
631
+ networkDetails: document.getElementById('network-details'),
632
+ networkBar: document.getElementById('network-bar'),
633
+ findingsSection: document.getElementById('findings-section')
634
+ };
635
+
636
+ // Initialize
637
+ function init() {
638
+ renderTimeline();
639
+ renderNetworkBar();
640
+ goToStep(0);
641
+ setupEventListeners();
642
+ }
643
+
644
+ // Render timeline
645
+ function renderTimeline() {
646
+ const html = RECORDING_DATA.steps.map((step, index) => {
647
+ const hasFinding = step.findings && step.findings.length > 0;
648
+ const findingClass = hasFinding ? 'finding' : (step.success ? 'success' : '');
649
+ const badge = hasFinding
650
+ ? \`<span class="timeline-badge badge-\${step.findings[0].severity.toLowerCase()}">\${step.findings[0].severity}</span>\`
651
+ : '';
652
+
653
+ return \`
654
+ <div class="timeline-item \${findingClass}" data-index="\${index}">
655
+ <div class="timeline-time">\${formatTime(step.timestamp)}</div>
656
+ <div class="timeline-action">\${step.action}</div>
657
+ <div class="timeline-detail">\${step.detail || step.url || ''}</div>
658
+ \${badge}
659
+ </div>
660
+ \`;
661
+ }).join('');
662
+
663
+ elements.timeline.innerHTML = html;
664
+ }
665
+
666
+ // Render network waterfall
667
+ function renderNetworkBar() {
668
+ if (!RECORDING_DATA.networkRequests) return;
669
+
670
+ const maxTime = Math.max(...RECORDING_DATA.networkRequests.map(r => r.endTime || r.startTime));
671
+
672
+ const html = RECORDING_DATA.networkRequests.map(req => {
673
+ const width = Math.max(4, ((req.duration || 100) / maxTime) * 100);
674
+ const typeClass = 'network-' + (req.type || 'other');
675
+
676
+ return \`<div class="network-item \${typeClass}"
677
+ style="width: \${width}px"
678
+ data-url="\${req.url}"></div>\`;
679
+ }).join('');
680
+
681
+ elements.networkBar.innerHTML = html;
682
+ }
683
+
684
+ // Go to specific step
685
+ function goToStep(index) {
686
+ if (index < 0 || index >= RECORDING_DATA.steps.length) return;
687
+
688
+ currentStepIndex = index;
689
+ const step = RECORDING_DATA.steps[index];
690
+
691
+ // Update screenshot
692
+ if (step.screenshot) {
693
+ elements.screenshot.src = step.screenshot;
694
+ }
695
+
696
+ // Update URL
697
+ elements.currentUrl.textContent = step.url || 'N/A';
698
+
699
+ // Update step counter
700
+ elements.currentStep.textContent = index + 1;
701
+
702
+ // Update progress bar
703
+ const progress = ((index + 1) / RECORDING_DATA.steps.length) * 100;
704
+ elements.progressFill.style.width = progress + '%';
705
+
706
+ // Update action details
707
+ elements.actionType.textContent = step.action;
708
+ elements.actionDetail.textContent = step.detail || step.selector || '';
709
+
710
+ // Update findings
711
+ updateFindings(step.findings);
712
+
713
+ // Update DOM diff
714
+ updateDomDiff(step.domDiff);
715
+
716
+ // Update network details
717
+ updateNetworkDetails(step.networkRequests);
718
+
719
+ // Update timeline selection
720
+ document.querySelectorAll('.timeline-item').forEach((item, i) => {
721
+ item.classList.toggle('active', i === index);
722
+ });
723
+
724
+ // Scroll timeline item into view
725
+ const activeItem = document.querySelector('.timeline-item.active');
726
+ if (activeItem) {
727
+ activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
728
+ }
729
+ }
730
+
731
+ // Update findings display
732
+ function updateFindings(findings) {
733
+ if (!findings || findings.length === 0) {
734
+ elements.findingsSection.style.display = 'none';
735
+ return;
736
+ }
737
+
738
+ elements.findingsSection.style.display = 'block';
739
+
740
+ const html = findings.map(finding => \`
741
+ <div class="finding-card \${finding.severity === 'WARN' ? 'warn' : ''}">
742
+ <div class="finding-title">\${finding.type}</div>
743
+ <div class="finding-message">\${finding.message}</div>
744
+ \${finding.evidence ? \`<div class="finding-evidence">\${finding.evidence}</div>\` : ''}
745
+ </div>
746
+ \`).join('');
747
+
748
+ elements.stepFindings.innerHTML = html;
749
+ }
750
+
751
+ // Update DOM diff display
752
+ function updateDomDiff(diff) {
753
+ if (!diff || diff.length === 0) {
754
+ elements.domDiff.innerHTML = '<div class="diff-context">No changes detected</div>';
755
+ return;
756
+ }
757
+
758
+ const html = diff.map(line => {
759
+ if (line.startsWith('+')) {
760
+ return \`<div class="diff-add">\${escapeHtml(line)}</div>\`;
761
+ } else if (line.startsWith('-')) {
762
+ return \`<div class="diff-remove">\${escapeHtml(line)}</div>\`;
763
+ }
764
+ return \`<div class="diff-context">\${escapeHtml(line)}</div>\`;
765
+ }).join('');
766
+
767
+ elements.domDiff.innerHTML = html;
768
+ }
769
+
770
+ // Update network details
771
+ function updateNetworkDetails(requests) {
772
+ if (!requests || requests.length === 0) {
773
+ elements.networkDetails.innerHTML = '<div class="network-detail">No requests</div>';
774
+ return;
775
+ }
776
+
777
+ const html = requests.slice(0, 5).map(req => \`
778
+ <div class="network-detail">
779
+ <div class="network-url">\${req.url}</div>
780
+ <div class="network-meta">
781
+ <span class="\${req.status < 400 ? 'status-ok' : 'status-error'}">\${req.status}</span>
782
+ <span>\${req.type}</span>
783
+ <span>\${req.duration ? req.duration + 'ms' : 'N/A'}</span>
784
+ <span>\${formatBytes(req.size)}</span>
785
+ </div>
786
+ </div>
787
+ \`).join('');
788
+
789
+ elements.networkDetails.innerHTML = html;
790
+ }
791
+
792
+ // Playback controls
793
+ function play() {
794
+ if (isPlaying) {
795
+ pause();
796
+ return;
797
+ }
798
+
799
+ isPlaying = true;
800
+ elements.playBtn.textContent = '⏸';
801
+ document.body.classList.add('playing');
802
+
803
+ const stepDuration = 1500 / playbackSpeed;
804
+ playbackInterval = setInterval(() => {
805
+ if (currentStepIndex >= RECORDING_DATA.steps.length - 1) {
806
+ pause();
807
+ return;
808
+ }
809
+ goToStep(currentStepIndex + 1);
810
+ }, stepDuration);
811
+ }
812
+
813
+ function pause() {
814
+ isPlaying = false;
815
+ elements.playBtn.textContent = '▶';
816
+ document.body.classList.remove('playing');
817
+ if (playbackInterval) {
818
+ clearInterval(playbackInterval);
819
+ playbackInterval = null;
820
+ }
821
+ }
822
+
823
+ // Event listeners
824
+ function setupEventListeners() {
825
+ elements.playBtn.addEventListener('click', play);
826
+ elements.prevBtn.addEventListener('click', () => goToStep(currentStepIndex - 1));
827
+ elements.nextBtn.addEventListener('click', () => goToStep(currentStepIndex + 1));
828
+
829
+ elements.speedSelect.addEventListener('change', (e) => {
830
+ playbackSpeed = parseFloat(e.target.value);
831
+ if (isPlaying) {
832
+ pause();
833
+ play();
834
+ }
835
+ });
836
+
837
+ // Timeline click
838
+ elements.timeline.addEventListener('click', (e) => {
839
+ const item = e.target.closest('.timeline-item');
840
+ if (item) {
841
+ const index = parseInt(item.dataset.index);
842
+ goToStep(index);
843
+ }
844
+ });
845
+
846
+ // Progress bar click
847
+ document.getElementById('progress-bar').addEventListener('click', (e) => {
848
+ const rect = e.target.getBoundingClientRect();
849
+ const percent = (e.clientX - rect.left) / rect.width;
850
+ const index = Math.floor(percent * RECORDING_DATA.steps.length);
851
+ goToStep(index);
852
+ });
853
+
854
+ // Filter buttons
855
+ document.querySelectorAll('.filter-btn').forEach(btn => {
856
+ btn.addEventListener('click', () => {
857
+ const filter = btn.dataset.filter;
858
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
859
+ btn.classList.add('active');
860
+ filterTimeline(filter);
861
+ });
862
+ });
863
+
864
+ // Keyboard navigation
865
+ document.addEventListener('keydown', (e) => {
866
+ if (e.key === 'ArrowLeft') goToStep(currentStepIndex - 1);
867
+ if (e.key === 'ArrowRight') goToStep(currentStepIndex + 1);
868
+ if (e.key === ' ') { e.preventDefault(); play(); }
869
+ });
870
+ }
871
+
872
+ // Filter timeline
873
+ function filterTimeline(filter) {
874
+ document.querySelectorAll('.timeline-item').forEach(item => {
875
+ const index = parseInt(item.dataset.index);
876
+ const step = RECORDING_DATA.steps[index];
877
+
878
+ let visible = true;
879
+ if (filter === 'finding') {
880
+ visible = step.findings && step.findings.length > 0;
881
+ } else if (filter === 'navigation') {
882
+ visible = step.action === 'navigate' || step.action === 'navigation';
883
+ } else if (filter === 'click') {
884
+ visible = step.action === 'click';
885
+ }
886
+
887
+ item.style.display = visible ? 'block' : 'none';
888
+ });
889
+ }
890
+
891
+ // Utility functions
892
+ function formatTime(timestamp) {
893
+ if (!timestamp) return '00:00.000';
894
+ const date = new Date(timestamp);
895
+ const mins = date.getMinutes().toString().padStart(2, '0');
896
+ const secs = date.getSeconds().toString().padStart(2, '0');
897
+ const ms = date.getMilliseconds().toString().padStart(3, '0');
898
+ return \`\${mins}:\${secs}.\${ms}\`;
899
+ }
900
+
901
+ function formatBytes(bytes) {
902
+ if (!bytes) return 'N/A';
903
+ if (bytes < 1024) return bytes + ' B';
904
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
905
+ return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
906
+ }
907
+
908
+ function escapeHtml(str) {
909
+ const div = document.createElement('div');
910
+ div.textContent = str;
911
+ return div.innerHTML;
912
+ }
913
+
914
+ // Start
915
+ init();
916
+ </script>
917
+ </body>
918
+ </html>`;
919
+
920
+ // ═══════════════════════════════════════════════════════════════════════════════
921
+ // FLIGHT RECORDER VIEWER GENERATOR
922
+ // ═══════════════════════════════════════════════════════════════════════════════
923
+
924
+ class FlightRecorderViewer {
925
+ constructor(options = {}) {
926
+ this.outputPath = options.outputPath || ".vibecheck/replay";
927
+ this.includeScreenshots = options.includeScreenshots !== false;
928
+ this.maxScreenshotSize = options.maxScreenshotSize || 500 * 1024; // 500KB
929
+ }
930
+
931
+ /**
932
+ * Generate interactive HTML viewer from recording data
933
+ */
934
+ generate(recordingData, outputFileName = "flight-recorder.html") {
935
+ // Process recording data
936
+ const processedData = this.processRecordingData(recordingData);
937
+
938
+ // Calculate summary stats
939
+ const stats = this.calculateStats(processedData);
940
+
941
+ // Generate HTML
942
+ let html = HTML_TEMPLATE
943
+ .replace(/\{\{TOTAL_STEPS\}\}/g, stats.totalSteps)
944
+ .replace(/\{\{FINDINGS_COUNT\}\}/g, stats.findingsCount)
945
+ .replace(/\{\{DURATION\}\}/g, stats.duration)
946
+ .replace("{{RECORDING_DATA}}", JSON.stringify(processedData, null, 2));
947
+
948
+ // Ensure output directory exists
949
+ const outputDir = path.resolve(this.outputPath);
950
+ if (!fs.existsSync(outputDir)) {
951
+ fs.mkdirSync(outputDir, { recursive: true });
952
+ }
953
+
954
+ // Write HTML file
955
+ const outputFile = path.join(outputDir, outputFileName);
956
+ fs.writeFileSync(outputFile, html, "utf8");
957
+
958
+ return {
959
+ path: outputFile,
960
+ stats
961
+ };
962
+ }
963
+
964
+ /**
965
+ * Process recording data for viewer
966
+ */
967
+ processRecordingData(data) {
968
+ const processed = {
969
+ meta: {
970
+ url: data.url || data.startUrl,
971
+ startTime: data.startTime,
972
+ endTime: data.endTime,
973
+ duration: data.duration
974
+ },
975
+ steps: [],
976
+ networkRequests: data.networkRequests || [],
977
+ findings: data.findings || []
978
+ };
979
+
980
+ // Process steps
981
+ for (const step of (data.steps || data.replay || [])) {
982
+ const processedStep = {
983
+ timestamp: step.timestamp,
984
+ action: step.action || step.type,
985
+ detail: step.detail || step.selector || step.url,
986
+ url: step.url || step.pageUrl,
987
+ success: step.success !== false,
988
+ findings: step.findings || step.detections || [],
989
+ networkRequests: step.networkRequests || [],
990
+ domDiff: step.domDiff || step.domChanges || []
991
+ };
992
+
993
+ // Handle screenshots
994
+ if (this.includeScreenshots && step.screenshot) {
995
+ if (typeof step.screenshot === "string") {
996
+ // Already a data URL or path
997
+ if (step.screenshot.startsWith("data:")) {
998
+ processedStep.screenshot = step.screenshot;
999
+ } else if (fs.existsSync(step.screenshot)) {
1000
+ // Convert file to data URL
1001
+ processedStep.screenshot = this.fileToDataUrl(step.screenshot);
1002
+ }
1003
+ } else if (Buffer.isBuffer(step.screenshot)) {
1004
+ processedStep.screenshot = `data:image/png;base64,${step.screenshot.toString("base64")}`;
1005
+ }
1006
+ }
1007
+
1008
+ processed.steps.push(processedStep);
1009
+ }
1010
+
1011
+ return processed;
1012
+ }
1013
+
1014
+ /**
1015
+ * Convert file to data URL
1016
+ */
1017
+ fileToDataUrl(filePath) {
1018
+ try {
1019
+ const buffer = fs.readFileSync(filePath);
1020
+
1021
+ // Check size limit
1022
+ if (buffer.length > this.maxScreenshotSize) {
1023
+ // Return placeholder or compressed version
1024
+ return null;
1025
+ }
1026
+
1027
+ const ext = path.extname(filePath).toLowerCase();
1028
+ const mimeTypes = {
1029
+ ".png": "image/png",
1030
+ ".jpg": "image/jpeg",
1031
+ ".jpeg": "image/jpeg",
1032
+ ".gif": "image/gif",
1033
+ ".webp": "image/webp"
1034
+ };
1035
+
1036
+ const mimeType = mimeTypes[ext] || "image/png";
1037
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * Calculate summary statistics
1045
+ */
1046
+ calculateStats(data) {
1047
+ const findingsCount = data.steps.reduce((count, step) => {
1048
+ return count + (step.findings?.length || 0);
1049
+ }, 0) + (data.findings?.length || 0);
1050
+
1051
+ let duration = "N/A";
1052
+ if (data.meta.duration) {
1053
+ duration = this.formatDuration(data.meta.duration);
1054
+ } else if (data.meta.startTime && data.meta.endTime) {
1055
+ const ms = new Date(data.meta.endTime) - new Date(data.meta.startTime);
1056
+ duration = this.formatDuration(ms);
1057
+ }
1058
+
1059
+ return {
1060
+ totalSteps: data.steps.length,
1061
+ findingsCount,
1062
+ duration
1063
+ };
1064
+ }
1065
+
1066
+ /**
1067
+ * Format duration in human readable format
1068
+ */
1069
+ formatDuration(ms) {
1070
+ if (typeof ms !== "number") return "N/A";
1071
+
1072
+ if (ms < 1000) return `${ms}ms`;
1073
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
1074
+
1075
+ const mins = Math.floor(ms / 60000);
1076
+ const secs = ((ms % 60000) / 1000).toFixed(0);
1077
+ return `${mins}m ${secs}s`;
1078
+ }
1079
+
1080
+ /**
1081
+ * Generate viewer from Reality Mode report
1082
+ */
1083
+ static fromRealityReport(reportPath, options = {}) {
1084
+ const viewer = new FlightRecorderViewer(options);
1085
+
1086
+ let reportData;
1087
+ if (typeof reportPath === "string") {
1088
+ reportData = JSON.parse(fs.readFileSync(reportPath, "utf8"));
1089
+ } else {
1090
+ reportData = reportPath;
1091
+ }
1092
+
1093
+ // Transform Reality Mode report to recording format
1094
+ const recordingData = {
1095
+ url: reportData.url || reportData.targetUrl,
1096
+ startTime: reportData.startTime || reportData.timestamp,
1097
+ endTime: reportData.endTime,
1098
+ duration: reportData.duration || reportData.totalDuration,
1099
+ steps: [],
1100
+ networkRequests: reportData.networkRequests || [],
1101
+ findings: reportData.findings || []
1102
+ };
1103
+
1104
+ // Convert Reality Mode findings to steps
1105
+ if (reportData.passA) {
1106
+ for (const page of (reportData.passA.pages || [])) {
1107
+ recordingData.steps.push({
1108
+ timestamp: page.timestamp,
1109
+ action: "navigate",
1110
+ url: page.url,
1111
+ success: true,
1112
+ findings: page.findings || []
1113
+ });
1114
+
1115
+ for (const interaction of (page.interactions || [])) {
1116
+ recordingData.steps.push({
1117
+ timestamp: interaction.timestamp,
1118
+ action: interaction.type || "click",
1119
+ detail: interaction.selector,
1120
+ url: page.url,
1121
+ success: !interaction.dead,
1122
+ findings: interaction.findings || [],
1123
+ screenshot: interaction.screenshot
1124
+ });
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ // Add findings as separate steps if not already included
1130
+ if (reportData.findings) {
1131
+ for (const finding of reportData.findings) {
1132
+ if (!recordingData.steps.some(s => s.findings?.includes(finding))) {
1133
+ recordingData.steps.push({
1134
+ timestamp: finding.timestamp,
1135
+ action: "finding",
1136
+ detail: finding.message,
1137
+ findings: [finding],
1138
+ screenshot: finding.screenshot
1139
+ });
1140
+ }
1141
+ }
1142
+ }
1143
+
1144
+ // Sort steps by timestamp
1145
+ recordingData.steps.sort((a, b) => {
1146
+ return new Date(a.timestamp) - new Date(b.timestamp);
1147
+ });
1148
+
1149
+ return viewer.generate(recordingData);
1150
+ }
1151
+ }
1152
+
1153
+ // ═══════════════════════════════════════════════════════════════════════════════
1154
+ // EXPORTS
1155
+ // ═══════════════════════════════════════════════════════════════════════════════
1156
+
1157
+ module.exports = {
1158
+ FlightRecorderViewer,
1159
+ HTML_TEMPLATE
1160
+ };