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.
- package/.github/workflows/forensics.yml +44 -0
- package/CLAUDE_CODE.md +49 -0
- package/MASTER_PROMPT.md +202 -0
- package/README.md +579 -0
- package/SKILL.md +1256 -0
- package/package.json +53 -0
- package/project summary.md +33 -0
- package/puppeteer-runner.js +486 -0
- package/scripts/00-tech-stack-detect.js +185 -0
- package/scripts/01-source-map-extractor.js +73 -0
- package/scripts/02-interaction-model.js +129 -0
- package/scripts/03-responsive-analysis.js +115 -0
- package/scripts/04-page-transitions.js +128 -0
- package/scripts/05-loading-sequence.js +124 -0
- package/scripts/06-audio-extraction.js +115 -0
- package/scripts/07-accessibility-reduced-motion.js +133 -0
- package/scripts/08-complexity-scorer.js +138 -0
- package/scripts/09-visual-diff-validator.js +113 -0
- package/scripts/10-webgpu-extractor.js +248 -0
- package/scripts/11-scroll-screenshot-grid.js +76 -0
- package/scripts/12-network-waterfall.js +151 -0
- package/scripts/13-react-fiber-walker.js +285 -0
- package/scripts/14-shader-hotpatch.js +349 -0
- package/scripts/15-multipage-crawler.js +221 -0
- package/scripts/16-gsap-timeline-recorder.js +335 -0
- package/scripts/17-r3f-fiber-serializer.js +316 -0
- package/scripts/18-font-extractor.js +290 -0
- package/scripts/19-design-token-export.js +85 -0
- package/scripts/20-timeline-visualizer.js +61 -0
- package/scripts/21-lighthouse-audit.js +35 -0
- package/scripts/22-sitemap-crawler.js +128 -0
- package/scripts/23-scaffold-generator.js +451 -0
- package/scripts/24-ai-reconstruction.js +226 -0
- package/scripts/25-shader-annotator.js +226 -0
- package/tui.js +196 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webgl-forensics — AI Reconstruction Report Generator (Script 24)
|
|
3
|
+
*
|
|
4
|
+
* Node.js module (NOT a browser script). Called by the runner after forensics
|
|
5
|
+
* completes. Pipes the forensics JSON to an AI (Claude or Gemini via API) and
|
|
6
|
+
* returns a human-readable "how to rebuild this site" guide.
|
|
7
|
+
*
|
|
8
|
+
* REQUIRES: ANTHROPIC_API_KEY or GEMINI_API_KEY in environment.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const https = require('https');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compress the large forensics report into a focused prompt payload.
|
|
15
|
+
* We cherry-pick the highest-signal data to stay under token limits.
|
|
16
|
+
*/
|
|
17
|
+
function buildPromptPayload(report) {
|
|
18
|
+
const phases = report?.meta?.phases || {};
|
|
19
|
+
|
|
20
|
+
const techStack = phases['tech-stack'] || {};
|
|
21
|
+
const gsap = phases['gsap-timeline'] || {};
|
|
22
|
+
const tokens = phases['design-token-export'] || {};
|
|
23
|
+
const fonts = phases['font-extractor'] || {};
|
|
24
|
+
const scroll = phases['interaction-model'] || {};
|
|
25
|
+
const r3f = phases['r3f-serializer'] || {};
|
|
26
|
+
const fiber = phases['react-fiber-walker'] || {};
|
|
27
|
+
const complexity = phases['complexity-scorer'] || {};
|
|
28
|
+
const loading = phases['loading-sequence'] || {};
|
|
29
|
+
const lighthouse = phases['lighthouse'] || {};
|
|
30
|
+
const transitions = phases['page-transitions'] || {};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
url: report?.meta?.url,
|
|
34
|
+
framework: techStack?.framework,
|
|
35
|
+
libraries: techStack?.libraries,
|
|
36
|
+
animations: techStack?.animations,
|
|
37
|
+
scroll: techStack?.scroll,
|
|
38
|
+
gsap: {
|
|
39
|
+
version: gsap?.version,
|
|
40
|
+
tweenCount: gsap?.tweens?.length,
|
|
41
|
+
scrollTriggerCount: gsap?.scrollTriggers?.length,
|
|
42
|
+
sampleTweens: (gsap?.tweens || []).slice(0, 5),
|
|
43
|
+
sampleSTs: (gsap?.scrollTriggers || []).slice(0, 5),
|
|
44
|
+
},
|
|
45
|
+
designTokens: {
|
|
46
|
+
colorCount: tokens?.colors?.length,
|
|
47
|
+
sampleColors: (tokens?.colors || []).slice(0, 10),
|
|
48
|
+
bodyFont: tokens?.bodyStyles?.fontFamily,
|
|
49
|
+
bodyBg: tokens?.bodyStyles?.backgroundColor,
|
|
50
|
+
},
|
|
51
|
+
fonts: (fonts?.webFonts || []).slice(0, 8),
|
|
52
|
+
r3fTree: r3f?.jsxTree ? r3f.jsxTree.substring(0, 1500) : null,
|
|
53
|
+
canvas: fiber?.canvasConfig,
|
|
54
|
+
complexity: complexity?.score,
|
|
55
|
+
complexityBreakdown: complexity?.breakdown,
|
|
56
|
+
lighthouse: {
|
|
57
|
+
performance: lighthouse?.categories?.performance?.score,
|
|
58
|
+
accessibility: lighthouse?.categories?.accessibility?.score,
|
|
59
|
+
},
|
|
60
|
+
loading: {
|
|
61
|
+
hasPreloader: loading?.hasPreloader,
|
|
62
|
+
preloaderSelector: loading?.preloaderSelector,
|
|
63
|
+
},
|
|
64
|
+
pageTransitions: transitions?.detected,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildPrompt(payload) {
|
|
69
|
+
return `You are a senior creative frontend engineer specializing in WebGL, Three.js, GSAP, and high-fidelity site reconstruction.
|
|
70
|
+
|
|
71
|
+
Below is a structured forensics report for the website: ${payload.url}
|
|
72
|
+
|
|
73
|
+
Your task: Write a concrete, actionable "Reconstruction Guide" that a developer can follow to rebuild this site from scratch. Be specific — reference actual values from the data.
|
|
74
|
+
|
|
75
|
+
## FORENSICS DATA:
|
|
76
|
+
${JSON.stringify(payload, null, 2)}
|
|
77
|
+
|
|
78
|
+
## OUTPUT STRUCTURE:
|
|
79
|
+
Return a detailed Markdown document with these sections:
|
|
80
|
+
|
|
81
|
+
### 1. Site Overview
|
|
82
|
+
- What this site is and who it's likely built for
|
|
83
|
+
- Stack summary (framework, rendering, animation system)
|
|
84
|
+
- Complexity rating and what drives it
|
|
85
|
+
|
|
86
|
+
### 2. Project Setup
|
|
87
|
+
- Exact \`npx create-next-app\` command or equivalent
|
|
88
|
+
- Dependencies to install with exact npm/pnpm commands
|
|
89
|
+
- File structure overview
|
|
90
|
+
|
|
91
|
+
### 3. Design System
|
|
92
|
+
- Color palette from tokens (actual hex values)
|
|
93
|
+
- Typography (font families, weights, sizes)
|
|
94
|
+
- How to set up the CSS variables
|
|
95
|
+
- Tailwind config snippet if applicable
|
|
96
|
+
|
|
97
|
+
### 4. Animation System
|
|
98
|
+
- GSAP setup code (version, plugins to register)
|
|
99
|
+
- How ScrollTrigger is used (pin sections, scrub, etc.)
|
|
100
|
+
- Sample tween reconstructions based on the data
|
|
101
|
+
- Lenis/smooth scroll config if detected
|
|
102
|
+
|
|
103
|
+
### 5. 3D / WebGL Layer
|
|
104
|
+
- Three.js / R3F scene setup
|
|
105
|
+
- Key geometries and materials detected
|
|
106
|
+
- Camera configuration
|
|
107
|
+
- Any shader patterns identified
|
|
108
|
+
|
|
109
|
+
### 6. Loading & Transitions
|
|
110
|
+
- Preloader implementation approach
|
|
111
|
+
- Page transition system
|
|
112
|
+
- Initial animation sequence
|
|
113
|
+
|
|
114
|
+
### 7. Critical Implementation Notes
|
|
115
|
+
- Gotchas, performance tips, order of operations
|
|
116
|
+
- What NOT to do when rebuilding this specific site
|
|
117
|
+
|
|
118
|
+
Be specific. Use code blocks. Reference the actual values from the JSON. Don't be generic.`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Call Claude API (Anthropic)
|
|
123
|
+
*/
|
|
124
|
+
async function callClaude(prompt, apiKey) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const body = JSON.stringify({
|
|
127
|
+
model: 'claude-opus-4-5',
|
|
128
|
+
max_tokens: 4096,
|
|
129
|
+
messages: [{ role: 'user', content: prompt }],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const req = https.request({
|
|
133
|
+
hostname: 'api.anthropic.com',
|
|
134
|
+
path: '/v1/messages',
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
'x-api-key': apiKey,
|
|
139
|
+
'anthropic-version': '2023-06-01',
|
|
140
|
+
'Content-Length': Buffer.byteLength(body),
|
|
141
|
+
},
|
|
142
|
+
}, (res) => {
|
|
143
|
+
let data = '';
|
|
144
|
+
res.on('data', chunk => data += chunk);
|
|
145
|
+
res.on('end', () => {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(data);
|
|
148
|
+
if (parsed.error) return reject(new Error(parsed.error.message));
|
|
149
|
+
resolve(parsed.content?.[0]?.text || '');
|
|
150
|
+
} catch (e) {
|
|
151
|
+
reject(e);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
req.on('error', reject);
|
|
157
|
+
req.write(body);
|
|
158
|
+
req.end();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Call Gemini API (Google)
|
|
164
|
+
*/
|
|
165
|
+
async function callGemini(prompt, apiKey) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const body = JSON.stringify({
|
|
168
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
169
|
+
generationConfig: { maxOutputTokens: 4096 },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const req = https.request({
|
|
173
|
+
hostname: 'generativelanguage.googleapis.com',
|
|
174
|
+
path: `/v1beta/models/gemini-1.5-pro:generateContent?key=${apiKey}`,
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: {
|
|
177
|
+
'Content-Type': 'application/json',
|
|
178
|
+
'Content-Length': Buffer.byteLength(body),
|
|
179
|
+
},
|
|
180
|
+
}, (res) => {
|
|
181
|
+
let data = '';
|
|
182
|
+
res.on('data', chunk => data += chunk);
|
|
183
|
+
res.on('end', () => {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(data);
|
|
186
|
+
if (parsed.error) return reject(new Error(parsed.error.message));
|
|
187
|
+
resolve(parsed.candidates?.[0]?.content?.parts?.[0]?.text || '');
|
|
188
|
+
} catch (e) {
|
|
189
|
+
reject(e);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
req.on('error', reject);
|
|
195
|
+
req.write(body);
|
|
196
|
+
req.end();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Main export — generate the AI reconstruction report.
|
|
202
|
+
* @param {Object} report - Full forensics report object
|
|
203
|
+
* @returns {Promise<string>} Markdown content of the reconstruction guide
|
|
204
|
+
*/
|
|
205
|
+
async function generateReconstructionReport(report) {
|
|
206
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
207
|
+
const geminiKey = process.env.GEMINI_API_KEY;
|
|
208
|
+
|
|
209
|
+
if (!anthropicKey && !geminiKey) {
|
|
210
|
+
return `# AI Reconstruction Report — Skipped\n\nNo API key found. Set \`ANTHROPIC_API_KEY\` or \`GEMINI_API_KEY\` to generate this report.\n\nRaw forensics data is saved in \`forensics-report.json\`.`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const payload = buildPromptPayload(report);
|
|
214
|
+
const prompt = buildPrompt(payload);
|
|
215
|
+
|
|
216
|
+
let markdown = '';
|
|
217
|
+
if (anthropicKey) {
|
|
218
|
+
markdown = await callClaude(prompt, anthropicKey);
|
|
219
|
+
} else if (geminiKey) {
|
|
220
|
+
markdown = await callGemini(prompt, geminiKey);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return markdown;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = generateReconstructionReport;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webgl-forensics — Shader GLSL Annotator (Script 25)
|
|
3
|
+
*
|
|
4
|
+
* Node.js module. Called by the runner when shader source code is found.
|
|
5
|
+
* Sends each shader's GLSL to an AI and returns a structured annotation:
|
|
6
|
+
* - What it does visually
|
|
7
|
+
* - The math pattern (Voronoi, FBM, SDF, etc.)
|
|
8
|
+
* - Key uniforms and what they control
|
|
9
|
+
* - A simplified "recipe" a dev can follow to recreate it
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const https = require('https');
|
|
13
|
+
|
|
14
|
+
const MATH_PATTERNS = [
|
|
15
|
+
{ pattern: /fbm|fBm|fractal.*brownian|octave/i, name: 'Fractional Brownian Motion (fBm)', desc: 'Layered noise for organic, cloud-like textures' },
|
|
16
|
+
{ pattern: /voronoi|worley/i, name: 'Voronoi / Worley Noise', desc: 'Cell-based patterns for geometric, cracked-surface effects' },
|
|
17
|
+
{ pattern: /sdf|signed.*distance|distanceField/i, name: 'Signed Distance Field (SDF)', desc: 'Math-driven shapes — clean edges, morphing, ray marching' },
|
|
18
|
+
{ pattern: /perlin|simplex/i, name: 'Perlin / Simplex Noise', desc: 'Smooth gradient noise for flowing, organic motion' },
|
|
19
|
+
{ pattern: /ray.*march|rayMarch|render.*sphere/i, name: 'Ray Marching', desc: 'Volumetric 3D rendering entirely in the fragment shader' },
|
|
20
|
+
{ pattern: /chromatic.*aberation|rgb.*split|rgbShift/i, name: 'Chromatic Aberration', desc: 'RGB channel splitting for lens distortion effect' },
|
|
21
|
+
{ pattern: /blur|gaussian|bokeh/i, name: 'Blur / Bokeh', desc: 'Sample-based blurring for depth of field or post-processing' },
|
|
22
|
+
{ pattern: /ripple|wave.*distort|distortion/i, name: 'Wave Distortion', desc: 'UV displacement for liquid or heat shimmer effects' },
|
|
23
|
+
{ pattern: /hsl|hsv|rgb2hsl|hue.*rotate/i, name: 'Color Space Manipulation', desc: 'HSL/HSV color shifting for palette effects' },
|
|
24
|
+
{ pattern: /dither|threshold.*pattern/i, name: 'Dithering', desc: 'Ordered or random pixel patterns for retro/lo-fi effects' },
|
|
25
|
+
{ pattern: /metaball|blob|merge.*sphere/i, name: 'Metaballs / Isosurface', desc: 'Organic blobs that merge and separate' },
|
|
26
|
+
{ pattern: /glitch|block.*displace|scanline/i, name: 'Glitch Effect', desc: 'Digital corruption, block displacement, scan lines' },
|
|
27
|
+
{ pattern: /particle|point.*sprite|billboard/i, name: 'Particle System', desc: 'Per-particle position/color animation in the shader' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect math patterns from GLSL source without AI (fast, offline)
|
|
32
|
+
*/
|
|
33
|
+
function detectPatternsLocally(glsl) {
|
|
34
|
+
const detected = [];
|
|
35
|
+
for (const { pattern, name, desc } of MATH_PATTERNS) {
|
|
36
|
+
if (pattern.test(glsl)) {
|
|
37
|
+
detected.push({ name, desc });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return detected;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract uniform declarations from GLSL
|
|
45
|
+
*/
|
|
46
|
+
function extractUniforms(glsl) {
|
|
47
|
+
const uniformRegex = /uniform\s+([\w]+)\s+([\w]+)\s*(?:=\s*[^;]+)?;/g;
|
|
48
|
+
const uniforms = [];
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = uniformRegex.exec(glsl)) !== null) {
|
|
51
|
+
uniforms.push({ type: match[1], name: match[2] });
|
|
52
|
+
}
|
|
53
|
+
return uniforms;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract varying declarations
|
|
58
|
+
*/
|
|
59
|
+
function extractVaryings(glsl) {
|
|
60
|
+
const varyingRegex = /varying\s+([\w]+)\s+([\w]+)\s*;/g;
|
|
61
|
+
const varyings = [];
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = varyingRegex.exec(glsl)) !== null) {
|
|
64
|
+
varyings.push({ type: match[1], name: match[2] });
|
|
65
|
+
}
|
|
66
|
+
return varyings;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a structured prompt for a single shader
|
|
71
|
+
*/
|
|
72
|
+
function buildShaderPrompt(shader) {
|
|
73
|
+
const maxGlslLength = 3000;
|
|
74
|
+
const truncated = shader.source.length > maxGlslLength;
|
|
75
|
+
const glslSnippet = shader.source.substring(0, maxGlslLength);
|
|
76
|
+
|
|
77
|
+
return `You are an expert WebGL/GLSL engineer and creative technologist.
|
|
78
|
+
|
|
79
|
+
Analyze this ${shader.type} shader and provide a detailed breakdown.
|
|
80
|
+
|
|
81
|
+
## SHADER TYPE: ${shader.type.toUpperCase()}
|
|
82
|
+
## UNIFORMS: ${JSON.stringify(shader.uniforms)}
|
|
83
|
+
## DETECTED PATTERN HINTS: ${shader.localPatterns.map(p => p.name).join(', ') || 'None auto-detected'}
|
|
84
|
+
## GLSL SOURCE${truncated ? ' (truncated)' : ''}:
|
|
85
|
+
\`\`\`glsl
|
|
86
|
+
${glslSnippet}
|
|
87
|
+
${truncated ? '\n// ... (truncated)' : ''}
|
|
88
|
+
\`\`\`
|
|
89
|
+
|
|
90
|
+
Respond with a JSON object with these exact keys:
|
|
91
|
+
{
|
|
92
|
+
"visualEffect": "1-2 sentence description of what this shader produces visually",
|
|
93
|
+
"mathPattern": "Primary mathematical technique used (e.g., Fractional Brownian Motion, SDF Ray Marching, Voronoi)",
|
|
94
|
+
"complexity": "simple|moderate|complex|advanced",
|
|
95
|
+
"uniforms": [
|
|
96
|
+
{ "name": "uniformName", "role": "What this uniform controls visually" }
|
|
97
|
+
],
|
|
98
|
+
"keyFunctions": ["List of notable custom functions and what they do"],
|
|
99
|
+
"recreationRecipe": "Step-by-step instructions to recreate this effect (4-8 steps)",
|
|
100
|
+
"similarExamples": ["Names of well-known effects/sites that use a similar technique"],
|
|
101
|
+
"performance": "Note about performance characteristics (vertex count, texture samples, etc.)"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Return ONLY valid JSON. No markdown fences.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function callAI(prompt) {
|
|
108
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
109
|
+
const geminiKey = process.env.GEMINI_API_KEY;
|
|
110
|
+
|
|
111
|
+
if (!anthropicKey && !geminiKey) return null;
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const useAnthropic = !!anthropicKey;
|
|
115
|
+
const apiKey = anthropicKey || geminiKey;
|
|
116
|
+
|
|
117
|
+
const body = useAnthropic
|
|
118
|
+
? JSON.stringify({
|
|
119
|
+
model: 'claude-haiku-4-5',
|
|
120
|
+
max_tokens: 1024,
|
|
121
|
+
messages: [{ role: 'user', content: prompt }],
|
|
122
|
+
})
|
|
123
|
+
: JSON.stringify({
|
|
124
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
125
|
+
generationConfig: { maxOutputTokens: 1024 },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const options = useAnthropic
|
|
129
|
+
? {
|
|
130
|
+
hostname: 'api.anthropic.com',
|
|
131
|
+
path: '/v1/messages',
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'x-api-key': apiKey,
|
|
136
|
+
'anthropic-version': '2023-06-01',
|
|
137
|
+
'Content-Length': Buffer.byteLength(body),
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
: {
|
|
141
|
+
hostname: 'generativelanguage.googleapis.com',
|
|
142
|
+
path: `/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`,
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: {
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
'Content-Length': Buffer.byteLength(body),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const req = https.request(options, (res) => {
|
|
151
|
+
let data = '';
|
|
152
|
+
res.on('data', chunk => data += chunk);
|
|
153
|
+
res.on('end', () => {
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(data);
|
|
156
|
+
const text = useAnthropic
|
|
157
|
+
? parsed.content?.[0]?.text
|
|
158
|
+
: parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
159
|
+
if (text) {
|
|
160
|
+
try { resolve(JSON.parse(text)); } catch { resolve({ raw: text }); }
|
|
161
|
+
} else {
|
|
162
|
+
resolve(null);
|
|
163
|
+
}
|
|
164
|
+
} catch { resolve(null); }
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
req.on('error', () => resolve(null));
|
|
169
|
+
req.write(body);
|
|
170
|
+
req.end();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Main export — annotate all shaders in the forensics report
|
|
176
|
+
* @param {Object} sheaderData - Output of shader-hotpatch phase
|
|
177
|
+
* @returns {Promise<Array>} Array of annotated shader objects
|
|
178
|
+
*/
|
|
179
|
+
async function annotateShaders(shaderData) {
|
|
180
|
+
const shaders = [];
|
|
181
|
+
|
|
182
|
+
// Collect shaders from report
|
|
183
|
+
const programs = shaderData?.interceptedPrograms || shaderData?.programs || [];
|
|
184
|
+
|
|
185
|
+
for (const prog of programs) {
|
|
186
|
+
const types = ['vertex', 'fragment'];
|
|
187
|
+
for (const type of types) {
|
|
188
|
+
const source = prog[`${type}Shader`] || prog[`${type}Source`];
|
|
189
|
+
if (!source || source.length < 50) continue;
|
|
190
|
+
|
|
191
|
+
const shader = {
|
|
192
|
+
id: prog.id || shaders.length,
|
|
193
|
+
type,
|
|
194
|
+
source,
|
|
195
|
+
uniforms: extractUniforms(source),
|
|
196
|
+
varyings: extractVaryings(source),
|
|
197
|
+
localPatterns: detectPatternsLocally(source),
|
|
198
|
+
lineCount: source.split('\n').length,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
shaders.push(shader);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (shaders.length === 0) {
|
|
206
|
+
return [{
|
|
207
|
+
note: 'No intercepted shader source found.',
|
|
208
|
+
hint: 'Ensure --no-headless is used and the site renders WebGL before extraction.',
|
|
209
|
+
}];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const hasApiKey = process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
|
|
213
|
+
|
|
214
|
+
if (hasApiKey) {
|
|
215
|
+
// Annotate each shader with AI (limit to first 5 to control API cost)
|
|
216
|
+
const toAnnotate = shaders.slice(0, 5);
|
|
217
|
+
for (const shader of toAnnotate) {
|
|
218
|
+
const prompt = buildShaderPrompt(shader);
|
|
219
|
+
shader.aiAnnotation = await callAI(prompt);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return shaders;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = annotateShaders;
|
package/tui.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* webgl-forensics — Interactive TUI
|
|
4
|
+
* Uses inquirer + chalk + ora for a rich terminal dashboard experience.
|
|
5
|
+
* Called when --interactive flag is passed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
// Dynamic imports for ESM-only packages (chalk v5, ora v8)
|
|
14
|
+
async function loadModules() {
|
|
15
|
+
const { default: chalk } = await import('chalk');
|
|
16
|
+
const { default: ora } = await import('ora');
|
|
17
|
+
const { default: inquirer } = await import('inquirer');
|
|
18
|
+
return { chalk, ora, inquirer };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PHASES = [
|
|
22
|
+
{ id: 'loading-sequence', label: '⚡ Loading Sequence', script: '05-loading-sequence.js' },
|
|
23
|
+
{ id: 'tech-stack', label: '🔍 Tech Stack Detection', script: '00-tech-stack-detect.js' },
|
|
24
|
+
{ id: 'source-maps', label: '🗺️ Source Maps', script: '01-source-map-extractor.js' },
|
|
25
|
+
{ id: 'sitemap-crawler', label: '🗂️ Sitemap Crawler', script: '22-sitemap-crawler.js' },
|
|
26
|
+
{ id: 'network-waterfall', label: '🌊 Network Waterfall', script: '12-network-waterfall.js' },
|
|
27
|
+
{ id: 'react-fiber-walker', label: '⚛️ React Fiber Walker', script: '13-react-fiber-walker.js' },
|
|
28
|
+
{ id: 'r3f-serializer', label: '🎲 R3F Serializer', script: '17-r3f-fiber-serializer.js' },
|
|
29
|
+
{ id: 'shader-hotpatch', label: '✨ Shader Hot-Patch', script: '14-shader-hotpatch.js' },
|
|
30
|
+
{ id: 'gsap-timeline', label: '🎬 GSAP Timeline', script: '16-gsap-timeline-recorder.js' },
|
|
31
|
+
{ id: 'interaction-model', label: '👆 Interaction Model', script: '02-interaction-model.js' },
|
|
32
|
+
{ id: 'page-transitions', label: '↔️ Page Transitions', script: '04-page-transitions.js' },
|
|
33
|
+
{ id: 'font-extractor', label: '🔤 Font Extractor', script: '18-font-extractor.js' },
|
|
34
|
+
{ id: 'design-token-export', label: '🎨 Design Tokens', script: '19-design-token-export.js' },
|
|
35
|
+
{ id: 'responsive-analysis', label: '📐 Responsive Analysis', script: '03-responsive-analysis.js' },
|
|
36
|
+
{ id: 'audio-extraction', label: '🔊 Audio Extraction', script: '06-audio-extraction.js' },
|
|
37
|
+
{ id: 'accessibility', label: '♿ Accessibility', script: '07-accessibility-reduced-motion.js' },
|
|
38
|
+
{ id: 'complexity-scorer', label: '📊 Complexity Scorer', script: '08-complexity-scorer.js' },
|
|
39
|
+
{ id: 'visual-diff', label: '🔬 Visual Diff', script: '09-visual-diff-validator.js' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function banner(chalk) {
|
|
43
|
+
return chalk.bold.cyan(`
|
|
44
|
+
╔═══════════════════════════════════════════════════════╗
|
|
45
|
+
║ ██╗ ██╗███████╗██████╗ ██████╗ ██╗ ║
|
|
46
|
+
║ ██║ ██║██╔════╝██╔══██╗██╔════╝ ██║ ║
|
|
47
|
+
║ ██║ █╗ ██║█████╗ ██████╔╝██║ ███╗██║ ║
|
|
48
|
+
║ ██║███╗██║██╔══╝ ██╔══██╗██║ ██║██║ ║
|
|
49
|
+
║ ╚███╔███╔╝███████╗██████╔╝╚██████╔╝███████╗ ║
|
|
50
|
+
║ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ║
|
|
51
|
+
║ ║
|
|
52
|
+
║ ${chalk.white('FORENSICS')} ${chalk.gray('v3.1')} ${chalk.green('— Universal Site Analyzer')} ║
|
|
53
|
+
╚═══════════════════════════════════════════════════════╝
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function promptConfig(inquirer, chalk) {
|
|
58
|
+
console.log(banner(chalk));
|
|
59
|
+
console.log(chalk.gray(' Interactive mode — configure your forensics run\n'));
|
|
60
|
+
|
|
61
|
+
const answers = await inquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: 'input',
|
|
64
|
+
name: 'url',
|
|
65
|
+
message: chalk.cyan('Target URL:'),
|
|
66
|
+
validate: (v) => v.startsWith('http') ? true : 'Must start with http/https',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'focus',
|
|
71
|
+
message: chalk.cyan('Analysis scope:'),
|
|
72
|
+
choices: [
|
|
73
|
+
{ name: '🌍 All phases (recommended)', value: 'all' },
|
|
74
|
+
{ name: '🎭 3D / WebGL only', value: '3d' },
|
|
75
|
+
{ name: '🎬 Animations only', value: 'animations' },
|
|
76
|
+
{ name: '📜 Scroll systems only', value: 'scroll' },
|
|
77
|
+
{ name: '🎨 Layout & design tokens', value: 'layout' },
|
|
78
|
+
],
|
|
79
|
+
default: 'all',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'checkbox',
|
|
83
|
+
name: 'features',
|
|
84
|
+
message: chalk.cyan('Enable features:'),
|
|
85
|
+
choices: [
|
|
86
|
+
{ name: '📸 Scroll screenshots', value: 'screenshots', checked: false },
|
|
87
|
+
{ name: '🗺️ Multi-page crawl (sitemap)', value: 'multipage', checked: true },
|
|
88
|
+
{ name: '⬇️ Download assets (GLB, fonts, HDR)', value: 'downloadAssets', checked: false },
|
|
89
|
+
{ name: '🎥 Record scroll video', value: 'record', checked: false },
|
|
90
|
+
{ name: '🏗️ Generate Next.js scaffold', value: 'scaffold', checked: true },
|
|
91
|
+
{ name: '🤖 AI reconstruction report (needs API key)', value: 'aiReport', checked: false },
|
|
92
|
+
{ name: '✨ AI shader annotation (needs API key)', value: 'annotateShaders', checked: false },
|
|
93
|
+
{ name: '🏆 Run Lighthouse audit', value: 'lighthouse', checked: false },
|
|
94
|
+
{ name: '📃 Generate HTML report', value: 'formatHtml', checked: false },
|
|
95
|
+
{ name: '📂 Save run to history', value: 'history', checked: false },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'list',
|
|
100
|
+
name: 'headless',
|
|
101
|
+
message: chalk.cyan('Browser mode:'),
|
|
102
|
+
choices: [
|
|
103
|
+
{ name: '🔮 Headless (faster)', value: true },
|
|
104
|
+
{ name: '👁️ Visible — required for some WebGL sites', value: false },
|
|
105
|
+
],
|
|
106
|
+
default: false,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: 'input',
|
|
110
|
+
name: 'compareUrl',
|
|
111
|
+
message: chalk.cyan('Compare URL (optional — leave blank to skip):'),
|
|
112
|
+
default: '',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'output',
|
|
117
|
+
message: chalk.cyan('Output directory:'),
|
|
118
|
+
default: './forensics-output',
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// Convert checkbox array to flags
|
|
123
|
+
const features = answers.features;
|
|
124
|
+
return {
|
|
125
|
+
url: answers.url,
|
|
126
|
+
focus: answers.focus,
|
|
127
|
+
headless: answers.headless,
|
|
128
|
+
compareUrl: answers.compareUrl || null,
|
|
129
|
+
output: answers.output,
|
|
130
|
+
screenshots: features.includes('screenshots'),
|
|
131
|
+
multipage: features.includes('multipage'),
|
|
132
|
+
downloadAssets: features.includes('downloadAssets'),
|
|
133
|
+
record: features.includes('record'),
|
|
134
|
+
scaffold: features.includes('scaffold'),
|
|
135
|
+
aiReport: features.includes('aiReport'),
|
|
136
|
+
annotateShaders: features.includes('annotateShaders'),
|
|
137
|
+
lighthouse: features.includes('lighthouse'),
|
|
138
|
+
formatHtml: features.includes('formatHtml'),
|
|
139
|
+
history: features.includes('history'),
|
|
140
|
+
dryRun: false,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Phase progress renderer
|
|
146
|
+
*/
|
|
147
|
+
function createPhaseRenderer(chalk, ora) {
|
|
148
|
+
const states = {};
|
|
149
|
+
let activeSpinner = null;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
start(phaseId, label) {
|
|
153
|
+
if (activeSpinner) activeSpinner.stop();
|
|
154
|
+
states[phaseId] = 'running';
|
|
155
|
+
activeSpinner = ora({
|
|
156
|
+
text: chalk.white(label),
|
|
157
|
+
color: 'cyan',
|
|
158
|
+
}).start();
|
|
159
|
+
},
|
|
160
|
+
succeed(phaseId, detail = '') {
|
|
161
|
+
if (activeSpinner) {
|
|
162
|
+
activeSpinner.succeed(chalk.green(`✓ `) + chalk.white(PHASES.find(p => p.id === phaseId)?.label || phaseId) + (detail ? chalk.gray(` — ${detail}`) : ''));
|
|
163
|
+
activeSpinner = null;
|
|
164
|
+
}
|
|
165
|
+
states[phaseId] = 'done';
|
|
166
|
+
},
|
|
167
|
+
fail(phaseId, err = '') {
|
|
168
|
+
if (activeSpinner) {
|
|
169
|
+
activeSpinner.fail(chalk.red(`✗ `) + chalk.white(PHASES.find(p => p.id === phaseId)?.label || phaseId) + chalk.gray(` — ${err}`));
|
|
170
|
+
activeSpinner = null;
|
|
171
|
+
}
|
|
172
|
+
states[phaseId] = 'failed';
|
|
173
|
+
},
|
|
174
|
+
log(msg) {
|
|
175
|
+
if (activeSpinner) activeSpinner.stop();
|
|
176
|
+
console.log(chalk.gray(` ${msg}`));
|
|
177
|
+
},
|
|
178
|
+
complete(report, outputDir, chalk) {
|
|
179
|
+
const complexity = report?.meta?.phases?.['complexity-scorer']?.score || 0;
|
|
180
|
+
const techStack = report?.meta?.phases?.['tech-stack'];
|
|
181
|
+
|
|
182
|
+
console.log('\n' + chalk.bold.green('═'.repeat(56)));
|
|
183
|
+
console.log(chalk.bold.green(' ✅ FORENSICS COMPLETE'));
|
|
184
|
+
console.log(chalk.bold.green('═'.repeat(56)));
|
|
185
|
+
console.log(chalk.cyan(` URL: `) + chalk.white(report?.meta?.url));
|
|
186
|
+
console.log(chalk.cyan(` Output: `) + chalk.white(outputDir));
|
|
187
|
+
console.log(chalk.cyan(` Complexity: `) + chalk.yellow(`${complexity}/100`));
|
|
188
|
+
if (techStack?.framework?.name) {
|
|
189
|
+
console.log(chalk.cyan(` Framework: `) + chalk.white(techStack.framework.name));
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.bold.green('═'.repeat(56)) + '\n');
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { promptConfig, createPhaseRenderer, loadModules, PHASES };
|