launchframe 0.3.1 → 0.4.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/.claude/skills/clone-website/SKILL.md +473 -0
- package/.clinerules +147 -0
- package/.codex/skills/clone-website/SKILL.md +473 -0
- package/{template/.continue → .continue}/commands/clone-website.md +475 -566
- package/.continue/rules/project.md +151 -0
- package/{template/.augment → .cursor}/commands/clone-website.md +470 -565
- package/.cursor/rules/project.mdc +7 -0
- package/{template/.gemini → .gemini}/commands/clone-website.toml +476 -567
- package/.github/copilot-instructions.md +147 -0
- package/.github/skills/clone-website/SKILL.md +473 -0
- package/.gitignore +49 -0
- package/{template/.opencode → .opencode}/commands/clone-website.md +473 -564
- package/{template/.cursor/commands → .windsurf/workflows}/clone-website.md +470 -561
- package/AGENTS.md +65 -0
- package/README.md +137 -31
- package/bin/launchframe.mjs +343 -315
- package/docs/research/INSPECTION_GUIDE.md +80 -0
- package/package.json +71 -26
- package/{template/src → src}/app/globals.css +1 -93
- package/{template/src → src}/app/layout.tsx +16 -5
- package/src/app/page.tsx +40 -0
- package/src/lib/launchframe-config.ts +8 -0
- package/template/.amazonq/cli-agents/clone-website.json +0 -9
- package/template/.amazonq/rules/project.md +0 -281
- package/template/.claude/skills/clone-website/SKILL.md +0 -564
- package/template/.claude/skills/marketing-social-proof-motion/SKILL.md +0 -47
- package/template/.clinerules +0 -285
- package/template/.codex/skills/clone-website/SKILL.md +0 -564
- package/template/.continue/rules/project.md +0 -285
- package/template/.cursor/commands/marketing-social-proof-motion.md +0 -42
- package/template/.cursor/rules/project.mdc +0 -22
- package/template/.github/copilot-instructions.md +0 -281
- package/template/.github/skills/clone-website/SKILL.md +0 -564
- package/template/.nvmrc +0 -1
- package/template/.windsurf/workflows/clone-website.md +0 -561
- package/template/AGENTS.md +0 -160
- package/template/LICENSE +0 -21
- package/template/README.md +0 -121
- package/template/START_HERE.md +0 -15
- package/template/docs/design-references/playwright-example.com-1440px.png +0 -0
- package/template/docs/design-references/playwright-example.com-390px.png +0 -0
- package/template/docs/research/INSPECTION_GUIDE.md +0 -124
- package/template/launchframe.config.json +0 -14
- package/template/package-lock.json +0 -9873
- package/template/package.json +0 -54
- package/template/scripts/.gitkeep +0 -0
- package/template/scripts/recon-playwright.mjs +0 -396
- package/template/src/app/page.tsx +0 -5
- package/template/src/components/marketing/scribewise-landing.tsx +0 -34
- package/template/src/hooks/.gitkeep +0 -0
- package/template/src/types/.gitkeep +0 -0
- /package/{template/.aider.conf.yml → .aider.conf.yml} +0 -0
- /package/{template/.dockerignore → .dockerignore} +0 -0
- /package/{template/.gitattributes → .gitattributes} +0 -0
- /package/{template/.github → .github}/ISSUE_TEMPLATE/bug_report.yml +0 -0
- /package/{template/.github → .github}/ISSUE_TEMPLATE/config.yml +0 -0
- /package/{template/.github → .github}/ISSUE_TEMPLATE/feature_request.yml +0 -0
- /package/{template/.github → .github}/PULL_REQUEST_TEMPLATE.md +0 -0
- /package/{template/.github → .github}/copilot-setup-steps.yml +0 -0
- /package/{template/.github → .github}/workflows/ci.yml +0 -0
- /package/{template/.windsurfrules → .windsurfrules} +0 -0
- /package/{template/CLAUDE.md → CLAUDE.md} +0 -0
- /package/{template/Dockerfile → Dockerfile} +0 -0
- /package/{template/Dockerfile.dev → Dockerfile.dev} +0 -0
- /package/{template/GEMINI.md → GEMINI.md} +0 -0
- /package/{template/components.json → components.json} +0 -0
- /package/{template/docker-compose.yml → docker-compose.yml} +0 -0
- /package/{template/docs → docs}/design-references/.gitkeep +0 -0
- /package/{template/docs → docs}/design-references/comparison.png +0 -0
- /package/{template/eslint.config.mjs → eslint.config.mjs} +0 -0
- /package/{template/next.config.ts → next.config.ts} +0 -0
- /package/{template/postcss.config.mjs → postcss.config.mjs} +0 -0
- /package/{template/public/images → scripts}/.gitkeep +0 -0
- /package/{template/scripts → scripts}/sync-agent-rules.sh +0 -0
- /package/{template/scripts → scripts}/sync-skills.mjs +0 -0
- /package/{template/src → src}/app/favicon.ico +0 -0
- /package/{template/src → src}/components/ui/button.tsx +0 -0
- /package/{template/public/seo → src/hooks}/.gitkeep +0 -0
- /package/{template/src → src}/lib/utils.ts +0 -0
- /package/{template/public/videos → src/types}/.gitkeep +0 -0
- /package/{template/tsconfig.json → tsconfig.json} +0 -0
package/template/package.json
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ai-website-cloner-template",
|
|
3
|
-
"version": "0.3.1",
|
|
4
|
-
"private": true,
|
|
5
|
-
"description": "clone any website into a clean, modern Next.js codebase using AI coding agents",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"claude-code",
|
|
9
|
-
"website-clone",
|
|
10
|
-
"reverse-engineering",
|
|
11
|
-
"nextjs",
|
|
12
|
-
"ai",
|
|
13
|
-
"template",
|
|
14
|
-
"tailwindcss",
|
|
15
|
-
"shadcn-ui"
|
|
16
|
-
],
|
|
17
|
-
"engines": {
|
|
18
|
-
"node": ">=24"
|
|
19
|
-
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"dev": "next dev",
|
|
22
|
-
"build": "next build",
|
|
23
|
-
"start": "next start",
|
|
24
|
-
"lint": "eslint",
|
|
25
|
-
"typecheck": "tsc --noEmit",
|
|
26
|
-
"check": "npm run lint && npm run typecheck && npm run build",
|
|
27
|
-
"recon": "node scripts/recon-playwright.mjs",
|
|
28
|
-
"recon:headed": "node scripts/recon-playwright.mjs --headed"
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"@base-ui/react": "^1.3.0",
|
|
32
|
-
"class-variance-authority": "^0.7.1",
|
|
33
|
-
"clsx": "^2.1.1",
|
|
34
|
-
"framer-motion": "^12.4.0",
|
|
35
|
-
"lucide-react": "^1.6.0",
|
|
36
|
-
"next": "16.2.1",
|
|
37
|
-
"react": "19.2.4",
|
|
38
|
-
"react-dom": "19.2.4",
|
|
39
|
-
"shadcn": "^4.1.0",
|
|
40
|
-
"tailwind-merge": "^3.5.0",
|
|
41
|
-
"tw-animate-css": "^1.4.0"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@tailwindcss/postcss": "^4",
|
|
45
|
-
"@types/node": "^24",
|
|
46
|
-
"@types/react": "^19",
|
|
47
|
-
"@types/react-dom": "^19",
|
|
48
|
-
"eslint": "^9",
|
|
49
|
-
"eslint-config-next": "16.2.1",
|
|
50
|
-
"playwright": "^1.49.1",
|
|
51
|
-
"tailwindcss": "^4",
|
|
52
|
-
"typescript": "^5"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
File without changes
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Playwright recon when Browser / Chrome DevTools MCP is unavailable or broken.
|
|
4
|
-
*
|
|
5
|
-
* Reads URL from launchframe.config.json (or --url). Writes:
|
|
6
|
-
* - docs/design-references/playwright-<host>-<w>px.png (full page)
|
|
7
|
-
* - docs/research/computed-snapshot.json (styles + merged asset inventory + bot-wall hint)
|
|
8
|
-
* - docs/research/MEDIA_MANIFEST.md (table of discovered media URLs)
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* npm run recon
|
|
12
|
-
* npm run recon:headed
|
|
13
|
-
* node scripts/recon-playwright.mjs --url https://example.com --headed
|
|
14
|
-
*
|
|
15
|
-
* First time: npx playwright install chromium
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
19
|
-
import { dirname, join } from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
|
|
22
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
-
|
|
24
|
-
function parseArgs(argv) {
|
|
25
|
-
const out = { url: null, headed: false, channel: process.env.PW_CHANNEL || undefined };
|
|
26
|
-
for (let i = 0; i < argv.length; i++) {
|
|
27
|
-
const a = argv[i];
|
|
28
|
-
if (a === "--headed" || a === "-H") out.headed = true;
|
|
29
|
-
else if (a === "--url" || a === "-u") out.url = argv[++i];
|
|
30
|
-
else if (a.startsWith("--url=")) out.url = a.slice(6);
|
|
31
|
-
else if (a === "--channel" || a === "-c") out.channel = argv[++i] || undefined;
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function readLaunchframeUrl() {
|
|
37
|
-
const p = join(ROOT, "launchframe.config.json");
|
|
38
|
-
if (!existsSync(p)) return null;
|
|
39
|
-
try {
|
|
40
|
-
const j = JSON.parse(readFileSync(p, "utf8"));
|
|
41
|
-
return j.url || null;
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function safeHost(url) {
|
|
48
|
-
try {
|
|
49
|
-
return new URL(url).hostname.replace(/[^a-z0-9.-]+/gi, "_");
|
|
50
|
-
} catch {
|
|
51
|
-
return "unknown-host";
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Scroll the full document depth slowly so lazy-loaded media and below-fold DOM resolve.
|
|
57
|
-
* Repeats until scrollHeight stabilizes (handles incremental infinite layouts up to max rounds).
|
|
58
|
-
*/
|
|
59
|
-
async function scrollFullPage(page) {
|
|
60
|
-
await page.evaluate(async () => {
|
|
61
|
-
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
62
|
-
const stepSize = () => Math.max(1, Math.floor(window.innerHeight * 0.85));
|
|
63
|
-
|
|
64
|
-
let unchangedStreak = 0;
|
|
65
|
-
let prevTotal = -1;
|
|
66
|
-
|
|
67
|
-
for (let round = 0; round < 12; round++) {
|
|
68
|
-
let y = 0;
|
|
69
|
-
let limit = document.documentElement.scrollHeight;
|
|
70
|
-
|
|
71
|
-
while (y < limit) {
|
|
72
|
-
y = Math.min(y + stepSize(), limit);
|
|
73
|
-
window.scrollTo(0, y);
|
|
74
|
-
await delay(130);
|
|
75
|
-
limit = document.documentElement.scrollHeight;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
window.scrollTo(0, document.documentElement.scrollHeight);
|
|
79
|
-
await delay(420);
|
|
80
|
-
|
|
81
|
-
const h = document.documentElement.scrollHeight;
|
|
82
|
-
|
|
83
|
-
window.scrollTo(0, 0);
|
|
84
|
-
await delay(220);
|
|
85
|
-
|
|
86
|
-
if (h === prevTotal) unchangedStreak++;
|
|
87
|
-
else unchangedStreak = 0;
|
|
88
|
-
prevTotal = h;
|
|
89
|
-
|
|
90
|
-
if (unchangedStreak >= 2) break;
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function mergeAssetInventory(base, extra) {
|
|
96
|
-
if (!extra) return base;
|
|
97
|
-
const imgKey = (i) => i.src || "";
|
|
98
|
-
const seenImg = new Set(base.images.map(imgKey));
|
|
99
|
-
const images = [...base.images];
|
|
100
|
-
for (const i of extra.images) {
|
|
101
|
-
const k = imgKey(i);
|
|
102
|
-
if (k && !seenImg.has(k)) {
|
|
103
|
-
seenImg.add(k);
|
|
104
|
-
images.push(i);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const vidKey = (v) => `${v.src || ""}|${v.poster || ""}`;
|
|
109
|
-
const seenVid = new Set(base.videos.map(vidKey));
|
|
110
|
-
const videos = [...base.videos];
|
|
111
|
-
for (const v of extra.videos) {
|
|
112
|
-
const k = vidKey(v);
|
|
113
|
-
if ((v.src || v.poster) && !seenVid.has(k)) {
|
|
114
|
-
seenVid.add(k);
|
|
115
|
-
videos.push(v);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const seenBg = new Set(base.backgroundImages.map((b) => b.url));
|
|
120
|
-
const backgroundImages = [...base.backgroundImages];
|
|
121
|
-
for (const b of extra.backgroundImages) {
|
|
122
|
-
if (b.url && !seenBg.has(b.url)) {
|
|
123
|
-
seenBg.add(b.url);
|
|
124
|
-
backgroundImages.push(b);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
images,
|
|
130
|
-
videos,
|
|
131
|
-
backgroundImages,
|
|
132
|
-
svgCount: Math.max(base.svgCount ?? 0, extra.svgCount ?? 0),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function gatherPageData(page) {
|
|
137
|
-
return page.evaluate(() => {
|
|
138
|
-
const title = document.title || "";
|
|
139
|
-
const text = (document.body?.innerText || "").slice(0, 80000);
|
|
140
|
-
const likelyBotWall =
|
|
141
|
-
/just a moment/i.test(title) ||
|
|
142
|
-
/checking your browser/i.test(text) ||
|
|
143
|
-
/cf-browser-verification/i.test(document.documentElement?.innerHTML || "") ||
|
|
144
|
-
document.querySelector("#challenge-running, .cf-browser-verification, #cf-challenge-running") !== null;
|
|
145
|
-
|
|
146
|
-
const imgs = [...document.querySelectorAll("img")].map((img) => ({
|
|
147
|
-
src: img.currentSrc || img.src || "",
|
|
148
|
-
srcset: img.srcset || "",
|
|
149
|
-
alt: img.alt || "",
|
|
150
|
-
w: img.naturalWidth,
|
|
151
|
-
h: img.naturalHeight,
|
|
152
|
-
}));
|
|
153
|
-
|
|
154
|
-
const videos = [...document.querySelectorAll("video")].map((v) => ({
|
|
155
|
-
src: v.currentSrc || v.src || "",
|
|
156
|
-
poster: v.poster || "",
|
|
157
|
-
autoplay: v.autoplay,
|
|
158
|
-
loop: v.loop,
|
|
159
|
-
muted: v.muted,
|
|
160
|
-
}));
|
|
161
|
-
|
|
162
|
-
const bgUrls = [];
|
|
163
|
-
const seen = new Set();
|
|
164
|
-
for (const el of document.querySelectorAll("*")) {
|
|
165
|
-
if (bgUrls.length > 400) break;
|
|
166
|
-
const bg = getComputedStyle(el).backgroundImage;
|
|
167
|
-
if (!bg || bg === "none") continue;
|
|
168
|
-
const m = /url\(["']?([^"')]+)["']?\)/.exec(bg);
|
|
169
|
-
if (m && m[1] && !seen.has(m[1])) {
|
|
170
|
-
seen.add(m[1]);
|
|
171
|
-
bgUrls.push({ url: m[1], tag: el.tagName.toLowerCase() });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const props = [
|
|
176
|
-
"fontSize",
|
|
177
|
-
"fontWeight",
|
|
178
|
-
"fontFamily",
|
|
179
|
-
"lineHeight",
|
|
180
|
-
"letterSpacing",
|
|
181
|
-
"color",
|
|
182
|
-
"backgroundColor",
|
|
183
|
-
"padding",
|
|
184
|
-
"margin",
|
|
185
|
-
"maxWidth",
|
|
186
|
-
"display",
|
|
187
|
-
"gap",
|
|
188
|
-
"borderRadius",
|
|
189
|
-
"boxShadow",
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
function pickStyles(el) {
|
|
193
|
-
if (!el) return null;
|
|
194
|
-
const cs = getComputedStyle(el);
|
|
195
|
-
const o = {};
|
|
196
|
-
for (const p of props) o[p] = cs[p];
|
|
197
|
-
return o;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const landmarks = {
|
|
201
|
-
html: pickStyles(document.documentElement),
|
|
202
|
-
body: pickStyles(document.body),
|
|
203
|
-
h1: pickStyles(document.querySelector("h1")),
|
|
204
|
-
header: pickStyles(document.querySelector("header")),
|
|
205
|
-
main: pickStyles(document.querySelector("main")),
|
|
206
|
-
firstSection: pickStyles(document.querySelector("main section, section")),
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
title,
|
|
211
|
-
likelyBotWall,
|
|
212
|
-
textSample: text.slice(0, 24000),
|
|
213
|
-
assetInventory: {
|
|
214
|
-
images: imgs.filter((i) => i.src),
|
|
215
|
-
videos,
|
|
216
|
-
backgroundImages: bgUrls,
|
|
217
|
-
svgCount: document.querySelectorAll("svg").length,
|
|
218
|
-
},
|
|
219
|
-
computedLandmarks: landmarks,
|
|
220
|
-
};
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function writeMediaManifest(host, data, limitationsPath) {
|
|
225
|
-
const { images, videos, backgroundImages } = data.assetInventory;
|
|
226
|
-
const lines = [
|
|
227
|
-
"# MEDIA_MANIFEST (Playwright)",
|
|
228
|
-
"",
|
|
229
|
-
`Host: **${host}**`,
|
|
230
|
-
"",
|
|
231
|
-
"## Images (`<img>`)",
|
|
232
|
-
"",
|
|
233
|
-
"| src | alt | natural |",
|
|
234
|
-
"|-----|-----|---------|",
|
|
235
|
-
];
|
|
236
|
-
for (const i of images.slice(0, 200)) {
|
|
237
|
-
const s = i.src.replace(/\|/g, "\\|");
|
|
238
|
-
lines.push(`| ${s} | ${(i.alt || "").replace(/\|/g, "\\|")} | ${i.w}×${i.h} |`);
|
|
239
|
-
}
|
|
240
|
-
if (images.length > 200) lines.push(`\n_…and ${images.length - 200} more_\n`);
|
|
241
|
-
|
|
242
|
-
lines.push("", "## Video", "", "| src | poster |", "|-----|--------|");
|
|
243
|
-
for (const v of videos) {
|
|
244
|
-
lines.push(`| ${(v.src || "").replace(/\|/g, "\\|")} | ${(v.poster || "").replace(/\|/g, "\\|")} |`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
lines.push("", "## Background images (sample)", "", "| url |", "|-----|");
|
|
248
|
-
for (const b of backgroundImages.slice(0, 150)) {
|
|
249
|
-
lines.push(`| ${b.url.replace(/\|/g, "\\|")} |`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
lines.push("", "---", "", `_Generated by scripts/recon-playwright.mjs. Download with scripts/download-assets.mjs when ready._`);
|
|
253
|
-
if (limitationsPath) lines.push("", `See also: **${limitationsPath}**`);
|
|
254
|
-
|
|
255
|
-
writeFileSync(join(ROOT, "docs", "research", "MEDIA_MANIFEST.md"), lines.join("\n") + "\n", "utf8");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function writeLimitations(host, url, finalUrl, data) {
|
|
259
|
-
const p = join(ROOT, "docs", "research", "EXTRACTION_LIMITATIONS.md");
|
|
260
|
-
const lines = [
|
|
261
|
-
"# Extraction limitations",
|
|
262
|
-
"",
|
|
263
|
-
`- **Requested URL:** ${url}`,
|
|
264
|
-
`- **Final URL:** ${finalUrl}`,
|
|
265
|
-
`- **Host:** ${host}`,
|
|
266
|
-
`- **Captured at:** ${new Date().toISOString()}`,
|
|
267
|
-
`- **Tool:** Playwright (\`npm run recon\`)`,
|
|
268
|
-
"",
|
|
269
|
-
];
|
|
270
|
-
if (data.likelyBotWall) {
|
|
271
|
-
lines.push(
|
|
272
|
-
"## Bot / challenge page suspected",
|
|
273
|
-
"",
|
|
274
|
-
"Title or DOM matches common WAF/interstitial patterns. **Computed styles and screenshots may not reflect the real marketing page.** Prefer \`npm run recon:headed\` with real Chrome, or open the site manually and export assets.",
|
|
275
|
-
""
|
|
276
|
-
);
|
|
277
|
-
} else {
|
|
278
|
-
lines.push(
|
|
279
|
-
"## Notes",
|
|
280
|
-
"",
|
|
281
|
-
"Snapshot-derived data is from Playwright Chromium, not interactive MCP. Re-run after layout shifts; verify against a real browser for pixel QA.",
|
|
282
|
-
""
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
writeFileSync(p, lines.join("\n"), "utf8");
|
|
286
|
-
return "docs/research/EXTRACTION_LIMITATIONS.md";
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function main() {
|
|
290
|
-
const args = parseArgs(process.argv.slice(2));
|
|
291
|
-
const url = args.url || readLaunchframeUrl();
|
|
292
|
-
if (!url) {
|
|
293
|
-
console.error("No URL: set launchframe.config.json or pass --url https://...");
|
|
294
|
-
process.exit(1);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
let chromium;
|
|
298
|
-
try {
|
|
299
|
-
({ chromium } = await import("playwright"));
|
|
300
|
-
} catch {
|
|
301
|
-
console.error("Install Playwright: npm i -D playwright && npx playwright install chromium");
|
|
302
|
-
process.exit(1);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const host = safeHost(url);
|
|
306
|
-
mkdirSync(join(ROOT, "docs", "design-references"), { recursive: true });
|
|
307
|
-
mkdirSync(join(ROOT, "docs", "research"), { recursive: true });
|
|
308
|
-
|
|
309
|
-
const browser = await chromium.launch({
|
|
310
|
-
headless: !args.headed,
|
|
311
|
-
channel: args.channel,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
const context = await browser.newContext({
|
|
315
|
-
userAgent:
|
|
316
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
317
|
-
locale: "en-US",
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
const page = await context.newPage();
|
|
321
|
-
|
|
322
|
-
const snapshot = {
|
|
323
|
-
fetchedAt: new Date().toISOString(),
|
|
324
|
-
requestedUrl: url,
|
|
325
|
-
finalUrl: null,
|
|
326
|
-
viewports: {},
|
|
327
|
-
title: null,
|
|
328
|
-
likelyBotWall: false,
|
|
329
|
-
assetInventory: null,
|
|
330
|
-
computedLandmarks: null,
|
|
331
|
-
textSample: null,
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 90_000 });
|
|
336
|
-
await new Promise((r) => setTimeout(r, 2500));
|
|
337
|
-
snapshot.finalUrl = page.url();
|
|
338
|
-
|
|
339
|
-
let mergedInventory = null;
|
|
340
|
-
let desktopLandmarks = null;
|
|
341
|
-
|
|
342
|
-
for (const w of [1440, 390]) {
|
|
343
|
-
await page.setViewportSize({ width: w, height: w === 1440 ? 900 : 844 });
|
|
344
|
-
await scrollFullPage(page);
|
|
345
|
-
|
|
346
|
-
const shotPath = join(ROOT, "docs", "design-references", `playwright-${host}-${w}px.png`);
|
|
347
|
-
await page.screenshot({ path: shotPath, fullPage: true });
|
|
348
|
-
snapshot.viewports[w] = { screenshot: `docs/design-references/playwright-${host}-${w}px.png` };
|
|
349
|
-
|
|
350
|
-
const data = await gatherPageData(page);
|
|
351
|
-
|
|
352
|
-
if (w === 1440) {
|
|
353
|
-
snapshot.title = data.title;
|
|
354
|
-
snapshot.likelyBotWall = data.likelyBotWall;
|
|
355
|
-
mergedInventory = data.assetInventory;
|
|
356
|
-
desktopLandmarks = data.computedLandmarks;
|
|
357
|
-
snapshot.textSample = data.textSample;
|
|
358
|
-
} else {
|
|
359
|
-
mergedInventory = mergeAssetInventory(mergedInventory, data.assetInventory);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
snapshot.assetInventory = mergedInventory;
|
|
364
|
-
snapshot.computedLandmarks = desktopLandmarks;
|
|
365
|
-
} finally {
|
|
366
|
-
await browser.close();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const limRel = writeLimitations(host, url, snapshot.finalUrl, {
|
|
370
|
-
likelyBotWall: snapshot.likelyBotWall,
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
writeFileSync(
|
|
374
|
-
join(ROOT, "docs", "research", "computed-snapshot.json"),
|
|
375
|
-
JSON.stringify(snapshot, null, 2) + "\n",
|
|
376
|
-
"utf8"
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
if (snapshot.assetInventory) {
|
|
380
|
-
writeMediaManifest(host, { assetInventory: snapshot.assetInventory }, limRel);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
console.log("Playwright recon finished.");
|
|
384
|
-
console.log(` Screenshots: docs/design-references/playwright-${host}-1440px.png (+ 390px)`);
|
|
385
|
-
console.log(` Snapshot: docs/research/computed-snapshot.json`);
|
|
386
|
-
console.log(` Media list: docs/research/MEDIA_MANIFEST.md`);
|
|
387
|
-
console.log(` Limits: ${limRel}`);
|
|
388
|
-
if (snapshot.likelyBotWall) {
|
|
389
|
-
console.warn("\n⚠ Possible bot wall / challenge page — check EXTRACTION_LIMITATIONS.md and try: npm run recon:headed\n");
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
main().catch((e) => {
|
|
394
|
-
console.error(e);
|
|
395
|
-
process.exit(1);
|
|
396
|
-
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
|
|
5
|
-
export function ScribewiseLanding() {
|
|
6
|
-
return (
|
|
7
|
-
<main className="relative flex flex-1 flex-col items-center justify-center gap-8 overflow-hidden px-6 py-24 text-center">
|
|
8
|
-
<div
|
|
9
|
-
aria-hidden
|
|
10
|
-
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,color-mix(in_oklab,var(--primary)_28%,transparent),transparent)]"
|
|
11
|
-
/>
|
|
12
|
-
<p className="text-muted-foreground text-xs font-medium tracking-[0.2em] uppercase">
|
|
13
|
-
Launchframe template
|
|
14
|
-
</p>
|
|
15
|
-
<div className="max-w-2xl space-y-4">
|
|
16
|
-
<h1 className="text-balance font-semibold text-4xl tracking-tight sm:text-5xl">
|
|
17
|
-
Ready for `/clone-website`
|
|
18
|
-
</h1>
|
|
19
|
-
<p className="text-muted-foreground text-balance text-lg">
|
|
20
|
-
Add your target URL and SaaS idea via <code className="text-foreground">launchframe.config.json</code>,
|
|
21
|
-
run recon if needed, then let your agent ship the full landing.
|
|
22
|
-
</p>
|
|
23
|
-
</div>
|
|
24
|
-
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
25
|
-
<Button nativeButton={false} render={<Link href="https://github.com/evangruhlkey/launchframe" />}>
|
|
26
|
-
Launchframe docs
|
|
27
|
-
</Button>
|
|
28
|
-
<Button variant="outline" nativeButton={false} render={<Link href="/" />}>
|
|
29
|
-
Replace this page
|
|
30
|
-
</Button>
|
|
31
|
-
</div>
|
|
32
|
-
</main>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|