webgl-forensics 3.1.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 (35) hide show
  1. package/.github/workflows/forensics.yml +44 -0
  2. package/CLAUDE_CODE.md +49 -0
  3. package/MASTER_PROMPT.md +202 -0
  4. package/README.md +579 -0
  5. package/SKILL.md +1256 -0
  6. package/package.json +53 -0
  7. package/project summary.md +33 -0
  8. package/puppeteer-runner.js +486 -0
  9. package/scripts/00-tech-stack-detect.js +185 -0
  10. package/scripts/01-source-map-extractor.js +73 -0
  11. package/scripts/02-interaction-model.js +129 -0
  12. package/scripts/03-responsive-analysis.js +115 -0
  13. package/scripts/04-page-transitions.js +128 -0
  14. package/scripts/05-loading-sequence.js +124 -0
  15. package/scripts/06-audio-extraction.js +115 -0
  16. package/scripts/07-accessibility-reduced-motion.js +133 -0
  17. package/scripts/08-complexity-scorer.js +138 -0
  18. package/scripts/09-visual-diff-validator.js +113 -0
  19. package/scripts/10-webgpu-extractor.js +248 -0
  20. package/scripts/11-scroll-screenshot-grid.js +76 -0
  21. package/scripts/12-network-waterfall.js +151 -0
  22. package/scripts/13-react-fiber-walker.js +285 -0
  23. package/scripts/14-shader-hotpatch.js +349 -0
  24. package/scripts/15-multipage-crawler.js +221 -0
  25. package/scripts/16-gsap-timeline-recorder.js +335 -0
  26. package/scripts/17-r3f-fiber-serializer.js +316 -0
  27. package/scripts/18-font-extractor.js +290 -0
  28. package/scripts/19-design-token-export.js +85 -0
  29. package/scripts/20-timeline-visualizer.js +61 -0
  30. package/scripts/21-lighthouse-audit.js +35 -0
  31. package/scripts/22-sitemap-crawler.js +128 -0
  32. package/scripts/23-scaffold-generator.js +451 -0
  33. package/scripts/24-ai-reconstruction.js +226 -0
  34. package/scripts/25-shader-annotator.js +226 -0
  35. package/tui.js +196 -0
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "webgl-forensics",
3
+ "version": "3.1.0",
4
+ "description": "The definitive 24-phase reverse engineering engine for WebGL, Three.js, GSAP, and animated websites. Clone any site.",
5
+ "bin": {
6
+ "webgl-forensics": "puppeteer-runner.js"
7
+ },
8
+ "scripts": {
9
+ "forensics": "node puppeteer-runner.js",
10
+ "test": "node puppeteer-runner.js https://example.com --dry-run",
11
+ "lint": "node -e \"require('./puppeteer-runner.js')\" 2>&1 || true"
12
+ },
13
+ "keywords": [
14
+ "webgl",
15
+ "three.js",
16
+ "gsap",
17
+ "forensics",
18
+ "reverse-engineering",
19
+ "website-cloning",
20
+ "shader-extraction",
21
+ "scroll-animation",
22
+ "puppeteer",
23
+ "r3f",
24
+ "react-three-fiber",
25
+ "design-tokens",
26
+ "framer-motion",
27
+ "lenis"
28
+ ],
29
+ "dependencies": {
30
+ "chalk": "^5.6.2",
31
+ "inquirer": "^9.3.8",
32
+ "lighthouse": "^13.0.3",
33
+ "ora": "^8.2.0",
34
+ "puppeteer": "^22.0.0",
35
+ "puppeteer-screen-recorder": "^3.0.6"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "author": "404kidwiz",
41
+ "license": "MIT",
42
+ "homepage": "https://github.com/404kidwiz/webgl-forensics#readme",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/404kidwiz/webgl-forensics.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/404kidwiz/webgl-forensics/issues"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }
@@ -0,0 +1,33 @@
1
+ # WebGL Forensics (v3.1)
2
+
3
+ ## Overview
4
+ WebGL Forensics is the definitive reverse engineering engine for WebGL, Three.js, GSAP, and complex animated websites. It acts as an automated "site cloner" and forensics tool for developers who want to understand or reconstruct high-end Awwwards-style sites. It leverages Puppeteer to seamlessly analyze page payloads, intercept WebGL shaders, capture GSAP timelines, and serialize React-Three-Fiber (R3F) fiber trees.
5
+
6
+ With the release of v3.1, WebGL Forensics shifts from merely being an analysis tool into being an **actionable scaffolding engine** that can auto-generate the foundation of a modern Next.js 15 site from the extracted design, animation, and 3D data.
7
+
8
+ ## Value Proposition
9
+ Instead of manually inspecting the network tab for shaders or trying to guess easing curves for scroll animations, developers can run this CLI and instantly get:
10
+ 1. **Design Tokens**: Standardized CSS variables extracted from the target DOM.
11
+ 2. **Animation Signatures**: Hard data on `gsap.to()` calls including durations and triggers.
12
+ 3. **3D Assets & Logic**: GLSL shaders (vertex + fragment) and the physical tree structure of Three.js scenes.
13
+ 4. **Instant Scaffolding**: Automatically built `Next.js` setups integrating `lenis`, `gsap`, and `@react-three/fiber` wired specifically to replicate the target site.
14
+ 5. **AI Guides**: Formatted system prompts designed to assist Claude/Gemini in reconstructing the exact UI and Logic based on forensic data.
15
+
16
+ ## Core v3.1 Additions
17
+ - **Sitemap Crawler** (`22-sitemap-crawler.js`): Intelligent crawler prioritizing links found in `/sitemap.xml` with automatic DOM-scraping fallback.
18
+ - **Next.js Scaffold Generator** (`23-scaffold-generator.js`): Parses forensic JSON and auto-generates a boilerplate React framework folder pre-configured with the exact libraries detected (e.g. framer-motion, R3F, gsap).
19
+ - **AI Reconstruction** (`24-ai-reconstruction.js`): Automatically packages forensic metadata and GLSL dumps into a Claude-ready "Here is a website, write the code to clone it" prompt.
20
+ - **Shader Annotator** (`25-shader-annotator.js`): Identifies common GLSL patterns offline and passes ambiguous shaders to local or remote AI for mathematical annotation.
21
+ - **TUI Dashboard** (`tui.js`): Terminal user interface using Ink/Chalk with real-time feedback on forensics progress instead of raw logs.
22
+
23
+ ## Quick Start
24
+ ```bash
25
+ # Full automated run with scaffolding and AI prep
26
+ npx webgl-forensics https://example.com --scaffold --ai-report --screenshots --sitemap
27
+
28
+ # Interactive CLI mode
29
+ npx webgl-forensics --interactive
30
+ ```
31
+
32
+ ## Architecture Notes
33
+ The engine runs scripts consecutively in a headless browser context by injecting them into the DOM (`page.evaluate()`) to access core objects like `window.__THREE__`, `window.gsap`, or `WebGLRenderingContext.prototype.shaderSource`. Data is aggregated into a central `forensics-report-[domain].json` which drives the secondary AI and scaffolding steps.
@@ -0,0 +1,486 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * webgl-forensics — Standalone Puppeteer Runner v3.1
4
+ * The definitive 24-phase forensics engine for WebGL, GSAP, R3F, and animated sites.
5
+ *
6
+ * USAGE:
7
+ * npx webgl-forensics [URL] [options]
8
+ * node puppeteer-runner.js [URL] [options]
9
+ *
10
+ * ─── FLAGS ────────────────────────────────────────────────────────────────────
11
+ *
12
+ * v3.1 (new):
13
+ * --interactive Interactive TUI — configure run from terminal menu
14
+ * --scaffold Generate a Next.js 15 starter project from forensics
15
+ * --ai-report Generate AI Claude/Gemini reconstruction report (needs API key)
16
+ * --annotate-shaders AI-annotate extracted GLSL shaders (needs API key)
17
+ * --sitemap Use sitemap.xml for page discovery instead of DOM crawl
18
+ *
19
+ * v3.0:
20
+ * --compare [URL] Diff against a local clone URL
21
+ * --record Record an MP4 of the scroll session
22
+ * --download-assets Download GLB, WOFF2, HDR assets
23
+ * --format [json|html] Output format (default: json)
24
+ * --history Persist run metrics to local JSON history
25
+ *
26
+ * v2.0:
27
+ * --focus [3d|animations|scroll|layout|all]
28
+ * --output [path] Output directory (default: ./forensics-output)
29
+ * --headless / --no-headless Browser mode
30
+ * --screenshots Capture scroll screenshots
31
+ * --multipage Crawl all internal pages
32
+ * --timeout [ms] Navigation timeout (default: 30000)
33
+ * --dry-run Show what would run without fetching
34
+ *
35
+ * ──────────────────────────────────────────────────────────────────────────────
36
+ */
37
+
38
+ 'use strict';
39
+
40
+ const path = require('path');
41
+ const fs = require('fs');
42
+
43
+ // ═══════════════════════════════════════════════════════
44
+ // OPTIONAL V3 DEPS — graceful fallback if not installed
45
+ // ═══════════════════════════════════════════════════════
46
+ let PuppeteerScreenRecorder;
47
+ try { PuppeteerScreenRecorder = require('puppeteer-screen-recorder').PuppeteerScreenRecorder; } catch {}
48
+
49
+ let runLighthouseAudit;
50
+ try { runLighthouseAudit = require('./scripts/21-lighthouse-audit.js'); } catch {}
51
+
52
+ let generateTimelineHtml;
53
+ try { generateTimelineHtml = require('./scripts/20-timeline-visualizer.js'); } catch {}
54
+
55
+ // V3.1 modules
56
+ let generateScaffold;
57
+ try { generateScaffold = require('./scripts/23-scaffold-generator.js'); } catch {}
58
+
59
+ let generateReconstructionReport;
60
+ try { generateReconstructionReport = require('./scripts/24-ai-reconstruction.js'); } catch {}
61
+
62
+ let annotateShaders;
63
+ try { annotateShaders = require('./scripts/25-shader-annotator.js'); } catch {}
64
+
65
+ let tuiModule;
66
+ try { tuiModule = require('./tui.js'); } catch {}
67
+
68
+ // ═══════════════════════════════════════════════════════
69
+ // ARG PARSING
70
+ // ═══════════════════════════════════════════════════════
71
+ const rawArgs = process.argv.slice(2);
72
+ let targetUrl = rawArgs.find(a => a.startsWith('http'));
73
+
74
+ function argVal(flag, def, args = rawArgs) {
75
+ const idx = args.indexOf(flag);
76
+ return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('--') ? args[idx + 1] : def;
77
+ }
78
+ function hasFlag(flag) { return rawArgs.includes(flag); }
79
+
80
+ let opts = {
81
+ focus: argVal('--focus', 'all'),
82
+ output: argVal('--output', './forensics-output'),
83
+ headless: !hasFlag('--no-headless'),
84
+ screenshots: hasFlag('--screenshots'),
85
+ multipage: hasFlag('--multipage'),
86
+ sitemap: hasFlag('--sitemap'),
87
+ timeout: parseInt(argVal('--timeout', '30000'), 10),
88
+ dryRun: hasFlag('--dry-run'),
89
+ interactive: hasFlag('--interactive'),
90
+ // V3.0
91
+ compareUrl: argVal('--compare', null),
92
+ record: hasFlag('--record'),
93
+ downloadAssets: hasFlag('--download-assets'),
94
+ formatHtml: argVal('--format', 'json') === 'html',
95
+ history: hasFlag('--history'),
96
+ // V3.1
97
+ scaffold: hasFlag('--scaffold'),
98
+ aiReport: hasFlag('--ai-report'),
99
+ annotateShaders: hasFlag('--annotate-shaders'),
100
+ };
101
+
102
+ // ═══════════════════════════════════════════════════════
103
+ // HELP / DRY-RUN
104
+ // ═══════════════════════════════════════════════════════
105
+ if (!targetUrl && !opts.interactive && !opts.dryRun) {
106
+ console.log(`
107
+ ╔══════════════════════════════════════════════════════════╗
108
+ ║ webgl-forensics v3.1 — The Site Reverse-Engineering CLI ║
109
+ ╚══════════════════════════════════════════════════════════╝
110
+
111
+ BASIC: npx webgl-forensics https://site.com
112
+ INTERACTIVE: npx webgl-forensics --interactive
113
+ COMPARE: npx webgl-forensics https://site.com --compare http://localhost:3000
114
+ SCAFFOLD: npx webgl-forensics https://site.com --scaffold --ai-report
115
+ SHADERS: npx webgl-forensics https://site.com --annotate-shaders --no-headless
116
+ FULL RUN: npx webgl-forensics https://site.com --scaffold --ai-report --screenshots --sitemap
117
+
118
+ Run with --interactive for an interactive configuration menu.
119
+ Run with --dry-run to preview phases without fetching.
120
+ `);
121
+ process.exit(0);
122
+ }
123
+
124
+ if (opts.dryRun) {
125
+ console.log('📋 DRY RUN — would analyze:', targetUrl || '(no URL given)');
126
+ console.log(' Focus:', opts.focus);
127
+ console.log(' Scaffold:', opts.scaffold);
128
+ console.log(' AI Report:', opts.aiReport);
129
+ console.log(' Sitemap:', opts.sitemap);
130
+ console.log(' Compare:', opts.compareUrl || 'None');
131
+ process.exit(0);
132
+ }
133
+
134
+ // ═══════════════════════════════════════════════════════
135
+ // SCRIPT LOADER
136
+ // ═══════════════════════════════════════════════════════
137
+ const SCRIPTS_DIR = path.join(__dirname, 'scripts');
138
+
139
+ function loadScript(filename) {
140
+ const p = path.join(SCRIPTS_DIR, filename);
141
+ if (!fs.existsSync(p)) throw new Error(`Script not found: ${p}`);
142
+ return fs.readFileSync(p, 'utf-8');
143
+ }
144
+
145
+ function getPhases(focus) {
146
+ const all = [
147
+ { name: 'loading-sequence', script: '05-loading-sequence.js', focus: ['all', '3d', 'animations'] },
148
+ { name: 'tech-stack', script: '00-tech-stack-detect.js', focus: ['all', '3d', 'animations', 'scroll', 'layout'] },
149
+ { name: 'source-maps', script: '01-source-map-extractor.js', focus: ['all'] },
150
+ { name: 'sitemap-crawler', script: '22-sitemap-crawler.js', focus: ['all', 'layout'] },
151
+ { name: 'network-waterfall', script: '12-network-waterfall.js', focus: ['all', 'layout'] },
152
+ { name: 'react-fiber-walker', script: '13-react-fiber-walker.js', focus: ['all', '3d'] },
153
+ { name: 'r3f-serializer', script: '17-r3f-fiber-serializer.js', focus: ['all', '3d'] },
154
+ { name: 'shader-hotpatch', script: '14-shader-hotpatch.js', focus: ['all', '3d'] },
155
+ { name: 'webgpu-extractor', script: '10-webgpu-extractor.js', focus: ['all', '3d'] },
156
+ { name: 'gsap-timeline', script: '16-gsap-timeline-recorder.js', focus: ['all', 'animations', 'scroll'] },
157
+ { name: 'interaction-model', script: '02-interaction-model.js', focus: ['all', 'animations'] },
158
+ { name: 'page-transitions', script: '04-page-transitions.js', focus: ['all', 'animations'] },
159
+ { name: 'font-extractor', script: '18-font-extractor.js', focus: ['all', 'layout'] },
160
+ { name: 'design-token-export', script: '19-design-token-export.js', focus: ['all', 'layout'] },
161
+ { name: 'responsive-analysis', script: '03-responsive-analysis.js', focus: ['all', 'layout'] },
162
+ { name: 'audio-extraction', script: '06-audio-extraction.js', focus: ['all'] },
163
+ { name: 'accessibility', script: '07-accessibility-reduced-motion.js', focus: ['all'] },
164
+ { name: 'complexity-scorer', script: '08-complexity-scorer.js', focus: ['all'] },
165
+ { name: 'visual-diff', script: '09-visual-diff-validator.js', focus: ['all'] },
166
+ ];
167
+
168
+ // Plugin discovery
169
+ if (fs.existsSync(SCRIPTS_DIR)) {
170
+ for (const f of fs.readdirSync(SCRIPTS_DIR)) {
171
+ if (f.startsWith('custom-') && f.endsWith('.js')) {
172
+ all.push({ name: f.replace('.js', ''), script: f, focus: ['all'] });
173
+ }
174
+ }
175
+ }
176
+
177
+ return focus === 'all' ? all : all.filter(p => p.focus.includes(focus));
178
+ }
179
+
180
+ // ═══════════════════════════════════════════════════════
181
+ // HISTORY
182
+ // ═══════════════════════════════════════════════════════
183
+ function writeHistory(report, outputDir) {
184
+ if (!opts.history) return;
185
+ const histFile = path.join(outputDir, 'run-history.json');
186
+ let history = [];
187
+ if (fs.existsSync(histFile)) {
188
+ try { history = JSON.parse(fs.readFileSync(histFile, 'utf8')); } catch {}
189
+ }
190
+ history.push({
191
+ url: report.meta.url,
192
+ timestamp: report.meta.timestamp,
193
+ complexity: report.meta.phases['complexity-scorer']?.score || 0,
194
+ tech: Object.keys(report.meta.phases['tech-stack']?.detected || {}).filter(k => report.meta.phases['tech-stack'].detected[k]),
195
+ });
196
+ fs.writeFileSync(histFile, JSON.stringify(history, null, 2));
197
+ }
198
+
199
+ // ═══════════════════════════════════════════════════════
200
+ // DIFF
201
+ // ═══════════════════════════════════════════════════════
202
+ function calculateDiff(source, clone) {
203
+ const scoreDiff = Math.abs(
204
+ (source.meta.phases['complexity-scorer']?.score || 0) -
205
+ (clone.meta.phases['complexity-scorer']?.score || 0)
206
+ );
207
+ const srcTech = Object.keys(source.meta.phases['tech-stack']?.detected || {}).filter(k => source.meta.phases['tech-stack'].detected[k]);
208
+ const clnTech = Object.keys(clone.meta.phases['tech-stack']?.detected || {}).filter(k => clone.meta.phases['tech-stack'].detected[k]);
209
+ return {
210
+ fidelityScore: Math.max(0, 100 - scoreDiff * 0.5),
211
+ techOverlap: srcTech.filter(t => clnTech.includes(t)),
212
+ missingInClone: srcTech.filter(t => !clnTech.includes(t)),
213
+ };
214
+ }
215
+
216
+ // ═══════════════════════════════════════════════════════
217
+ // MAIN ANALYZER
218
+ // ═══════════════════════════════════════════════════════
219
+ async function analyzeUrl(url, browser, isCompare = false, renderer = null) {
220
+ const domain = new URL(url).hostname.replace(/[^a-z0-9]/gi, '-');
221
+ const ts = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '-');
222
+ const dirName = isCompare ? `compare-${domain}-${ts}` : `${domain}-${ts}`;
223
+ const outputDir = path.resolve(opts.output, dirName);
224
+ fs.mkdirSync(outputDir, { recursive: true });
225
+
226
+ const log = renderer
227
+ ? (msg) => renderer.log(msg)
228
+ : (msg) => console.log(`[${new Date().toLocaleTimeString()}] [${isCompare ? 'CLONE' : 'SRC'}] ${msg}`);
229
+
230
+ log(`Analyzing: ${url}`);
231
+ const page = await browser.newPage();
232
+
233
+ // VIDEO RECORDING
234
+ let recorder;
235
+ if (opts.record && PuppeteerScreenRecorder && !isCompare) {
236
+ log('Starting video recording...');
237
+ recorder = new PuppeteerScreenRecorder(page, { format: 'mp4' });
238
+ await recorder.start(path.join(outputDir, 'scroll-recording.mp4'));
239
+ }
240
+
241
+ // ASSET DOWNLOAD INTERCEPTOR
242
+ if (opts.downloadAssets) {
243
+ const assetsDir = path.join(outputDir, 'assets');
244
+ fs.mkdirSync(assetsDir, { recursive: true });
245
+ page.on('response', async (response) => {
246
+ const rUrl = response.url();
247
+ const ext = rUrl.split('.').pop()?.split('?')[0]?.toLowerCase();
248
+ if (['glb', 'gltf', 'woff2', 'hdr', 'ttf', 'exr'].includes(ext)) {
249
+ try {
250
+ const buf = await response.buffer();
251
+ const fname = rUrl.split('/').pop().split('?')[0];
252
+ fs.writeFileSync(path.join(assetsDir, fname), buf);
253
+ log(`Downloaded: ${fname}`);
254
+ } catch {}
255
+ }
256
+ });
257
+ }
258
+
259
+ // SHADER HOTPATCH (pre-navigation)
260
+ try {
261
+ const shaderScript = loadScript('14-shader-hotpatch.js');
262
+ await page.evaluateOnNewDocument(shaderScript);
263
+ } catch {}
264
+
265
+ log('Navigating...');
266
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: opts.timeout });
267
+ await page.waitForTimeout(3000);
268
+
269
+ const report = {
270
+ meta: { url, domain, timestamp: new Date().toISOString(), focus: opts.focus, phases: {} },
271
+ errors: [],
272
+ };
273
+
274
+ // ── RUN ALL PHASES ──────────────────────────────────
275
+ const phases = getPhases(opts.focus);
276
+ for (const phase of phases) {
277
+ if (renderer) renderer.start(phase.name, `Running ${phase.name}...`);
278
+ try {
279
+ const script = loadScript(phase.script);
280
+ const result = await page.evaluate(new Function(`
281
+ try { return eval(${JSON.stringify(script)}) }
282
+ catch(e) { return { error: e.message, phase: '${phase.name}' }; }
283
+ `));
284
+ report.meta.phases[phase.name] = result;
285
+ fs.writeFileSync(path.join(outputDir, `${phase.name}.json`), JSON.stringify(result, null, 2));
286
+
287
+ const detail = getPhaseDetail(phase.name, result);
288
+ if (renderer) renderer.succeed(phase.name, detail);
289
+ else log(`✓ ${phase.name}${detail ? ` — ${detail}` : ''}`);
290
+ } catch (err) {
291
+ report.errors.push(`Phase ${phase.name}: ${err.message}`);
292
+ if (renderer) renderer.fail(phase.name, err.message);
293
+ else log(`✗ ${phase.name} failed: ${err.message}`);
294
+ }
295
+ }
296
+
297
+ // ── LIGHTHOUSE ─────────────────────────────────────
298
+ if (opts.lighthouse !== false && runLighthouseAudit) {
299
+ log('Running Lighthouse audit...');
300
+ try {
301
+ const wsPort = new URL(browser.wsEndpoint()).port;
302
+ const lhResult = await runLighthouseAudit(url, wsPort);
303
+ report.meta.phases['lighthouse'] = lhResult;
304
+ fs.writeFileSync(path.join(outputDir, 'lighthouse.json'), JSON.stringify(lhResult, null, 2));
305
+ } catch (e) {
306
+ log(`Lighthouse failed: ${e.message}`);
307
+ }
308
+ }
309
+
310
+ // ── SCROLL (screenshots / recording) ───────────────
311
+ if (opts.screenshots || (opts.record && !isCompare)) {
312
+ const totalH = await page.evaluate(() => document.documentElement.scrollHeight);
313
+ const viewH = 900;
314
+ const steps = 10;
315
+ const ssDir = path.join(outputDir, 'screenshots');
316
+ if (opts.screenshots) fs.mkdirSync(ssDir, { recursive: true });
317
+
318
+ for (let i = 0; i <= steps; i++) {
319
+ const pct = i / steps;
320
+ await page.evaluate(y => window.scrollTo({ top: y, behavior: 'smooth' }), Math.round(pct * (totalH - viewH)));
321
+ await page.waitForTimeout(1000);
322
+ if (opts.screenshots) {
323
+ await page.screenshot({ path: path.join(ssDir, `scroll-${Math.round(pct * 100)}pct.png`) });
324
+ }
325
+ }
326
+ await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'smooth' }));
327
+ await page.waitForTimeout(1000);
328
+ }
329
+
330
+ if (recorder) await recorder.stop();
331
+
332
+ // ── HTML TIMELINE ───────────────────────────────────
333
+ if (opts.formatHtml && generateTimelineHtml) {
334
+ const html = generateTimelineHtml(report.meta.phases['gsap-timeline']?.keyTimelines || []);
335
+ fs.writeFileSync(path.join(outputDir, 'timeline.html'), html);
336
+ log('Generated timeline.html');
337
+ }
338
+
339
+ // ── V3.1: SHADER ANNOTATION ─────────────────────────
340
+ if (opts.annotateShaders && annotateShaders) {
341
+ log('Annotating shaders with AI...');
342
+ try {
343
+ const shaderData = report.meta.phases['shader-hotpatch'];
344
+ const annotations = await annotateShaders(shaderData);
345
+ report.meta.phases['shader-annotations'] = annotations;
346
+ fs.writeFileSync(path.join(outputDir, 'shader-annotations.json'), JSON.stringify(annotations, null, 2));
347
+ log(`Annotated ${annotations.length} shader(s)`);
348
+ } catch (e) {
349
+ log(`Shader annotation failed: ${e.message}`);
350
+ }
351
+ }
352
+
353
+ // ── V3.1: SCAFFOLD ──────────────────────────────────
354
+ if (opts.scaffold && generateScaffold && !isCompare) {
355
+ log('Generating Next.js scaffold...');
356
+ try {
357
+ const result = generateScaffold(report, outputDir);
358
+ report.meta.scaffold = result;
359
+ log(`Scaffold generated: ${result.filesGenerated} files → ${result.scaffoldDir}`);
360
+ } catch (e) {
361
+ log(`Scaffold failed: ${e.message}`);
362
+ }
363
+ }
364
+
365
+ // ── V3.1: AI RECONSTRUCTION REPORT ─────────────────
366
+ if (opts.aiReport && generateReconstructionReport && !isCompare) {
367
+ log('Generating AI reconstruction report...');
368
+ try {
369
+ const md = await generateReconstructionReport(report);
370
+ const mdPath = path.join(outputDir, 'reconstruction-guide.md');
371
+ fs.writeFileSync(mdPath, md);
372
+ log(`AI report saved: reconstruction-guide.md`);
373
+ } catch (e) {
374
+ log(`AI report failed: ${e.message}`);
375
+ }
376
+ }
377
+
378
+ // ── WRITE MASTER REPORT ─────────────────────────────
379
+ fs.writeFileSync(path.join(outputDir, 'forensics-report.json'), JSON.stringify(report, null, 2));
380
+ await page.close();
381
+ writeHistory(report, opts.output);
382
+
383
+ return { report, outputDir };
384
+ }
385
+
386
+ function getPhaseDetail(name, result) {
387
+ if (!result || result.error) return result?.error || '';
388
+ switch (name) {
389
+ case 'tech-stack': return Object.keys(result.detected || {}).filter(k => result.detected[k]).join(', ') || '';
390
+ case 'gsap-timeline': return `${result.tweens?.length || 0} tweens, ${result.scrollTriggers?.length || 0} STs`;
391
+ case 'react-fiber-walker': return `${result.componentCount || 0} components`;
392
+ case 'r3f-serializer': return result.canvasCount ? `${result.canvasCount} canvas(es)` : '';
393
+ case 'complexity-scorer': return result.score ? `score: ${result.score}/100` : '';
394
+ case 'sitemap-crawler': return result.sitemapFound ? `${result.data?.total || 0} URLs` : 'no sitemap (DOM fallback)';
395
+ case 'font-extractor': return result.webFonts?.length ? `${result.webFonts.length} fonts` : '';
396
+ case 'design-token-export': return result.cssVariables?.length ? `${result.cssVariables.length} vars` : '';
397
+ default: return '';
398
+ }
399
+ }
400
+
401
+ // ═══════════════════════════════════════════════════════
402
+ // ENTRY POINT
403
+ // ═══════════════════════════════════════════════════════
404
+ async function run() {
405
+ // ── INTERACTIVE MODE ─────────────────────────────────
406
+ if (opts.interactive && tuiModule) {
407
+ const { promptConfig, createPhaseRenderer, loadModules } = tuiModule;
408
+ const { chalk, ora, inquirer } = await loadModules();
409
+ const config = await promptConfig(inquirer, chalk);
410
+
411
+ // Merge TUI config into opts
412
+ Object.assign(opts, config);
413
+ targetUrl = config.url;
414
+
415
+ // Override opts with TUI selections
416
+ opts.output = config.output;
417
+
418
+ let puppeteer;
419
+ try { puppeteer = require('puppeteer'); }
420
+ catch { console.error('Puppeteer not installed.'); process.exit(1); }
421
+
422
+ const browser = await puppeteer.launch({
423
+ headless: opts.headless ? 'new' : false,
424
+ defaultViewport: { width: 1440, height: 900, deviceScaleFactor: 2 },
425
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-webgl', '--enable-webgl2', '--use-gl=desktop', '--enable-unsafe-webgpu'],
426
+ });
427
+
428
+ const renderer = createPhaseRenderer(chalk, ora);
429
+ const { report, outputDir } = await analyzeUrl(targetUrl, browser, false, renderer);
430
+
431
+ if (opts.compareUrl) {
432
+ const { report: cloneReport, outputDir: cloneDir } = await analyzeUrl(opts.compareUrl, browser, true, renderer);
433
+ const diff = calculateDiff(report, cloneReport);
434
+ fs.writeFileSync(path.join(outputDir, 'comparison-diff.json'), JSON.stringify(diff, null, 2));
435
+ renderer.log(`🏆 Fidelity Score: ${diff.fidelityScore.toFixed(1)}/100`);
436
+ }
437
+
438
+ renderer.complete(report, outputDir, chalk);
439
+ await browser.close();
440
+ return;
441
+ }
442
+
443
+ // ── CLI MODE ─────────────────────────────────────────
444
+ let puppeteer;
445
+ try { puppeteer = require('puppeteer'); }
446
+ catch { console.error('❌ Puppeteer not installed. Run: npm install puppeteer'); process.exit(1); }
447
+
448
+ console.log(`\n╔══════════════════════════════════╗`);
449
+ console.log(`║ webgl-forensics v3.1 — Running ║`);
450
+ console.log(`╚══════════════════════════════════╝\n`);
451
+
452
+ const browser = await puppeteer.launch({
453
+ headless: opts.headless ? 'new' : false,
454
+ defaultViewport: { width: 1440, height: 900, deviceScaleFactor: 2 },
455
+ args: [
456
+ '--no-sandbox', '--disable-setuid-sandbox',
457
+ '--enable-webgl', '--enable-webgl2', '--use-gl=desktop',
458
+ '--enable-features=WebGL2ComputeContext',
459
+ '--enable-unsafe-webgpu', '--enable-features=WebGPU',
460
+ ],
461
+ });
462
+
463
+ const { report: sourceReport, outputDir: sourceDir } = await analyzeUrl(targetUrl, browser, false);
464
+
465
+ if (opts.compareUrl) {
466
+ console.log(`\n⚖️ COMPARING WITH: ${opts.compareUrl}`);
467
+ const { report: cloneReport } = await analyzeUrl(opts.compareUrl, browser, true);
468
+ const diff = calculateDiff(sourceReport, cloneReport);
469
+ fs.writeFileSync(path.join(sourceDir, 'comparison-diff.json'), JSON.stringify(diff, null, 2));
470
+ console.log(`\n🏆 FIDELITY SCORE: ${diff.fidelityScore.toFixed(1)}/100`);
471
+ console.log(`✅ Tech Overlap: ${diff.techOverlap.join(', ') || 'None'}`);
472
+ console.log(`❌ Missing in clone: ${diff.missingInClone.join(', ') || 'None'}`);
473
+ }
474
+
475
+ console.log(`\n✅ Done! Output: ${sourceDir}`);
476
+ if (sourceReport.meta.scaffold) {
477
+ console.log(`🏗️ Scaffold: ${sourceReport.meta.scaffold.scaffoldDir} (${sourceReport.meta.scaffold.filesGenerated} files)`);
478
+ }
479
+
480
+ await browser.close();
481
+ }
482
+
483
+ run().catch(err => {
484
+ console.error('❌ Fatal:', err.message);
485
+ process.exit(1);
486
+ });