/** * Clone Architect — Static Site Generator * * Reads catalog/index.json → generates public/index.html * Full site: hero + stats + brands directory with search/filter + compare + how it works * * Usage: npx tsx scripts/generate-site.ts */ import { readFile, writeFile, mkdir } from 'fs/promises'; import { existsSync } from 'fs'; import { join } from 'path'; const ROOT = process.cwd(); const CATALOG_PATH = join(ROOT, 'catalog', 'index.json'); const PUBLIC_DIR = join(ROOT, 'public'); const OUT_PATH = join(PUBLIC_DIR, 'index.html'); interface CatalogBrand { domain: string; hasDesignMd: boolean; hasTokens: boolean; hasScreenshots: boolean; hasFrontmatter: boolean; accent: string; description: string; sizeKb: number; category: string; dark: boolean; font: string; completeness: number; extractedAt: string; } interface Catalog { version: string; count: number; brands: CatalogBrand[]; } function gradeColor(score: number): string { if (score >= 90) return '#4ade80'; // green if (score >= 75) return '#a3e635'; // lime if (score >= 60) return '#facc15'; // yellow return '#f87171'; // red } function gradeLabel(score: number): string { if (score >= 90) return 'A'; if (score >= 75) return 'B'; if (score >= 60) return 'C'; return 'D'; } function domainName(domain: string): string { return domain .replace(/^www\./, '') .replace(/\.(com|app|io|dev|net|org|ai|co|xyz|so|design|new)$/, '') .replace(/^(.)/, c => c.toUpperCase()); } function accentHex(accent: string): string { if (!accent) return '#5e6ad2'; if (accent.startsWith('#')) return accent; // parse rgb() const m = accent.match(/\d+/g); if (m && m.length >= 3) { return '#' + m.slice(0, 3).map(v => parseInt(v).toString(16).padStart(2, '0')).join(''); } return '#5e6ad2'; } function showcaseUrl(domain: string): string { // On the public site, showcases are served from /brands/{domain}/ return `brands/${domain}/`; } async function main() { if (!existsSync(CATALOG_PATH)) { console.error('catalog/index.json not found — run: npx tsx scripts/enrich-catalog.ts'); process.exit(1); } const catalogRaw = await readFile(CATALOG_PATH, 'utf-8'); const catalog: Catalog = JSON.parse(catalogRaw); // Read npm package version (not catalog version) for eyebrow const pkgRaw = await readFile(join(ROOT, 'package.json'), 'utf-8'); const pkgVersion: string = JSON.parse(pkgRaw).version; // Sort by completeness desc const brands = [...catalog.brands].sort((a, b) => (b.completeness || 0) - (a.completeness || 0)); const avgScore = Math.round(brands.reduce((s, b) => s + (b.completeness || 0), 0) / brands.length); const darkCount = brands.filter(b => b.dark).length; const categories = [...new Set(brands.map(b => b.category).filter(Boolean))].sort(); // Inline catalog as JSON for client-side filtering const catalogJson = JSON.stringify(brands); await mkdir(PUBLIC_DIR, { recursive: true }); const html = ` Clone Architect — Extract real design from any URL
v${pkgVersion} · MIT · local-first · Playwright

Extract real design from
any URL. Automatically.

Playwright + getComputedStyle() → narrative DESIGN.md + tokens.json + screenshots. The verifiable alternative to manually-written design docs. No catalog. No $39/brand. MIT.

Path A — Browse catalog · no setup required
$ npx clone-architect add linear.app
Path B — Extract any URL · requires Playwright
$ npx clone-architect extract https://linear.app
${brands.length}
brands extracted
${avgScore}/100
avg completeness
${darkCount}
dark-mode sites
MIT
license · local-first
Directory

${brands.length} design systems extracted

Each extraction ships with: raw-css.json (ground truth), desktop + mobile screenshots, tokens.json, layout analysis, and a 16-section narrative DESIGN.md.

${categories.map(c => ``).join('\n ')}
${brands.length} brands
Comparison

Why not just use getdesign.md?

Respect to the VoltAgent team — getdesign.md proved the format. But it's a static, manually-written catalog of 73 brands. Clone Architect extracts any URL in 90 seconds.

Feature getdesign.md Clone Architect
Extract any URL 73 pre-written brands Unlimited · any URL
Source of truth Manually written getComputedStyle() — pixel-perfect
Raw CSS audit Not shipped raw-css.json (360 KB for mistral.ai)
Visual proof None Desktop 1440px + Mobile 390px screenshots
Structured tokens Markdown prose only tokens.json — import directly in code
Component states Not captured Hover / focus / pressed via Playwright interaction
Page structure skeleton Not included §13 — section order + height + layout type
Real copy / CTAs Not included §14 — h1/h2/h3/nav/CTAs extracted verbatim
Re-extract on demand Static — gets stale clone-architect update <domain>
Completeness score No confidence signal 0-100 grade (A/B/C/D) per extraction
Price per brand $39 custom + 2-day wait Free · MIT · 90 seconds
How it works

One command. ${brands.length > 1 ? 'Four' : 'Three'} artifacts.

No accounts, no API keys, no waiting. Playwright loads the URL, scrapes computed styles, generates everything locally.

1

Playwright extraction

Real browser headlessly loads the URL. getComputedStyle() captures every rendered value — not class names, actual pixels. Screenshots at 1440px and 390px.

2

Semantic tokenization

CSS custom properties (--color-text-*, --color-bg-*) map to semantic roles. OpenType features, variable font axes, translucent borders all surfaced automatically.

3

DESIGN.md generation

16 sections: Visual Theme, Colors, Typography, Components, Layout, Depth, Motion, Do/Don'ts, Responsive, Agent Guide, Page Skeleton, Copy Library, Asset Inventory, Component Templates, CSS Raw Export, Known Gaps.

`; await writeFile(OUT_PATH, html); console.log(`✅ Site generated → ${OUT_PATH}`); console.log(` ${brands.length} brands · avg ${avgScore}/100 · ${categories.length} categories`); console.log(`\nDeploy: copy public/ to your web server root`); } main().catch(err => { console.error(err); process.exit(1); });