@trueclone/cli 0.0.0-stage → 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ayush Pratap Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
1
+ # trueclone
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **A working twin of any website, from one URL, with scores that prove it.**
4
+
5
+ Give your coding agent a URL. trueclone captures the site, extracts its design system, guides the rebuild and measures the result against the original until it matches: layout, scroll states, motion curves, interactions, hover effects and computed styles, at phone, tablet and desktop widths. You get the clone as HTML and as a Next.js app, plus the design system as `DESIGN.md` and a shadcn `globals.css`.
6
+
7
+ ```bash
8
+ npx @trueclone/cli init # once per project: installs trueclone, the skill for your agent, REA as an MCP server
9
+ # then tell Claude Code, Codex or Cursor:
10
+ # "clone https://example.com with trueclone"
11
+ ```
12
+
13
+ ## What you get from one URL
14
+
15
+ | Output | What it is |
16
+ |---|---|
17
+ | `clone/` | The site in plain HTML, CSS and JS, interactions included (sliders, tabs, menus, canvas demos, scroll motion) |
18
+ | `next/` | The same site as a Next.js + Tailwind v4 + shadcn app, one component per section, scored against the original too |
19
+ | `DESIGN.md` | The design system in [Google's design.md format](https://github.com/google-labs-code/design.md), linted |
20
+ | `globals.css` | A shadcn theme (Tailwind v4, oklch) generated from the same tokens |
21
+ | `motion.md` | The motion recipe: every ScrollTrigger, tween, reveal and loop with its trigger, range, easing and timing |
22
+ | `REPORT.md` | Every score against its target, what to fix first, and what was not copied (server logic, tracking, each API call) |
23
+
24
+ ## Proof
25
+
26
+ Two benchmarks with very different stacks, scored by the tool itself:
27
+
28
+ **[rea.tools](https://rea.tools/)**: static docs with a canvas game and interactive demos, exact mode (copied with the owner's permission).
29
+
30
+ | Check | Desktop | Tablet | Mobile |
31
+ |---|---|---|---|
32
+ | Layout, real pixels | 100% | 100% | 100% |
33
+ | Scroll states | 100% | 100% | 99.99% |
34
+ | Computed styles (≈280 points per width) | 100% | 100% | 100% |
35
+ | Hover effects | 100% | | |
36
+ | Interactions (copy, tabs, calculator, dinosaur game, language menu) | 5/5 | | |
37
+
38
+ **A Webflow motorsport portfolio** built with GSAP, ScrollTrigger, SplitText, Lenis, Swiper and three.js (97 scroll triggers), template mode (own placeholder content):
39
+
40
+ | Check | Desktop | Tablet | Mobile |
41
+ |---|---|---|---|
42
+ | Layout, structure | 99.6% | 99.7% | 96.8% |
43
+ | Scroll states | 97.1% | 96.7% | 96.5% |
44
+ | Motion curves | 96.1% | 98.2% | 100% |
45
+ | Computed styles | 94% | 88.9% | 81.8% |
46
+ | Hover effects | 100% | | |
47
+ | Interactions (drag sliders, drag gallery, cursor follow, menu, skip) | 5/5 | | |
48
+
49
+ Not perfect yet, and the report says so: the computed-style check is below its 95% target (placeholder text in highlighted phrases, a scroll-linked colour fade, mobile spacing), and so is mobile layout. The generated Next.js app scores within half a point of the HTML clone on every check. A full scoring run takes 1 to 9 minutes.
50
+
51
+ ## How it works
52
+
53
+ ```
54
+ URL ─► capture ─► understand ─► design system ─► build ─► score ─► report
55
+ 3 widths motion DESIGN.md your 7 checks REPORT.md
56
+ HAR, HTML inventory globals.css agent in parallel
57
+ assets + REA tokens.css ▲ │
58
+ └─────┘ until every target passes
59
+ ```
60
+
61
+ 1. **Capture** (`trueclone capture`): full-page screenshots at 390, 768 and 1440px, sections, rendered HTML, assets, every network request.
62
+ 2. **Understand** (`trueclone motion`, REA): reads the live animation runtime (GSAP timelines, ScrollTriggers, Lenis and Swiper settings, CSS animations, cursor-driven tweens) and, through REA, bundles, source maps, event listeners and API endpoints.
63
+ 3. **Design system** (`trueclone design`): one canonical `design-tokens.json`, rendered to DESIGN.md, globals.css and the clone's tokens.
64
+ 4. **Build**: your agent writes the clone, guided by the skill. `trueclone rules` shows the original's CSS for any element (like DevTools), `trueclone textshape` gives placeholder text the original's line breaks, `trueclone seed` starts an exact clone from a script-free mirror.
65
+ 5. **Score** (`trueclone score`): layout pixel diff per section, scroll states replayed, motion curves sampled, interaction scenarios with change assertions, every hover, computed styles at matching points. Reference measurements are cached, so only the clone is re-measured.
66
+ 6. **Report**: `REPORT.md` with the verdict, what to fix first, and what was not copied.
67
+
68
+ ## Why not just REA?
69
+
70
+ trueclone **bundles** [REA](https://github.com/morluto/rea) (Reverse Engineer Anything) and uses it, but works without it too.
71
+
72
+ | | REA on its own | trueclone |
73
+ |---|---|---|
74
+ | What it is | A microscope: typed tools that let your agent look inside apps, binaries and pages | A factory with quality control: capture, design system, build guide, scores, report |
75
+ | Capture | Your Chrome over CDP, one page state | 3 widths, every scroll state, motion runtime, assets, network |
76
+ | Knows when the clone is right | No: the agent judges by eye | Yes: 7 measured checks with targets |
77
+ | Tells you what's wrong | No | "Footer column 2 is at x 189, the original's at 732", plus the original's CSS rule |
78
+ | Outputs | Whatever the agent writes | HTML clone, Next.js app, DESIGN.md, globals.css, motion recipe, REPORT.md |
79
+
80
+ REA answers *how does this work?* trueclone answers *is my copy identical, and where isn't it?* For the hard parts (a bundled physics rule, a shader, the API a component calls) the skill sends your agent to REA.
81
+
82
+ ## Modes and guardrails
83
+
84
+ | Mode | For | Content |
85
+ |---|---|---|
86
+ | `template` (default) | Any site: learning, layout study, a starting template | Structure, motion and behaviour of the original; your own text, images and brand |
87
+ | `exact` | Sites you own or are authorised to copy, such as a Webflow, Framer or WordPress site moving to Next.js | The site's own text and media |
88
+
89
+ - Exact mode refuses to run without `--permission "<who authorised it>"` and records it in the capture and the report.
90
+ - No bot-check, CAPTCHA, login or paywall bypass. Tracking and analytics are never copied.
91
+ - Clones are `noindex`, forms submit nowhere, and everything stays in your local `sites/` folder.
92
+
93
+ ## Commands
94
+
95
+ ```bash
96
+ npx @trueclone/cli init # set up a project for your agent (then `npx trueclone ...`)
97
+ npx trueclone doctor # check Node, Chrome and REA
98
+ npx trueclone <url> [name] # capture + design-system draft
99
+ npx trueclone capture <url> [name] [--mode exact --permission "<owner>"]
100
+ npx trueclone motion <name> # motion inventory
101
+ npx trueclone design <name> # DESIGN.md + globals.css
102
+ npx trueclone seed <name> # exact mode: start clone/ from the mirror
103
+ npx trueclone rules <name> "<selector>" # the original's CSS rules for an element
104
+ npx trueclone textshape <name> --write # template mode: placeholder text with the original's line breaks
105
+ npx trueclone score <name> [--quick] # every check, then REPORT.md
106
+ npx trueclone next <name> --verify # Next.js + Tailwind + shadcn app, scored
107
+ npx trueclone rea <args> # bundled REA
108
+ npx trueclone help # every step
109
+ ```
110
+
111
+ ## What it can't do
112
+
113
+ - **Server-side behaviour** is invisible from outside: databases, accounts, payments, search. trueclone rebuilds the client and lists every API call the original makes in the report.
114
+ - A live site is never pixel-identical twice: rotating content, A/B tests and font hinting vary.
115
+ - One page at a time today. Multi-page crawling with shared components is next.
116
+ - Developed and tested on Windows 11 with Chrome. macOS and Linux should work but aren't tested yet.
117
+
118
+ ## Requirements
119
+
120
+ Node 22.19+, 24.11+ or 26+ (the same as REA), and Google Chrome for captures (it plays the H.264/HLS video many sites use; Playwright's Chromium is the fallback). A coding agent: Claude Code, Codex, Cursor or any agent that reads `AGENTS.md`.
121
+
122
+ ## Roadmap
123
+
124
+ - **Whole sites:** crawl every route, shared header and footer as components.
125
+ - **Data layer:** captured API responses turned into mock routes and fixtures, so the clone works offline.
126
+ - **Benchmarks:** React with Framer Motion, a Next.js site with source maps, a data-driven app; each scored with and without REA.
127
+ - **A free web page:** paste a URL, get its DESIGN.md and globals.css.
128
+
129
+ ## Credits
130
+
131
+ [REA](https://github.com/morluto/rea) by morluto (MIT), [Playwright](https://playwright.dev), [pixelmatch](https://github.com/mapbox/pixelmatch), [@google/design.md](https://github.com/google-labs-code/design.md), [parse5](https://github.com/inikulin/parse5), and the [shadcn](https://ui.shadcn.com) theme format.
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ // trueclone CLI: one entry point for every step. Steps run in the current folder: everything about a site lives
3
+ // in ./sites/<name>/ (reference capture, clone, Next.js app, design system, scores).
4
+ //
5
+ // npx @trueclone/cli init set up this folder for a coding agent (skill, AGENTS.md, REA MCP, .gitignore) and
6
+ // install trueclone as a dev dependency, so `npx trueclone <step>` works from then on
7
+ // trueclone <url> [name] capture a site (template mode) and draft its design system: the agent's first step
8
+ // trueclone <step> <name> [...] run one step (see `trueclone help`)
9
+ // trueclone rea <args> REA (Reverse Engineer Anything), bundled: `trueclone rea mcp`, `trueclone rea doctor`
10
+ import { spawn } from 'node:child_process';
11
+ import fs from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { createRequire } from 'node:module';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const PKG = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const SCRIPTS = path.join(PKG, 'scripts');
18
+ const require = createRequire(import.meta.url);
19
+ const { name, version } = JSON.parse(await fs.readFile(path.join(PKG, 'package.json'), 'utf8'));
20
+
21
+ const STEPS = {
22
+ capture: ['capture.mjs', 'capture a live page: screenshots at 390/768/1440, sections, tokens, rendered HTML, assets'],
23
+ seed: ['seed.mjs', 'exact mode: start clone/ from the script-free mirror (markup, text, styles, media)'],
24
+ design: ['design.mjs', 'design-tokens.json -> DESIGN.md (linted), shadcn globals.css, clone/tokens.css'],
25
+ motion: ['motion.mjs', 'motion inventory from the live runtime (GSAP/ScrollTrigger, Lenis, Swiper, CSS animations)'],
26
+ states: ['states.mjs', 'live scroll states; --clone scores the rebuild step by step'],
27
+ curves: ['curves.mjs', 'motion score: sampled property curves, reference vs clone (motion-map.json)'],
28
+ interact: ['interact.mjs', 'interaction tests: click/hover/drag/key scenarios on both sides (interactions.json)'],
29
+ detail: ['detail.mjs', 'style diff at matching points: font, size, weight, line height, colour, background, radius'],
30
+ hovers: ['hovers.mjs', 'hover every interactive element on both sides and compare what changes'],
31
+ rules: ['rules.mjs', 'every CSS rule matching an element on the reference (+ ::before/::after, @media)'],
32
+ textshape: ['textshape.mjs', 'template mode: placeholder words with the reference word widths, so lines break the same'],
33
+ diff: ['diff.mjs', 'full-page pixel diff per viewport and section (--structure masks text and media)'],
34
+ spec: ['spec.mjs', 'measured boxes and styles of the live page (--clone for the rebuild)'],
35
+ tiles: ['tiles.mjs', 'cut a tall screenshot into readable tiles'],
36
+ next: ['next.mjs', 'Next.js + Tailwind v4 + shadcn app from the verified clone; --verify scores it'],
37
+ score: ['score.mjs', 'every score in parallel groups, time limit per step, writes REPORT.md'],
38
+ report: ['report.mjs', 'REPORT.md from the latest scores: every number, every gap, what cannot be copied'],
39
+ };
40
+
41
+ const run = (file, args) => new Promise((resolve) => {
42
+ const child = spawn(process.execPath, [file, ...args], { stdio: 'inherit' });
43
+ child.on('close', (code) => resolve(code ?? 1));
44
+ });
45
+
46
+ function help() {
47
+ console.log(`trueclone ${version}: a working twin of a website, proven by scores
48
+
49
+ usage
50
+ npx @trueclone/cli init set up this folder for your coding agent (installs trueclone)
51
+ trueclone <url> [name] capture + design-system draft (template mode)
52
+ trueclone <step> <name> [options] run one step
53
+ trueclone rea <args> bundled REA (Reverse Engineer Anything)
54
+ trueclone doctor check Node, browsers and REA
55
+
56
+ steps
57
+ ${Object.entries(STEPS).map(([k, [, d]]) => ` ${k.padEnd(10)} ${d}`).join('\n')}
58
+
59
+ Then ask your agent: "clone <url> with trueclone". The skill walks it through capture, build, scores and report.`);
60
+ }
61
+
62
+ async function exists(file) { return fs.stat(file).then(() => true, () => false); }
63
+
64
+ async function init() {
65
+ const cwd = process.cwd();
66
+ // 1. the agent skill (Claude Code reads .claude/skills; other agents read AGENTS.md, which points at the same file)
67
+ const skillDir = path.join(cwd, '.claude', 'skills', 'trueclone');
68
+ await fs.mkdir(skillDir, { recursive: true });
69
+ await fs.copyFile(path.join(PKG, 'skill', 'SKILL.md'), path.join(skillDir, 'SKILL.md'));
70
+ // 2. AGENTS.md block for Codex, Cursor and other agents
71
+ const agentsFile = path.join(cwd, 'AGENTS.md');
72
+ const block = `<!-- trueclone:start -->\n## Cloning websites with trueclone\n\nTo clone or rebuild a website, read \`.claude/skills/trueclone/SKILL.md\` first and follow it step by step. Run every step with \`npx trueclone <step> <name>\` and finish only when \`npx trueclone score <name>\` passes the targets in the skill; \`sites/<name>/REPORT.md\` is the deliverable.\n<!-- trueclone:end -->\n`;
73
+ let agents = (await exists(agentsFile)) ? await fs.readFile(agentsFile, 'utf8') : '';
74
+ agents = agents.includes('<!-- trueclone:start -->') ? agents.replace(/<!-- trueclone:start -->[\s\S]*?<!-- trueclone:end -->\n?/, block) : `${agents}${agents && !agents.endsWith('\n\n') ? '\n' : ''}${block}`;
75
+ await fs.writeFile(agentsFile, agents);
76
+ // 3. REA as an MCP server for agents that speak MCP (Claude Code reads .mcp.json)
77
+ const mcpFile = path.join(cwd, '.mcp.json');
78
+ const mcp = (await exists(mcpFile)) ? JSON.parse(await fs.readFile(mcpFile, 'utf8')) : {};
79
+ mcp.mcpServers = { ...(mcp.mcpServers || {}), rea: { command: 'npx', args: ['-y', `${name}@${version}`, 'rea', 'mcp'] } };
80
+ await fs.writeFile(mcpFile, `${JSON.stringify(mcp, null, 2)}\n`);
81
+ // 4. captures, clones and browser profiles stay local
82
+ const ignoreFile = path.join(cwd, '.gitignore');
83
+ let ignore = (await exists(ignoreFile)) ? await fs.readFile(ignoreFile, 'utf8') : '';
84
+ const want = ['sites/', '.chrome-profile/', '*.har'].filter((line) => !ignore.split(/\r?\n/).includes(line));
85
+ if (want.length) await fs.writeFile(ignoreFile, `${ignore}${ignore && !ignore.endsWith('\n') ? '\n' : ''}${want.join('\n')}\n`);
86
+ // 5. the package itself as a dev dependency, pinned: afterwards `npx trueclone <step>` runs this version, offline
87
+ let installed = 'skipped (--no-install)';
88
+ if (!process.argv.includes('--no-install')) {
89
+ if (!(await exists(path.join(cwd, 'package.json')))) await fs.writeFile(path.join(cwd, 'package.json'), `${JSON.stringify({ name: path.basename(cwd).toLowerCase().replace(/[^a-z0-9-]+/g, '-'), private: true }, null, 2)}\n`);
90
+ const code = await new Promise((resolve) => {
91
+ const child = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '--save-dev', '--save-exact', `${name}@${version}`], { stdio: 'inherit', shell: process.platform === 'win32' });
92
+ child.on('close', (c) => resolve(c ?? 1));
93
+ });
94
+ installed = code === 0 ? `${name}@${version} (dev dependency: npx trueclone <step> now runs it)` : `failed: run npm install -D ${name}@${version} yourself`;
95
+ }
96
+ console.log(`trueclone set up in ${cwd}
97
+ .claude/skills/trueclone/SKILL.md the workflow your agent follows
98
+ AGENTS.md points Codex, Cursor and others at the skill
99
+ .mcp.json REA as an MCP server
100
+ .gitignore sites/ stays local
101
+ package.json ${installed}
102
+ next: npx trueclone doctor, then ask your agent "clone <url> with trueclone"`);
103
+ }
104
+
105
+ async function doctor() {
106
+ let ok = true;
107
+ const [major, minor] = process.versions.node.split('.').map(Number);
108
+ const nodeOk = (major === 22 && minor >= 19) || (major === 24 && minor >= 11) || major >= 26;
109
+ console.log(`${nodeOk ? 'ok ' : 'FAIL'} Node ${process.versions.node} (needs 22.19+, 24.11+ or 26+)`);
110
+ ok &&= nodeOk;
111
+ const { chromium } = await import('playwright');
112
+ for (const [label, opts] of [['Chrome (captures; plays H.264/HLS video)', { channel: 'chrome' }], ['Playwright Chromium (fallback)', {}]]) {
113
+ try { const b = await chromium.launch(opts); await b.close(); console.log(`ok ${label}`); }
114
+ catch { console.log(`warn ${label} not found${opts.channel ? ': install Google Chrome' : ': npx playwright install chromium'}`); }
115
+ }
116
+ try { require.resolve('rea-agents/package.json'); console.log('ok REA bundled (trueclone rea doctor checks its decompilers)'); }
117
+ catch { console.log('warn REA not installed'); ok = false; }
118
+ process.exitCode = ok ? 0 : 1;
119
+ }
120
+
121
+ const [cmd, ...rest] = process.argv.slice(2);
122
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') help();
123
+ else if (cmd === '--version' || cmd === '-v') console.log(version);
124
+ else if (cmd === 'init') await init();
125
+ else if (cmd === 'doctor') await doctor();
126
+ else if (cmd === 'rea') process.exitCode = await run(path.join(path.dirname(require.resolve('rea-agents/package.json')), 'scripts', 'rea.mjs'), rest);
127
+ else if (/^https?:\/\//.test(cmd)) {
128
+ // the first step for any site: capture it (template mode) and draft its design system
129
+ const name = rest.find((a) => !a.startsWith('--')) || new URL(cmd).hostname.replace(/^www\./, '').replace(/\.[a-z]+$/, '').replace(/[^a-z0-9]+/gi, '-');
130
+ let code = await run(path.join(SCRIPTS, 'capture.mjs'), [cmd, name, ...rest.filter((a) => a !== name)]);
131
+ if (code === 0) code = await run(path.join(SCRIPTS, 'design.mjs'), [name]);
132
+ if (code === 0) console.log(`\nnext: npx trueclone motion ${name}, then build sites/${name}/clone/ following .claude/skills/trueclone/SKILL.md`);
133
+ process.exitCode = code;
134
+ } else if (STEPS[cmd]) process.exitCode = await run(path.join(SCRIPTS, STEPS[cmd][0]), rest);
135
+ else {
136
+ console.error(`unknown command: ${cmd}\n`);
137
+ help();
138
+ process.exitCode = 1;
139
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@trueclone/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A working twin of any website from its URL: HTML clone, Next.js + Tailwind + shadcn app, DESIGN.md and globals.css, proven by layout, motion, interaction and style scores. Bundles REA.",
5
+ "type": "module",
6
+ "bin": {
7
+ "trueclone": "bin/trueclone.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "scripts/*.mjs",
12
+ "skill/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": "^22.19.0 || ^24.11.0 || >=26.0.0"
18
+ },
19
+ "scripts": {
20
+ "trueclone": "node bin/trueclone.mjs",
21
+ "chrome": "powershell -ExecutionPolicy Bypass -File scripts/chrome-debug.ps1"
22
+ },
23
+ "dependencies": {
24
+ "@google/design.md": "^0.4.0",
25
+ "parse5": "^8.0.1",
26
+ "pixelmatch": "^7.2.0",
27
+ "playwright": "^1.64.0",
28
+ "pngjs": "^7.0.0",
29
+ "rea-agents": "^6.4.0"
30
+ },
31
+ "keywords": [
32
+ "clone",
33
+ "website",
34
+ "replicate",
35
+ "rebuild",
36
+ "migration",
37
+ "design-system",
38
+ "design.md",
39
+ "shadcn",
40
+ "nextjs",
41
+ "tailwind",
42
+ "playwright",
43
+ "pixel-diff",
44
+ "gsap",
45
+ "coding-agent",
46
+ "claude-code",
47
+ "mcp",
48
+ "reverse-engineering",
49
+ "rea"
50
+ ],
51
+ "author": "Ayush Pratap Singh",
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/Ayuxh-Pratap/trueclone.git"
56
+ },
57
+ "homepage": "https://github.com/Ayuxh-Pratap/trueclone#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/Ayuxh-Pratap/trueclone/issues"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }
@@ -0,0 +1,189 @@
1
+ // Capture a live page as the clone's reference: full-page screenshots, sections, design tokens,
2
+ // rendered HTML, CSS/image/font files and a HAR.
3
+ // usage: node scripts/capture.mjs <url> [name] [--hide ".cookie-banner,#popup"] [--cdp [endpoint]]
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import crypto from 'node:crypto';
7
+ import { VIEWPORTS, parseArgs, siteName, openBrowser, newPage, settle, sectionsOf, mask } from './lib.mjs';
8
+
9
+ const { pos, flags } = parseArgs(process.argv.slice(2));
10
+ const [url, nameArg] = pos;
11
+ if (!url) {
12
+ console.error('usage: npm run capture -- <url> [name] [--hide "<css selectors>"] [--cdp]');
13
+ process.exit(1);
14
+ }
15
+ const name = nameArg || siteName(url);
16
+ const hide = typeof flags.hide === 'string' ? flags.hide.split(',').map((s) => s.trim()).filter(Boolean) : [];
17
+ // --mode template (default): assets are kept as reference only; the clone uses placeholder content.
18
+ // --mode exact: the clone may reuse the site's own text and media. Only for sites you own or are
19
+ // authorised to copy, so it refuses to run without --permission "<who authorised it>". Output stays in
20
+ // sites/ (gitignored), the mirror is noindex with every script removed, and nothing here deploys it.
21
+ const mode = flags.mode === 'exact' ? 'exact' : 'template';
22
+ const permission = typeof flags.permission === 'string' ? flags.permission : null;
23
+ if (mode === 'exact' && !permission) {
24
+ console.error('exact mode copies the site\'s own text and media. Pass --permission "<owner / who authorised this copy>".');
25
+ process.exit(1);
26
+ }
27
+ const root = path.resolve('sites', name);
28
+ const ref = path.join(root, 'reference');
29
+ const MAX_ASSET_BYTES = mode === 'exact' ? 200 * 1024 * 1024 : 15 * 1024 * 1024;
30
+ const SAVED_TYPES = new Set(['stylesheet', 'image', 'font', 'media']);
31
+ // Exact mode also keeps runtime-fetched media: 3D models, textures, video streams, animation files. Never scripts.
32
+ const EXTRA_MEDIA = /\.(glb|gltf|bin|hdr|exr|ktx2|basis|mp4|webm|mov|m3u8|ts|m4s|lottie|riv|json)(\?|$)/i;
33
+ const keepResponse = (type, url) => SAVED_TYPES.has(type) || (mode === 'exact' && ['fetch', 'xhr', 'other'].includes(type) && EXTRA_MEDIA.test(url));
34
+
35
+ // Only clear what capture owns; states/, motion.*, *.spec.json come from other scripts and stay.
36
+ await fs.rm(path.join(ref, 'assets'), { recursive: true, force: true });
37
+ await fs.rm(path.join(ref, 'mirror'), { recursive: true, force: true });
38
+ await fs.mkdir(path.join(ref, 'assets'), { recursive: true });
39
+
40
+ function tokensInPage() {
41
+ const count = (m, k) => m.set(k, (m.get(k) || 0) + 1);
42
+ const colors = new Map(), bgs = new Map(), fonts = new Map(), type = new Map(), radii = new Map(), shadows = new Map();
43
+ for (const el of [...document.querySelectorAll('body *')].slice(0, 8000)) {
44
+ const s = getComputedStyle(el);
45
+ if (s.display === 'none' || s.visibility === 'hidden') continue;
46
+ if ([...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) {
47
+ count(colors, s.color);
48
+ count(fonts, s.fontFamily);
49
+ count(type, `${s.fontSize} / ${s.lineHeight} / ${s.fontWeight} / ${s.letterSpacing}`);
50
+ }
51
+ if (s.backgroundColor !== 'rgba(0, 0, 0, 0)') count(bgs, s.backgroundColor);
52
+ if (s.backgroundImage !== 'none') count(bgs, s.backgroundImage.slice(0, 200));
53
+ if (s.borderRadius !== '0px') count(radii, s.borderRadius);
54
+ if (s.boxShadow !== 'none') count(shadows, s.boxShadow);
55
+ }
56
+ const top = (m) => [...m].sort((a, b) => b[1] - a[1]).slice(0, 30).map(([value, uses]) => ({ value, uses }));
57
+ const cssVariables = {};
58
+ for (const sheet of document.styleSheets) {
59
+ try {
60
+ for (const rule of sheet.cssRules) {
61
+ if (rule.selectorText === ':root' || rule.selectorText === 'html') {
62
+ for (const p of rule.style) if (p.startsWith('--')) cssVariables[p] = rule.style.getPropertyValue(p).trim();
63
+ }
64
+ }
65
+ } catch { /* cross-origin sheet */ }
66
+ }
67
+ return {
68
+ title: document.title,
69
+ lang: document.documentElement.lang,
70
+ bodyBackground: getComputedStyle(document.body).backgroundColor,
71
+ textColors: top(colors),
72
+ backgrounds: top(bgs),
73
+ fontFamilies: top(fonts),
74
+ typeScale: top(type),
75
+ radii: top(radii),
76
+ shadows: top(shadows),
77
+ cssVariables,
78
+ fontsLoaded: [...document.fonts].filter((f) => f.status === 'loaded').map((f) => `${f.family} ${f.weight} ${f.style}`),
79
+ };
80
+ }
81
+
82
+ const browser = await openBrowser(flags.cdp);
83
+ const manifest = [];
84
+ const summary = [];
85
+ const rootFontSize = {};
86
+ try {
87
+ for (const vp of VIEWPORTS) {
88
+ const desktop = vp.name === 'desktop';
89
+ const { context, page } = await newPage(browser, vp, desktop ? { recordHar: { path: path.join(ref, 'desktop.har'), content: 'omit' } } : {});
90
+ if (desktop) {
91
+ page.on('response', async (res) => {
92
+ const rawType = res.request().resourceType();
93
+ if (!keepResponse(rawType, res.url()) || res.status() >= 300) return;
94
+ const type = SAVED_TYPES.has(rawType) ? rawType : 'media';
95
+ try {
96
+ const body = await res.body();
97
+ if (body.length > MAX_ASSET_BYTES) return;
98
+ const u = new URL(res.url());
99
+ const base = (path.basename(u.pathname) || 'index').replace(/[^a-z0-9._-]/gi, '_').slice(-80);
100
+ const hash = crypto.createHash('sha1').update(res.url()).digest('hex').slice(0, 8);
101
+ const file = path.join('assets', type, `${hash}-${base}`);
102
+ await fs.mkdir(path.join(ref, 'assets', type), { recursive: true });
103
+ await fs.writeFile(path.join(ref, file), body);
104
+ manifest.push({ url: res.url(), type, file: file.replaceAll('\\', '/'), contentType: res.headers()['content-type'], bytes: body.length });
105
+ } catch { /* redirects, data: urls, aborted */ }
106
+ });
107
+ }
108
+ await page.goto(url, { waitUntil: 'load', timeout: 60000 });
109
+ await settle(page, hide);
110
+ await page.screenshot({ path: path.join(ref, `${vp.name}.png`), fullPage: true });
111
+ const sections = await sectionsOf(page);
112
+ await fs.writeFile(path.join(ref, `${vp.name}.sections.json`), JSON.stringify(sections, null, 2));
113
+ const height = await page.evaluate(() => document.documentElement.scrollHeight);
114
+ rootFontSize[vp.name] = await page.evaluate(() => getComputedStyle(document.documentElement).fontSize);
115
+ if (desktop) {
116
+ const html = await page.evaluate(() => {
117
+ const doc = document.documentElement.cloneNode(true);
118
+ doc.querySelector('style[data-lab-freeze]')?.remove();
119
+ return '<!DOCTYPE html>\n' + doc.outerHTML;
120
+ });
121
+ await fs.writeFile(path.join(ref, 'rendered.html'), html);
122
+ await fs.writeFile(path.join(ref, 'tokens.json'), JSON.stringify(await page.evaluate(tokensInPage), null, 2));
123
+ }
124
+ await mask(page);
125
+ await page.screenshot({ path: path.join(ref, `${vp.name}.mask.png`), fullPage: true });
126
+ summary.push({ viewport: vp.name, width: vp.width, pageHeight: height, sections: sections.length });
127
+ await context.close(); // flushes the HAR
128
+ }
129
+ } finally {
130
+ if (!flags.cdp) await browser.close();
131
+ }
132
+
133
+ await fs.writeFile(path.join(ref, 'assets', 'manifest.json'), JSON.stringify(manifest, null, 2));
134
+ await fs.writeFile(path.join(ref, 'meta.json'), JSON.stringify({ url, name, mode, permission, hide, capturedAt: new Date().toISOString(), rootFontSize, viewports: summary }, null, 2));
135
+ if (mode === 'exact') await buildMirror();
136
+ console.table(summary);
137
+ console.log(`${manifest.length} assets saved. Reference: ${path.relative(process.cwd(), ref)}${mode === 'exact' ? ` (exact mode; mirror in reference/mirror, authorised by: ${permission})` : ''}`);
138
+
139
+ // ---------- Exact mode: static local mirror of the rendered page ----------
140
+ // Same DOM as rendered.html, asset URLs rewritten to the saved copies, every script and inline handler
141
+ // removed (we never reuse the site's code), forms disabled, noindex. Static by design: motion is rebuilt
142
+ // from the states/motion inventory, not replayed.
143
+ async function buildMirror() {
144
+ const mirror = path.join(ref, 'mirror');
145
+ await fs.rm(mirror, { recursive: true, force: true });
146
+ await fs.mkdir(path.join(mirror, 'css'), { recursive: true });
147
+ const byUrl = new Map(manifest.map((m) => [m.url.split('#')[0], m]));
148
+ const abs = (u, base) => { try { return new URL(u.trim().replace(/^['"]|['"]$/g, '').replaceAll('&amp;', '&'), base).href.split('#')[0]; } catch { return null; } };
149
+ const cssName = (m) => path.basename(m.file);
150
+ // where a saved asset lives, seen from a file inside `fromDir`
151
+ const local = (absUrl, fromDir) => {
152
+ const m = absUrl && byUrl.get(absUrl);
153
+ if (!m) return null;
154
+ if (m.type === 'stylesheet') return path.relative(fromDir, path.join(mirror, 'css', cssName(m))).replaceAll('\\', '/');
155
+ return path.relative(fromDir, path.join(ref, m.file)).replaceAll('\\', '/');
156
+ };
157
+ const rewriteCssUrls = (css, base, fromDir) => css
158
+ .replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g, (all, q, u) => (u.startsWith('data:') ? all : `url("${local(abs(u, base), fromDir) || abs(u, base) || u}")`))
159
+ .replace(/@import\s+(['"])([^'"]+)\1/g, (all, q, u) => `@import "${local(abs(u, base), fromDir) || u}"`);
160
+
161
+ // stylesheets: rewritten copies under mirror/css
162
+ for (const m of manifest.filter((x) => x.type === 'stylesheet')) {
163
+ const css = await fs.readFile(path.join(ref, m.file), 'utf8');
164
+ await fs.writeFile(path.join(mirror, 'css', cssName(m)), rewriteCssUrls(css, m.url, path.join(mirror, 'css')));
165
+ }
166
+
167
+ let html = await fs.readFile(path.join(ref, 'rendered.html'), 'utf8');
168
+ html = html
169
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
170
+ .replace(/<script\b[^>]*\/>/gi, '')
171
+ .replace(/<link\b[^>]*rel=["']?(?:modulepreload|preload)["']?[^>]*as=["']?script["']?[^>]*>/gi, '')
172
+ .replace(/\son[a-z]+\s*=\s*("[^"]*"|'[^']*')/gi, '')
173
+ .replace(/<form\b([^>]*?)\saction=("[^"]*"|'[^']*')/gi, '<form$1')
174
+ .replace(/<form\b/gi, '<form action="#"')
175
+ .replace(/\s(src|href|poster|data-src)=("|')([^"']+)\2/gi, (all, attr, q, u) => {
176
+ const l = local(abs(u, url), mirror);
177
+ return l ? ` ${attr}=${q}${l}${q}` : all;
178
+ })
179
+ .replace(/\s(srcset|data-srcset)=("|')([^"']+)\2/gi, (all, attr, q, list) => ` ${attr}=${q}${list.split(',').map((part) => {
180
+ const [u, ...rest] = part.trim().split(/\s+/);
181
+ return [local(abs(u, url), mirror) || u, ...rest].join(' ');
182
+ }).join(', ')}${q}`)
183
+ .replace(/<style\b([^>]*)>([\s\S]*?)<\/style>/gi, (all, attrs, css) => `<style${attrs}>${rewriteCssUrls(css, url, mirror)}</style>`)
184
+ .replace(/\sstyle=("|')([^"']*url\([^"']*)\1/gi, (all, q, css) => ` style=${q}${rewriteCssUrls(css, url, mirror)}${q}`)
185
+ .replace(/<head([^>]*)>/i, `<head$1>\n<!-- rea-lab local mirror of ${url}. Authorised by: ${permission.replace(/--/g, '-')}. Local reference only: do not deploy. -->\n<meta name="robots" content="noindex,nofollow">`);
186
+ await fs.writeFile(path.join(mirror, 'index.html'), html);
187
+ const missing = [...html.matchAll(/\s(?:src|href)=["'](https?:[^"']+)["']/gi)].map((m) => m[1]).filter((u) => !/^https?:\/\/[^/]+\/?$/.test(u));
188
+ console.log(`mirror: reference/mirror/index.html (${manifest.length} local assets, ${missing.length} remote URLs left, e.g. links)`);
189
+ }