/** * Clone Architect — Orchestrateur principal * * Pipeline complet : URL → Extract → Analyze → Tokenize * Usage: npm run clone -- */ import { execSync } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; import { shouldSkipExtraction, writeFingerprint } from './shared/cache.js'; // Phase 2.3 — Parse CLI flags: clone [--force] const args = process.argv.slice(2); const forceFlag = args.includes('--force'); const url = args.find(a => !a.startsWith('--')); if (!url) { console.error('Usage: npm run clone -- [--force]'); console.error('Example: npm run clone -- https://linear.app'); console.error(' npm run clone -- https://linear.app --force (bypass cache)'); process.exit(1); } try { new URL(url); } catch { console.error(`Invalid URL: ${url}`); process.exit(1); } const domain = new URL(url).hostname.replace('www.', ''); const baseDir = join(process.cwd(), 'extractions', domain); const startTime = Date.now(); console.log('╔══════════════════════════════════════════════════╗'); console.log('║ CLONE ARCHITECT — Full Pipeline ║'); console.log('╚══════════════════════════════════════════════════╝'); console.log(`\n🎯 Target: ${url}\n`); // Phase 2.3 — Cache check before expensive Playwright extraction const cacheResult = await shouldSkipExtraction(url, domain, { force: forceFlag }); if (cacheResult.skip) { console.log(`✨ Cache hit: ${cacheResult.reason} — skipping extraction (saved ~140s)`); console.log(` Use --force to bypass cache and re-extract`); console.log(`\n📁 Existing extraction at: ${baseDir}\n`); process.exit(0); } console.log(`🔍 Cache: ${cacheResult.reason}`); // Step 1 — Extract console.log('━━━ STEP 1/4 — EXTRACTION ━━━'); try { execSync(`npx tsx scripts/extract.ts "${url}"`, { stdio: 'inherit', cwd: process.cwd(), timeout: 120000, }); } catch (err) { console.error('❌ Extraction failed'); process.exit(1); } // Phase 5.4.1 — Write fingerprint ONLY after extraction validation // Previously (bug G4): fingerprint was written even if raw-css.json was empty/cassée. // Result: future runs hit cache and skip a broken extraction forever. // Now: validate raw-css.json has at least 5 CSS vars AND a body element before writing fingerprint. if (cacheResult.fingerprint) { try { const rawCssPath = join(baseDir, 'raw-css.json'); if (existsSync(rawCssPath)) { const rawData = JSON.parse(require('fs').readFileSync(rawCssPath, 'utf-8')); const cssVarCount = Object.keys(rawData?.desktop?.cssCustomProperties || {}).length; const hasBody = !!rawData?.desktop?.elements?.body; const hasColors = (rawData?.desktop?.allColors || []).length >= 3; // Threshold: at least 3 colors + a body element. Extractions that fail anti-bot have ~0 vars + no body. if (hasBody && hasColors) { await writeFingerprint(domain, cacheResult.fingerprint); } else { console.warn(` ⚠️ Skipping cache fingerprint (cssVars=${cssVarCount}, hasBody=${hasBody}, colors=${(rawData?.desktop?.allColors || []).length}) — looks like an aborted extraction`); } } } catch (e) { console.warn(` ⚠️ Failed to validate raw-css.json before fingerprint: ${(e as Error).message}`); } } // Check extraction output if (!existsSync(join(baseDir, 'raw-css.json'))) { console.error('❌ raw-css.json not found after extraction'); process.exit(1); } // Step 2 — Analyze console.log('\n━━━ STEP 2/4 — LAYOUT ANALYSIS ━━━'); try { execSync(`npx tsx scripts/analyze.ts "${domain}"`, { stdio: 'inherit', cwd: process.cwd(), timeout: 30000, }); } catch (err) { console.error('❌ Analysis failed'); process.exit(1); } // Step 3 — Tokenize console.log('\n━━━ STEP 3/4 — TOKENIZATION ━━━'); try { execSync(`npx tsx scripts/tokenize.ts "${domain}"`, { stdio: 'inherit', cwd: process.cwd(), timeout: 30000, }); } catch (err) { console.error('❌ Tokenization failed'); process.exit(1); } // Step 4 — Generate DESIGN.md console.log('\n━━━ STEP 4/5 — DESIGN.MD GENERATION ━━━\n'); try { execSync(`npx tsx scripts/generate-design-md.ts "${domain}"`, { stdio: 'inherit', cwd: process.cwd(), timeout: 30000, }); } catch (err) { console.error('⚠️ DESIGN.md generation failed (non-blocking)'); } // Step 5 — Generate showcase HTML console.log('\n━━━ STEP 5/5 — SHOWCASE ━━━\n'); try { execSync(`npx tsx scripts/generate-showcase.ts "${domain}"`, { stdio: 'inherit', cwd: process.cwd(), timeout: 30000, }); } catch (err) { console.error('⚠️ Showcase generation failed (non-blocking)'); } const duration = ((Date.now() - startTime) / 1000).toFixed(1); console.log('\n╔══════════════════════════════════════════════════╗'); console.log('║ PIPELINE COMPLETE ║'); console.log('╚══════════════════════════════════════════════════╝'); console.log(`\n⏱️ Duration: ${duration}s`); console.log(`📁 Output: extractions/${domain}/`); console.log(' ├── screenshots/ — Playwright captures'); console.log(' ├── raw-css.json — Computed styles brut'); console.log(' ├── extraction-summary.json'); console.log(' ├── layout-analysis.md — Structure analysée'); console.log(' ├── tokens.json — Design tokens normalisés'); console.log(' ├── DESIGN.md — Narratif LLM-optimisé (9 sections)'); console.log(' └── showcase/index.html — Page de présentation visuelle'); console.log('\n📋 Next steps:'); console.log(' 1. Review tokens.json and layout-analysis.md'); console.log(' 2. Use tokens to generate faithful code (R16 pipeline)'); console.log(' 3. Run: npm run compare -- ' + domain);