@trueclone/cli 0.2.0 → 0.4.0-rc.1

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.
@@ -0,0 +1,116 @@
1
+ // The reference's own code, as evidence for the agent (never copied into the clone): every script the page ran, saved
2
+ // as shipped and as a readable copy, ranked by how much of it actually ran while the page loaded and was scrolled
3
+ // through (V8 precise coverage), with every element's event listeners pointing at file:line:col. This is what lets an
4
+ // agent port a scene's algorithm (a grid computed from the viewport, a spring, a scroll scene) instead of copying the
5
+ // numbers it produced at one screen size. Read through Chrome's DevTools protocol; no extra tool needed.
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { createRequire } from 'node:module';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const TRACKING = /google-analytics|googletagmanager|gtag|doubleclick|facebook\.(com|net)|fbevents|hotjar|segment\.(io|com)|plausible|cloudflareinsights|clarity\.ms|sentry|datadog|mixpanel|amplitude|posthog|intercom|hubspot|hs-scripts|linkedin|licdn|tiktok|twitter|ads-twitter|redditstatic|marketo|munchkin|challenges\.cloudflare|recaptcha|hcaptcha|turnstile|cdn-cgi\/rum|debugbear/i;
12
+
13
+ // Before navigation: start collecting. Returns finish(), to call once the page has loaded and been scrolled through.
14
+ export async function startScriptEvidence(page, pageUrl) {
15
+ const cdp = await page.context().newCDPSession(page);
16
+ const scripts = new Map(); // scriptId -> { url, inline }
17
+ cdp.on('Debugger.scriptParsed', (e) => {
18
+ if (!e.executionContextAuxData?.isDefault) return; // the page's own world, not tooling
19
+ if (!e.url) return; // eval'd code
20
+ scripts.set(e.scriptId, { url: e.url, inline: e.url.split('#')[0] === pageUrl.split('#')[0], start: e.startLine });
21
+ });
22
+ await cdp.send('Debugger.enable');
23
+ await cdp.send('Profiler.enable');
24
+ await cdp.send('Profiler.startPreciseCoverage', { callCount: true, detailed: false });
25
+
26
+ return async function finish(outDir) {
27
+ const { result: coverage } = await cdp.send('Profiler.takePreciseCoverage');
28
+ await cdp.send('Profiler.stopPreciseCoverage').catch(() => {});
29
+ // listeners anywhere in the document, with the element they sit on
30
+ let listeners = [];
31
+ try {
32
+ const { result } = await cdp.send('Runtime.evaluate', { expression: 'document', objectGroup: 'tc' });
33
+ ({ listeners } = await cdp.send('DOMDebugger.getEventListeners', { objectId: result.objectId, depth: -1, pierce: true }));
34
+ } catch { /* no listeners readable */ }
35
+ const labels = new Map();
36
+ const labelOf = async (backendNodeId) => {
37
+ if (!backendNodeId) return 'window';
38
+ if (labels.has(backendNodeId)) return labels.get(backendNodeId);
39
+ let label = '?';
40
+ try {
41
+ const { node } = await cdp.send('DOM.describeNode', { backendNodeId });
42
+ const attrs = {};
43
+ for (let i = 0; i < (node.attributes || []).length; i += 2) attrs[node.attributes[i]] = node.attributes[i + 1];
44
+ label = node.nodeName === '#document' ? 'document' : `${node.localName}${attrs.id ? `#${attrs.id}` : ''}${attrs.class ? `.${attrs.class.trim().split(/\s+/).slice(0, 2).join('.')}` : ''}`;
45
+ } catch { /* detached */ }
46
+ labels.set(backendNodeId, label);
47
+ return label;
48
+ };
49
+
50
+ await fs.rm(outDir, { recursive: true, force: true });
51
+ await fs.mkdir(outDir, { recursive: true });
52
+ const beautify = require('js-beautify').js;
53
+ const files = new Map(); // scriptId -> record
54
+ let n = 0;
55
+ for (const [scriptId, s] of scripts) {
56
+ let source = '';
57
+ try { ({ scriptSource: source } = await cdp.send('Debugger.getScriptSource', { scriptId })); } catch { continue; }
58
+ if (!source || source.length < 40) continue;
59
+ const base = s.inline ? `inline-${++n}` : `${String(++n).padStart(2, '0')}-${(new URL(s.url).pathname.split('/').pop() || 'script').replace(/[^a-z0-9._-]/gi, '_').replace(/\.m?js$/i, '').slice(-60)}`;
60
+ const file = `${base}.js`;
61
+ await fs.writeFile(path.join(outDir, file), source);
62
+ // a readable copy when the code is minified (long lines); line numbers in INDEX.md refer to the original
63
+ const lines = source.split('\n');
64
+ const minified = source.length / Math.max(1, lines.length) > 200;
65
+ let pretty = null;
66
+ if (minified && source.length < 3_000_000) {
67
+ try { pretty = `${base}.pretty.js`; await fs.writeFile(path.join(outDir, pretty), beautify(source, { indent_size: 2, max_preserve_newlines: 1 })); } catch { pretty = null; }
68
+ }
69
+ const cov = coverage.find((c) => c.scriptId === scriptId);
70
+ const fns = cov?.functions || [];
71
+ const ran = fns.filter((f) => f.ranges?.[0]?.count > 0);
72
+ files.set(scriptId, { file, pretty, url: s.inline ? `${s.url} (inline)` : s.url, bytes: source.length, functions: fns.length, functionsRan: ran.length, ranNames: [...new Set(ran.map((f) => f.functionName).filter(Boolean))].slice(0, 14), tracking: TRACKING.test(s.url) && !s.inline, source, start: s.start });
73
+ }
74
+
75
+ // listeners grouped by handler location
76
+ const groups = new Map();
77
+ for (const l of listeners) {
78
+ const f = files.get(l.scriptId);
79
+ const key = `${l.type}|${l.scriptId}|${l.lineNumber}|${l.columnNumber}`;
80
+ if (!groups.has(key)) groups.set(key, { type: l.type, file: f?.file || null, url: f?.url || null, line: l.lineNumber + 1, column: l.columnNumber, elements: [] });
81
+ const g = groups.get(key);
82
+ if (g.elements.length < 6) g.elements.push(await labelOf(l.backendNodeId));
83
+ else g.more = (g.more || 0) + 1;
84
+ }
85
+ const excerpt = (f, line, col) => {
86
+ const text = f?.source.split('\n')[line - 1 - (f.inline ? 0 : 0)] || '';
87
+ return text.length > 160 ? text.slice(Math.max(0, col - 40), col + 140) : text.trim();
88
+ };
89
+ const ranked = [...files.values()].sort((a, b) => b.functionsRan - a.functionsRan);
90
+ const listenerRows = [...groups.values()].filter((g) => g.file && !files.get([...files.entries()].find(([, v]) => v.file === g.file)?.[0])?.tracking)
91
+ .sort((a, b) => (a.file || '').localeCompare(b.file || '') || a.line - b.line);
92
+ const md = [
93
+ `# The reference's scripts (evidence, never copied)`,
94
+ '',
95
+ `Saved as the page shipped them (\`NN-name.js\`) and, when minified, as a readable copy (\`.pretty.js\`). Ranked by how much of each ran while the page loaded and was scrolled through. Read the algorithm, then write your own code: a value a script computes from the screen size must be computed again, not copied.`,
96
+ '',
97
+ '| file | ran | functions | size | from | names that ran |',
98
+ '|---|---|---|---|---|---|',
99
+ ...ranked.map((f) => `| [${f.file}](${f.file})${f.pretty ? ` ([readable](${f.pretty}))` : ''} | ${f.functionsRan} | ${f.functions} | ${Math.round(f.bytes / 1024)} KB | ${f.tracking ? 'tracking (never copy)' : f.url.slice(0, 90)} | ${f.ranNames.join(', ')} |`),
100
+ '',
101
+ '## Event listeners, by handler',
102
+ '',
103
+ 'Line and column refer to the original file. Elements that share one handler are listed together; a handler on `document` or `window` usually serves every matching element below it (delegation).',
104
+ '',
105
+ '| event | handler | on | code at the handler |',
106
+ '|---|---|---|---|',
107
+ ...listenerRows.map((g) => { const f = [...files.values()].find((x) => x.file === g.file); return `| ${g.type} | ${g.file}:${g.line}:${g.column} | ${g.elements.join(', ')}${g.more ? ` +${g.more}` : ''} | \`${excerpt(f, g.line, g.column).replace(/\|/g, '\\|').replace(/`/g, "'")}\` |`; }),
108
+ '',
109
+ ].join('\n');
110
+ await fs.writeFile(path.join(outDir, 'INDEX.md'), md);
111
+ await fs.writeFile(path.join(outDir, 'scripts.json'), JSON.stringify({ scripts: ranked.map(({ source, ...r }) => r), listeners: listenerRows }, null, 2));
112
+ await cdp.send('Debugger.disable').catch(() => {});
113
+ await cdp.detach().catch(() => {});
114
+ return { scripts: ranked.length, ran: ranked.filter((f) => f.functionsRan).length, listeners: listenerRows.length };
115
+ };
116
+ }