@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,1160 +0,0 @@
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
- };