extraktor 1.0.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.
@@ -0,0 +1,3261 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GenomeEngine,
4
+ SiteCloner
5
+ } from "../chunk-VLLFGYUN.js";
6
+ import {
7
+ Orchestrator,
8
+ PlaywrightDriver,
9
+ getDefaultConfig,
10
+ loadConfig
11
+ } from "../chunk-PHMSK7VD.js";
12
+ import {
13
+ createLogger
14
+ } from "../chunk-5IH5TLAQ.js";
15
+
16
+ // src/cli/index.ts
17
+ import { Command } from "commander";
18
+ import ora from "ora";
19
+ import chalk from "chalk";
20
+ import path3 from "path";
21
+ import os from "os";
22
+
23
+ // src/cloner/transformer.ts
24
+ import fs from "fs-extra";
25
+ import path from "path";
26
+
27
+ // src/generators/ai-component.ts
28
+ var SYSTEM_PROMPT = `You are an expert React/TypeScript developer specializing in converting raw HTML into clean, modern React components.
29
+
30
+ Your task is to transform messy HTML (often from Framer, Webflow, or other site builders) into clean, maintainable React components.
31
+
32
+ RULES:
33
+ 1. Use Tailwind CSS classes instead of inline styles
34
+ 2. Use semantic HTML elements (header, nav, main, section, footer, article, etc.)
35
+ 3. Use framer-motion for animations (fade-in, slide-up, stagger effects)
36
+ 4. Create TypeScript interfaces for props
37
+ 5. Make content editable via props (text, images, links)
38
+ 6. Remove all auto-generated class names (framer-*, w-*, etc.)
39
+ 7. Keep the visual structure but simplify the DOM
40
+ 8. Use CSS variables for colors from the provided design tokens
41
+ 9. NEVER use dangerouslySetInnerHTML
42
+ 10. Export the component as both named and default export
43
+
44
+ OUTPUT FORMAT:
45
+ Return ONLY the TypeScript/React code. No explanations, no markdown code blocks.
46
+ Start directly with the imports.`;
47
+ function buildUserPrompt(options) {
48
+ const { componentName, html, tokens, includeAnimations = true } = options;
49
+ const tokenSummary = `
50
+ DESIGN TOKENS:
51
+ Colors: ${tokens.colors.map((c) => `${c.name}: ${c.value}`).join(", ")}
52
+ Fonts: ${tokens.fonts.map((f) => `${f.name}: ${f.value}`).join(", ")}
53
+ Spacing: ${tokens.spacing.map((s) => `${s.name}: ${s.value}`).join(", ")}
54
+ ${tokens.shadows ? `Shadows: ${tokens.shadows.map((s) => `${s.name}: ${s.value}`).join(", ")}` : ""}
55
+ ${tokens.borderRadius ? `Border Radius: ${tokens.borderRadius.map((r) => `${r.name}: ${r.value}`).join(", ")}` : ""}
56
+ `;
57
+ return `Convert this HTML into a clean React component named "${componentName}".
58
+
59
+ ${tokenSummary}
60
+
61
+ ${includeAnimations ? "Include framer-motion animations for entrance effects (fadeIn, slideUp)." : "No animations needed."}
62
+
63
+ HTML TO CONVERT:
64
+ \`\`\`html
65
+ ${html}
66
+ \`\`\`
67
+
68
+ Generate a clean TypeScript React component with:
69
+ - Tailwind CSS classes
70
+ - TypeScript props interface
71
+ - Semantic HTML structure
72
+ - ${includeAnimations ? "framer-motion animations" : "No animations"}
73
+ - Props for all editable content (titles, descriptions, images, buttons)`;
74
+ }
75
+ var AIComponentGenerator = class {
76
+ apiKey;
77
+ model;
78
+ maxRetries;
79
+ constructor(options) {
80
+ this.apiKey = options?.apiKey || process.env.ANTHROPIC_API_KEY || "";
81
+ this.model = options?.model || "claude-sonnet-4-20250514";
82
+ this.maxRetries = options?.maxRetries || 2;
83
+ if (!this.apiKey) {
84
+ console.warn("Warning: ANTHROPIC_API_KEY not set. AI component generation will fail.");
85
+ }
86
+ }
87
+ /**
88
+ * Check if AI generation is available
89
+ */
90
+ isAvailable() {
91
+ return !!this.apiKey;
92
+ }
93
+ /**
94
+ * Generate a React component from HTML using Claude API
95
+ */
96
+ async generate(options) {
97
+ if (!this.apiKey) {
98
+ return {
99
+ code: this.fallbackComponent(options.componentName, options.html),
100
+ componentName: options.componentName,
101
+ imports: [],
102
+ props: [],
103
+ success: false,
104
+ error: "ANTHROPIC_API_KEY not set"
105
+ };
106
+ }
107
+ const userPrompt = buildUserPrompt(options);
108
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
109
+ try {
110
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ "x-api-key": this.apiKey,
115
+ "anthropic-version": "2023-06-01"
116
+ },
117
+ body: JSON.stringify({
118
+ model: this.model,
119
+ max_tokens: 4096,
120
+ system: SYSTEM_PROMPT,
121
+ messages: [
122
+ {
123
+ role: "user",
124
+ content: userPrompt
125
+ }
126
+ ]
127
+ })
128
+ });
129
+ if (!response.ok) {
130
+ const errorText = await response.text();
131
+ throw new Error(`API error ${response.status}: ${errorText}`);
132
+ }
133
+ const data = await response.json();
134
+ const content = data.content?.[0]?.text || "";
135
+ let code = content.replace(/^```(?:tsx?|typescript|javascript)?\n?/gm, "").replace(/```$/gm, "").trim();
136
+ const imports = this.extractImports(code);
137
+ const props = this.extractProps(code);
138
+ return {
139
+ code,
140
+ componentName: options.componentName,
141
+ imports,
142
+ props,
143
+ success: true
144
+ };
145
+ } catch (error) {
146
+ console.error(`AI generation attempt ${attempt + 1} failed:`, error);
147
+ if (attempt === this.maxRetries) {
148
+ return {
149
+ code: this.fallbackComponent(options.componentName, options.html),
150
+ componentName: options.componentName,
151
+ imports: [],
152
+ props: [],
153
+ success: false,
154
+ error: error instanceof Error ? error.message : String(error)
155
+ };
156
+ }
157
+ await new Promise((resolve) => setTimeout(resolve, 1e3 * (attempt + 1)));
158
+ }
159
+ }
160
+ return {
161
+ code: this.fallbackComponent(options.componentName, options.html),
162
+ componentName: options.componentName,
163
+ imports: [],
164
+ props: [],
165
+ success: false,
166
+ error: "Unknown error"
167
+ };
168
+ }
169
+ /**
170
+ * Generate multiple components in batch
171
+ */
172
+ async generateBatch(components, tokens, options) {
173
+ const results = [];
174
+ for (let i = 0; i < components.length; i++) {
175
+ const comp = components[i];
176
+ options?.onProgress?.(i + 1, components.length);
177
+ const result = await this.generate({
178
+ componentName: comp.name,
179
+ html: comp.html,
180
+ tokens,
181
+ includeAnimations: options?.includeAnimations ?? true
182
+ });
183
+ results.push(result);
184
+ if (i < components.length - 1) {
185
+ await new Promise((resolve) => setTimeout(resolve, 500));
186
+ }
187
+ }
188
+ return results;
189
+ }
190
+ /**
191
+ * Fallback component when AI is not available
192
+ */
193
+ fallbackComponent(name, html) {
194
+ const escapedHtml = html.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
195
+ return `'use client';
196
+
197
+ import React from 'react';
198
+
199
+ interface ${name}Props {
200
+ className?: string;
201
+ }
202
+
203
+ // NOTE: This is a fallback component using dangerouslySetInnerHTML
204
+ // Run with --ai flag and ANTHROPIC_API_KEY to generate clean components
205
+ const htmlContent = \`${escapedHtml}\`;
206
+
207
+ export function ${name}({ className }: ${name}Props) {
208
+ return (
209
+ <div
210
+ className={className}
211
+ dangerouslySetInnerHTML={{ __html: htmlContent }}
212
+ />
213
+ );
214
+ }
215
+
216
+ export default ${name};
217
+ `;
218
+ }
219
+ /**
220
+ * Extract import statements from generated code
221
+ */
222
+ extractImports(code) {
223
+ const importRegex = /^import\s+.*?from\s+['"][^'"]+['"];?$/gm;
224
+ const matches = code.match(importRegex) || [];
225
+ return matches;
226
+ }
227
+ /**
228
+ * Extract prop names from generated code
229
+ */
230
+ extractProps(code) {
231
+ const interfaceMatch = code.match(/interface\s+\w+Props\s*\{([^}]+)\}/);
232
+ if (!interfaceMatch) return [];
233
+ const propsBlock = interfaceMatch[1];
234
+ const propRegex = /(\w+)\s*[?]?\s*:/g;
235
+ const props = [];
236
+ let match;
237
+ while ((match = propRegex.exec(propsBlock)) !== null) {
238
+ props.push(match[1]);
239
+ }
240
+ return props;
241
+ }
242
+ };
243
+
244
+ // src/cloner/transformer.ts
245
+ var Transformer = class {
246
+ colors = /* @__PURE__ */ new Map();
247
+ fonts = /* @__PURE__ */ new Set();
248
+ spacings = /* @__PURE__ */ new Set();
249
+ shadows = /* @__PURE__ */ new Set();
250
+ borderRadii = /* @__PURE__ */ new Set();
251
+ async transform(options) {
252
+ const inputDir = path.resolve(options.inputDir);
253
+ const outputDir = options.outputDir ? path.resolve(options.outputDir) : inputDir;
254
+ const log = options.onProgress || console.log;
255
+ const htmlPath = path.join(inputDir, "public/page-content.html");
256
+ if (!await fs.pathExists(htmlPath)) {
257
+ throw new Error(`No page-content.html found in ${inputDir}. Run 'extraktor clone' first.`);
258
+ }
259
+ const html = await fs.readFile(htmlPath, "utf-8");
260
+ const result = {
261
+ components: [],
262
+ tokensFile: null,
263
+ tailwindConfig: null,
264
+ aiGenerated: false,
265
+ errors: []
266
+ };
267
+ if (options.tokens || options.tailwind || options.useAI) {
268
+ log("Extracting design tokens...");
269
+ await this.extractTokens(html, inputDir);
270
+ }
271
+ if (options.tokens) {
272
+ const tokensPath = path.join(outputDir, "src/styles/tokens.css");
273
+ await this.generateTokensFile(tokensPath);
274
+ result.tokensFile = tokensPath;
275
+ log(`Generated tokens: ${tokensPath}`);
276
+ }
277
+ if (options.tailwind) {
278
+ const tailwindPath = path.join(outputDir, "tailwind.config.js");
279
+ await this.generateTailwindConfig(tailwindPath);
280
+ result.tailwindConfig = tailwindPath;
281
+ log(`Generated Tailwind config: ${tailwindPath}`);
282
+ }
283
+ if (options.components) {
284
+ const componentsDir = path.join(outputDir, "src/components/extracted");
285
+ await fs.ensureDir(componentsDir);
286
+ const sections = this.splitIntoSections(html);
287
+ log(`Found ${sections.length} sections to convert`);
288
+ const useAI = options.useAI || process.env.AI_ENABLED === "true";
289
+ const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
290
+ if (useAI && apiKey) {
291
+ log("Using AI-powered component generation...");
292
+ result.aiGenerated = true;
293
+ const aiGenerator = new AIComponentGenerator({ apiKey });
294
+ const tokens = this.getDesignTokens();
295
+ for (let i = 0; i < sections.length; i++) {
296
+ const section = sections[i];
297
+ log(`[${i + 1}/${sections.length}] Generating ${section.name} with AI...`);
298
+ try {
299
+ const aiResult = await aiGenerator.generate({
300
+ componentName: section.name,
301
+ html: section.html,
302
+ tokens,
303
+ includeAnimations: true
304
+ });
305
+ if (aiResult.success) {
306
+ const compPath = path.join(componentsDir, `${section.name}.tsx`);
307
+ await fs.writeFile(compPath, aiResult.code);
308
+ result.components.push(section.name);
309
+ log(`\u2713 ${section.name} generated successfully`);
310
+ } else {
311
+ log(`\u26A0 AI failed for ${section.name}, using fallback: ${aiResult.error}`);
312
+ result.errors.push(`${section.name}: ${aiResult.error}`);
313
+ const compPath = path.join(componentsDir, `${section.name}.tsx`);
314
+ await fs.writeFile(compPath, this.htmlToComponentFallback(section.name, section.html));
315
+ result.components.push(section.name);
316
+ }
317
+ } catch (error) {
318
+ log(`\u2717 Error generating ${section.name}: ${error}`);
319
+ result.errors.push(`${section.name}: ${error}`);
320
+ const compPath = path.join(componentsDir, `${section.name}.tsx`);
321
+ await fs.writeFile(compPath, this.htmlToComponentFallback(section.name, section.html));
322
+ result.components.push(section.name);
323
+ }
324
+ if (i < sections.length - 1) {
325
+ await new Promise((resolve) => setTimeout(resolve, 500));
326
+ }
327
+ }
328
+ } else {
329
+ if (useAI && !apiKey) {
330
+ log("\u26A0 AI mode requested but ANTHROPIC_API_KEY not set. Using fallback.");
331
+ result.errors.push("ANTHROPIC_API_KEY not set");
332
+ }
333
+ log("Generating components (fallback mode)...");
334
+ for (const section of sections) {
335
+ const compPath = path.join(componentsDir, `${section.name}.tsx`);
336
+ await fs.writeFile(compPath, this.htmlToComponentFallback(section.name, section.html));
337
+ result.components.push(section.name);
338
+ }
339
+ }
340
+ if (result.components.length > 0) {
341
+ try {
342
+ await this.generateComponentIndex(componentsDir, result.components);
343
+ log(`Generated component index with ${result.components.length} components`);
344
+ } catch (err) {
345
+ log(`\u26A0 Could not generate component index: ${err}`);
346
+ result.errors.push(`Component index: ${err}`);
347
+ }
348
+ }
349
+ if (result.aiGenerated && result.components.length > 0) {
350
+ try {
351
+ await this.generatePageFile(outputDir, result.components);
352
+ log("Generated page.tsx with component imports");
353
+ } catch (err) {
354
+ log(`\u26A0 Could not generate page file: ${err}`);
355
+ result.errors.push(`Page file: ${err}`);
356
+ }
357
+ }
358
+ }
359
+ return result;
360
+ }
361
+ /**
362
+ * Get extracted tokens in format suitable for AI generator
363
+ */
364
+ getDesignTokens() {
365
+ const colors = [];
366
+ this.colors.forEach((name, value) => {
367
+ colors.push({ name, value });
368
+ });
369
+ const fonts = [];
370
+ let fontIndex = 0;
371
+ this.fonts.forEach((font) => {
372
+ const name = fontIndex === 0 ? "primary" : fontIndex === 1 ? "secondary" : `font-${fontIndex}`;
373
+ fonts.push({ name, value: font });
374
+ fontIndex++;
375
+ });
376
+ const spacing = [];
377
+ const spacingArray = Array.from(this.spacings).sort((a, b) => parseFloat(a) - parseFloat(b));
378
+ spacingArray.forEach((value, i) => {
379
+ spacing.push({ name: `spacing-${i + 1}`, value });
380
+ });
381
+ const shadows = [];
382
+ let shadowIndex = 0;
383
+ this.shadows.forEach((shadow) => {
384
+ shadows.push({ name: `shadow-${shadowIndex++}`, value: shadow });
385
+ });
386
+ const borderRadius = [];
387
+ let radiusIndex = 0;
388
+ this.borderRadii.forEach((radius) => {
389
+ borderRadius.push({ name: `radius-${radiusIndex++}`, value: radius });
390
+ });
391
+ return { colors, fonts, spacing, shadows, borderRadius };
392
+ }
393
+ /**
394
+ * Extract design tokens from HTML and stylesheets
395
+ */
396
+ async extractTokens(html, inputDir) {
397
+ const colorRegex = /(#[0-9A-Fa-f]{3,8}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\))/g;
398
+ const colorMatches = html.match(colorRegex) || [];
399
+ colorMatches.forEach((color) => {
400
+ const normalized = this.normalizeColor(color);
401
+ if (!this.colors.has(normalized)) {
402
+ const name = this.generateColorName(normalized, this.colors.size);
403
+ this.colors.set(normalized, name);
404
+ }
405
+ });
406
+ const fontRegex = /font-family:\s*([^;}"]+)/g;
407
+ let fontMatch;
408
+ while ((fontMatch = fontRegex.exec(html)) !== null) {
409
+ const font = fontMatch[1].trim().replace(/['"]/g, "").split(",")[0].trim();
410
+ if (font && !font.includes("system") && !font.includes("sans-serif")) {
411
+ this.fonts.add(font);
412
+ }
413
+ }
414
+ const spacingRegex = /(?:padding|margin|gap):\s*(\d+(?:\.\d+)?(?:px|rem|em))/g;
415
+ let spacingMatch;
416
+ while ((spacingMatch = spacingRegex.exec(html)) !== null) {
417
+ this.spacings.add(spacingMatch[1]);
418
+ }
419
+ const shadowRegex = /box-shadow:\s*([^;}"]+)/g;
420
+ let shadowMatch;
421
+ while ((shadowMatch = shadowRegex.exec(html)) !== null) {
422
+ const shadow = shadowMatch[1].trim();
423
+ if (shadow && shadow !== "none") {
424
+ this.shadows.add(shadow);
425
+ }
426
+ }
427
+ const radiusRegex = /border-radius:\s*(\d+(?:\.\d+)?(?:px|rem|em|%))/g;
428
+ let radiusMatch;
429
+ while ((radiusMatch = radiusRegex.exec(html)) !== null) {
430
+ this.borderRadii.add(radiusMatch[1]);
431
+ }
432
+ const externalCssPath = path.join(inputDir, "src/styles/external.css");
433
+ if (await fs.pathExists(externalCssPath)) {
434
+ const css = await fs.readFile(externalCssPath, "utf-8");
435
+ const cssColors = css.match(colorRegex) || [];
436
+ cssColors.forEach((color) => {
437
+ const normalized = this.normalizeColor(color);
438
+ if (!this.colors.has(normalized)) {
439
+ const name = this.generateColorName(normalized, this.colors.size);
440
+ this.colors.set(normalized, name);
441
+ }
442
+ });
443
+ }
444
+ }
445
+ /**
446
+ * Generate tokens.css file with CSS variables
447
+ */
448
+ async generateTokensFile(outputPath) {
449
+ let css = `/* Design Tokens - Auto-generated by extraktor */
450
+ /* Edit these values to customize the theme */
451
+
452
+ :root {
453
+ /* Colors */
454
+ `;
455
+ this.colors.forEach((name, value) => {
456
+ css += ` --color-${name}: ${value};
457
+ `;
458
+ });
459
+ css += `
460
+ /* Fonts */
461
+ `;
462
+ let fontIndex = 0;
463
+ this.fonts.forEach((font) => {
464
+ const varName = fontIndex === 0 ? "primary" : fontIndex === 1 ? "secondary" : `font-${fontIndex}`;
465
+ css += ` --font-${varName}: '${font}', system-ui, sans-serif;
466
+ `;
467
+ fontIndex++;
468
+ });
469
+ css += `
470
+ /* Spacing */
471
+ `;
472
+ const spacingArray = Array.from(this.spacings).sort((a, b) => parseFloat(a) - parseFloat(b));
473
+ spacingArray.forEach((spacing, i) => {
474
+ css += ` --spacing-${i + 1}: ${spacing};
475
+ `;
476
+ });
477
+ if (this.shadows.size > 0) {
478
+ css += `
479
+ /* Shadows */
480
+ `;
481
+ let shadowIndex = 0;
482
+ this.shadows.forEach((shadow) => {
483
+ css += ` --shadow-${shadowIndex++}: ${shadow};
484
+ `;
485
+ });
486
+ }
487
+ if (this.borderRadii.size > 0) {
488
+ css += `
489
+ /* Border Radius */
490
+ `;
491
+ let radiusIndex = 0;
492
+ this.borderRadii.forEach((radius) => {
493
+ css += ` --radius-${radiusIndex++}: ${radius};
494
+ `;
495
+ });
496
+ }
497
+ css += `}
498
+ `;
499
+ await fs.ensureDir(path.dirname(outputPath));
500
+ await fs.writeFile(outputPath, css);
501
+ }
502
+ /**
503
+ * Generate Tailwind config with extracted colors
504
+ */
505
+ async generateTailwindConfig(outputPath) {
506
+ const colorEntries = [];
507
+ this.colors.forEach((name, value) => {
508
+ colorEntries.push(` '${name}': '${value}'`);
509
+ });
510
+ const fontEntries = [];
511
+ let fontIndex = 0;
512
+ this.fonts.forEach((font) => {
513
+ const varName = fontIndex === 0 ? "primary" : fontIndex === 1 ? "secondary" : `custom-${fontIndex}`;
514
+ fontEntries.push(` '${varName}': ['${font}', 'system-ui', 'sans-serif']`);
515
+ fontIndex++;
516
+ });
517
+ const config = `/** @type {import('tailwindcss').Config} */
518
+ module.exports = {
519
+ content: [
520
+ './src/**/*.{js,ts,jsx,tsx,mdx}',
521
+ ],
522
+ theme: {
523
+ extend: {
524
+ colors: {
525
+ ${colorEntries.join(",\n")}
526
+ },
527
+ fontFamily: {
528
+ ${fontEntries.join(",\n")}
529
+ },
530
+ },
531
+ },
532
+ plugins: [],
533
+ };
534
+ `;
535
+ await fs.writeFile(outputPath, config);
536
+ }
537
+ /**
538
+ * Split HTML into logical sections (for both AI and fallback mode)
539
+ */
540
+ splitIntoSections(html) {
541
+ const sections = [];
542
+ const foundNames = /* @__PURE__ */ new Set();
543
+ const patterns = [
544
+ { pattern: /<header[^>]*>[\s\S]*?<\/header>/gi, name: "Header" },
545
+ { pattern: /<nav[^>]*>[\s\S]*?<\/nav>/gi, name: "Navigation" },
546
+ { pattern: /<footer[^>]*>[\s\S]*?<\/footer>/gi, name: "Footer" },
547
+ { pattern: /<section[^>]*class="[^"]*hero[^"]*"[^>]*>[\s\S]*?<\/section>/gi, name: "Hero" },
548
+ { pattern: /<section[^>]*class="[^"]*feature[^"]*"[^>]*>[\s\S]*?<\/section>/gi, name: "Features" },
549
+ { pattern: /<section[^>]*class="[^"]*pricing[^"]*"[^>]*>[\s\S]*?<\/section>/gi, name: "Pricing" },
550
+ { pattern: /<section[^>]*class="[^"]*testimonial[^"]*"[^>]*>[\s\S]*?<\/section>/gi, name: "Testimonials" },
551
+ { pattern: /<section[^>]*class="[^"]*faq[^"]*"[^>]*>[\s\S]*?<\/section>/gi, name: "FAQ" },
552
+ { pattern: /<section[^>]*class="[^"]*cta[^"]*"[^>]*>[\s\S]*?<\/section>/gi, name: "CTA" }
553
+ ];
554
+ const framerNameRegex = /<div[^>]*data-framer-name="([^"]+)"[^>]*>([\s\S]*?)<\/div>(?=\s*<div[^>]*data-framer-name|<\/body|$)/gi;
555
+ let framerMatch;
556
+ while ((framerMatch = framerNameRegex.exec(html)) !== null) {
557
+ const name = this.sanitizeComponentName(framerMatch[1]);
558
+ if (!foundNames.has(name) && framerMatch[0].length < 5e4) {
559
+ sections.push({ name, html: framerMatch[0] });
560
+ foundNames.add(name);
561
+ }
562
+ }
563
+ patterns.forEach(({ pattern, name }) => {
564
+ if (foundNames.has(name)) return;
565
+ const matches = html.match(pattern);
566
+ if (matches && matches.length > 0 && matches[0].length < 5e4) {
567
+ sections.push({ name, html: matches[0] });
568
+ foundNames.add(name);
569
+ }
570
+ });
571
+ if (sections.length < 3) {
572
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
573
+ if (bodyMatch) {
574
+ const bodyContent = bodyMatch[1];
575
+ const divPattern = /<div[^>]*class="[^"]*(?:section|container|wrapper|block|framer-)[^"]*"[^>]*>[\s\S]*?<\/div>(?=\s*<div|<\/body|$)/gi;
576
+ const divMatches = bodyContent.match(divPattern);
577
+ if (divMatches) {
578
+ divMatches.slice(0, 10).forEach((div, index) => {
579
+ const name = `Section${index + 1}`;
580
+ if (!foundNames.has(name) && div.length < 5e4) {
581
+ sections.push({ name, html: div });
582
+ foundNames.add(name);
583
+ }
584
+ });
585
+ }
586
+ }
587
+ }
588
+ if (sections.length === 0) {
589
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
590
+ const content = bodyMatch ? bodyMatch[1] : html;
591
+ sections.push({ name: "Main", html: content.slice(0, 5e4) });
592
+ }
593
+ return sections;
594
+ }
595
+ /**
596
+ * Legacy method for backward compatibility
597
+ */
598
+ splitIntoComponents(html) {
599
+ const sections = this.splitIntoSections(html);
600
+ return sections.map((section) => ({
601
+ name: section.name,
602
+ html: section.html,
603
+ code: this.htmlToComponentFallback(section.name, section.html)
604
+ }));
605
+ }
606
+ /**
607
+ * Sanitize a string to be a valid component name
608
+ */
609
+ sanitizeComponentName(name) {
610
+ return name.replace(/[^a-zA-Z0-9]/g, "").replace(/^\d+/, "") || "Component";
611
+ }
612
+ /**
613
+ * Fallback: Convert HTML to a React component using dangerouslySetInnerHTML
614
+ * Used when AI is not available
615
+ */
616
+ htmlToComponentFallback(name, html) {
617
+ const escapedHtml = html.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
618
+ return `'use client';
619
+
620
+ import React from 'react';
621
+
622
+ interface ${name}Props {
623
+ className?: string;
624
+ }
625
+
626
+ // NOTE: This is a fallback component using dangerouslySetInnerHTML
627
+ // Run 'extraktor transform --ai' with ANTHROPIC_API_KEY to generate clean components
628
+ const htmlContent = \`${escapedHtml}\`;
629
+
630
+ export function ${name}({ className }: ${name}Props) {
631
+ return (
632
+ <div
633
+ className={className}
634
+ dangerouslySetInnerHTML={{ __html: htmlContent }}
635
+ />
636
+ );
637
+ }
638
+
639
+ export default ${name};
640
+ `;
641
+ }
642
+ /**
643
+ * Generate a page.tsx that properly imports and uses the components
644
+ */
645
+ async generatePageFile(outputDir, componentNames) {
646
+ const imports = componentNames.map((name) => `import { ${name} } from '@/components/extracted/${name}';`).join("\n");
647
+ const components = componentNames.map((name) => ` <${name} />`).join("\n");
648
+ const pageContent = `'use client';
649
+
650
+ ${imports}
651
+
652
+ export default function Home() {
653
+ return (
654
+ <main className="min-h-screen">
655
+ ${components}
656
+ </main>
657
+ );
658
+ }
659
+ `;
660
+ const pagePath = path.join(outputDir, "src/app/page-components.tsx");
661
+ await fs.writeFile(pagePath, pageContent);
662
+ }
663
+ /**
664
+ * Generate component index file
665
+ */
666
+ async generateComponentIndex(componentsDir, componentNames) {
667
+ const imports = componentNames.map((name) => `export { ${name} } from './${name}';`).join("\n");
668
+ const indexContent = `/**
669
+ * Extracted Components Index
670
+ * Generated by extraktor transform
671
+ *
672
+ * These components are extracted from the cloned HTML.
673
+ * ${componentNames.length > 0 ? "Import and use them in your pages." : ""}
674
+ */
675
+
676
+ ${imports}
677
+ `;
678
+ await fs.writeFile(path.join(componentsDir, "index.ts"), indexContent);
679
+ }
680
+ /**
681
+ * Normalize color value
682
+ */
683
+ normalizeColor(color) {
684
+ return color.toLowerCase().replace(/\s+/g, "");
685
+ }
686
+ /**
687
+ * Generate semantic color name
688
+ */
689
+ generateColorName(color, index) {
690
+ const lower = color.toLowerCase();
691
+ if (lower.includes("fff") || lower === "#ffffff" || lower === "rgb(255,255,255)") return "white";
692
+ if (lower.includes("000") || lower === "#000000" || lower === "rgb(0,0,0)") return "black";
693
+ if (lower.match(/#[0-9a-f]{3}0{3}|rgba?\([^)]*,\s*0\.?0*\)/)) return `transparent-${index}`;
694
+ const prefixes = ["primary", "secondary", "accent", "muted", "background", "foreground", "border"];
695
+ if (index < prefixes.length) return prefixes[index];
696
+ return `color-${index}`;
697
+ }
698
+ };
699
+
700
+ // src/extractor/design-extractor.ts
701
+ import Anthropic from "@anthropic-ai/sdk";
702
+ import fs2 from "fs-extra";
703
+ import path2 from "path";
704
+ import { chromium } from "playwright";
705
+ var DesignExtractor = class {
706
+ browser = null;
707
+ page = null;
708
+ anthropic = null;
709
+ constructor(apiKey) {
710
+ const key = apiKey || process.env.ANTHROPIC_API_KEY;
711
+ if (key) {
712
+ this.anthropic = new Anthropic({ apiKey: key });
713
+ }
714
+ }
715
+ async extract(url, outputDir) {
716
+ this.browser = await chromium.launch({ headless: true });
717
+ const context = await this.browser.newContext({
718
+ viewport: { width: 1920, height: 1080 },
719
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
720
+ });
721
+ this.page = await context.newPage();
722
+ try {
723
+ await this.page.goto(url, { waitUntil: "networkidle", timeout: 6e4 });
724
+ await this.page.waitForTimeout(3e3);
725
+ await this.autoScroll();
726
+ const rawExtraction = await this.extractEverything();
727
+ const organized = await this.organizeWithAI(url, rawExtraction);
728
+ await this.generateCompleteOutput(organized, outputDir, rawExtraction);
729
+ return organized;
730
+ } finally {
731
+ await this.browser.close();
732
+ }
733
+ }
734
+ async autoScroll() {
735
+ await this.page.evaluate(async () => {
736
+ await new Promise((resolve) => {
737
+ let totalHeight = 0;
738
+ const distance = 300;
739
+ const timer = setInterval(() => {
740
+ window.scrollBy(0, distance);
741
+ totalHeight += distance;
742
+ if (totalHeight >= document.body.scrollHeight * 2) {
743
+ clearInterval(timer);
744
+ window.scrollTo(0, 0);
745
+ resolve();
746
+ }
747
+ }, 50);
748
+ });
749
+ });
750
+ await this.page.waitForTimeout(2e3);
751
+ }
752
+ async extractEverything() {
753
+ return await this.page.evaluate(() => {
754
+ const rgbToHex = (rgb) => {
755
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
756
+ if (!match) return null;
757
+ const r = parseInt(match[1]);
758
+ const g = parseInt(match[2]);
759
+ const b = parseInt(match[3]);
760
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
761
+ };
762
+ const addToSet = (set, value) => {
763
+ if (value && value !== "none" && value !== "normal" && value !== "auto" && value !== "inherit" && value !== "initial" && value !== "0px" && value !== "0" && value !== "rgba(0, 0, 0, 0)" && value !== "transparent") {
764
+ set.add(value);
765
+ }
766
+ };
767
+ const colors = {
768
+ solid: /* @__PURE__ */ new Set(),
769
+ gradients: /* @__PURE__ */ new Set(),
770
+ rgba: /* @__PURE__ */ new Set()
771
+ };
772
+ const typography = {
773
+ fontFamilies: /* @__PURE__ */ new Set(),
774
+ fontSizes: /* @__PURE__ */ new Set(),
775
+ fontWeights: /* @__PURE__ */ new Set(),
776
+ lineHeights: /* @__PURE__ */ new Set(),
777
+ letterSpacing: /* @__PURE__ */ new Set(),
778
+ textTransforms: /* @__PURE__ */ new Set(),
779
+ textDecorations: /* @__PURE__ */ new Set()
780
+ };
781
+ const spacing = {
782
+ padding: /* @__PURE__ */ new Set(),
783
+ margin: /* @__PURE__ */ new Set(),
784
+ gap: /* @__PURE__ */ new Set(),
785
+ all: /* @__PURE__ */ new Set()
786
+ };
787
+ const borders = {
788
+ radius: /* @__PURE__ */ new Set(),
789
+ widths: /* @__PURE__ */ new Set(),
790
+ colors: /* @__PURE__ */ new Set(),
791
+ styles: /* @__PURE__ */ new Set()
792
+ };
793
+ const shadows = {
794
+ boxShadows: /* @__PURE__ */ new Set(),
795
+ textShadows: /* @__PURE__ */ new Set(),
796
+ dropShadows: /* @__PURE__ */ new Set()
797
+ };
798
+ const effects = {
799
+ backdropBlur: /* @__PURE__ */ new Set(),
800
+ backdropSaturate: /* @__PURE__ */ new Set(),
801
+ backdropBrightness: /* @__PURE__ */ new Set(),
802
+ opacity: /* @__PURE__ */ new Set(),
803
+ filters: /* @__PURE__ */ new Set()
804
+ };
805
+ const zIndexes = /* @__PURE__ */ new Set();
806
+ const transitions = {
807
+ properties: /* @__PURE__ */ new Set(),
808
+ durations: /* @__PURE__ */ new Set(),
809
+ timingFunctions: /* @__PURE__ */ new Set(),
810
+ delays: /* @__PURE__ */ new Set(),
811
+ full: /* @__PURE__ */ new Set()
812
+ };
813
+ const transforms = /* @__PURE__ */ new Set();
814
+ const layout = {
815
+ displays: /* @__PURE__ */ new Set(),
816
+ justifyContent: /* @__PURE__ */ new Set(),
817
+ alignItems: /* @__PURE__ */ new Set(),
818
+ flexDirection: /* @__PURE__ */ new Set(),
819
+ flexWrap: /* @__PURE__ */ new Set(),
820
+ gridTemplateColumns: /* @__PURE__ */ new Set(),
821
+ gridTemplateRows: /* @__PURE__ */ new Set(),
822
+ positions: /* @__PURE__ */ new Set(),
823
+ maxWidths: /* @__PURE__ */ new Set(),
824
+ widths: /* @__PURE__ */ new Set(),
825
+ heights: /* @__PURE__ */ new Set()
826
+ };
827
+ const cursors = /* @__PURE__ */ new Set();
828
+ const outlines = /* @__PURE__ */ new Set();
829
+ const buttonData = [];
830
+ const inputData = [];
831
+ const cardData = [];
832
+ const linkData = [];
833
+ const headingData = [];
834
+ const containerData = [];
835
+ const navItemData = [];
836
+ const badgeData = [];
837
+ const stylesheetContents = [];
838
+ const keyframeRules = [];
839
+ const mediaQueries = [];
840
+ for (const sheet of document.styleSheets) {
841
+ try {
842
+ if (sheet.cssRules) {
843
+ for (const rule of sheet.cssRules) {
844
+ stylesheetContents.push(rule.cssText);
845
+ if (rule instanceof CSSKeyframesRule) {
846
+ keyframeRules.push(rule.cssText);
847
+ }
848
+ if (rule instanceof CSSMediaRule) {
849
+ mediaQueries.push(rule.cssText);
850
+ }
851
+ }
852
+ }
853
+ } catch (e) {
854
+ }
855
+ }
856
+ const cssVariables = {};
857
+ const rootStyles = window.getComputedStyle(document.documentElement);
858
+ for (let i = 0; i < rootStyles.length; i++) {
859
+ const prop = rootStyles[i];
860
+ if (prop.startsWith("--")) {
861
+ cssVariables[prop] = rootStyles.getPropertyValue(prop).trim();
862
+ }
863
+ }
864
+ const allElements = document.querySelectorAll("*");
865
+ allElements.forEach((el) => {
866
+ const computed = window.getComputedStyle(el);
867
+ const rect = el.getBoundingClientRect();
868
+ ["color", "backgroundColor", "borderColor", "borderTopColor", "borderBottomColor", "borderLeftColor", "borderRightColor", "outlineColor", "caretColor", "accentColor"].forEach((prop) => {
869
+ const value = computed.getPropertyValue(prop.replace(/([A-Z])/g, "-$1").toLowerCase());
870
+ if (value) {
871
+ if (value.includes("gradient")) {
872
+ colors.gradients.add(value);
873
+ } else if (value.includes("rgba")) {
874
+ colors.rgba.add(value);
875
+ const hex = rgbToHex(value);
876
+ if (hex) colors.solid.add(hex);
877
+ } else if (value.includes("rgb")) {
878
+ const hex = rgbToHex(value);
879
+ if (hex) colors.solid.add(hex);
880
+ } else if (value.startsWith("#")) {
881
+ colors.solid.add(value.toUpperCase());
882
+ }
883
+ }
884
+ });
885
+ const bgImage = computed.backgroundImage;
886
+ if (bgImage && bgImage !== "none" && bgImage.includes("gradient")) {
887
+ colors.gradients.add(bgImage);
888
+ }
889
+ const fontFamily = computed.fontFamily;
890
+ if (fontFamily) {
891
+ fontFamily.split(",").forEach((f) => {
892
+ const cleaned = f.trim().replace(/['"]/g, "");
893
+ if (cleaned && !["system-ui", "-apple-system", "BlinkMacSystemFont", "sans-serif", "serif", "monospace", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans"].includes(cleaned)) {
894
+ typography.fontFamilies.add(cleaned);
895
+ }
896
+ });
897
+ }
898
+ addToSet(typography.fontSizes, computed.fontSize);
899
+ addToSet(typography.fontWeights, computed.fontWeight);
900
+ addToSet(typography.lineHeights, computed.lineHeight);
901
+ addToSet(typography.letterSpacing, computed.letterSpacing);
902
+ addToSet(typography.textTransforms, computed.textTransform);
903
+ addToSet(typography.textDecorations, computed.textDecoration);
904
+ ["padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"].forEach((prop) => {
905
+ const value = computed.getPropertyValue(prop.replace(/([A-Z])/g, "-$1").toLowerCase());
906
+ addToSet(spacing.padding, value);
907
+ addToSet(spacing.all, value);
908
+ });
909
+ ["margin", "marginTop", "marginRight", "marginBottom", "marginLeft"].forEach((prop) => {
910
+ const value = computed.getPropertyValue(prop.replace(/([A-Z])/g, "-$1").toLowerCase());
911
+ addToSet(spacing.margin, value);
912
+ addToSet(spacing.all, value);
913
+ });
914
+ addToSet(spacing.gap, computed.gap);
915
+ addToSet(spacing.gap, computed.rowGap);
916
+ addToSet(spacing.gap, computed.columnGap);
917
+ addToSet(spacing.all, computed.gap);
918
+ addToSet(borders.radius, computed.borderRadius);
919
+ ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"].forEach((prop) => {
920
+ addToSet(borders.radius, computed.getPropertyValue(prop.replace(/([A-Z])/g, "-$1").toLowerCase()));
921
+ });
922
+ addToSet(borders.widths, computed.borderWidth);
923
+ addToSet(borders.widths, computed.borderTopWidth);
924
+ addToSet(borders.styles, computed.borderStyle);
925
+ addToSet(shadows.boxShadows, computed.boxShadow);
926
+ addToSet(shadows.textShadows, computed.textShadow);
927
+ const filter = computed.filter;
928
+ if (filter && filter.includes("drop-shadow")) {
929
+ shadows.dropShadows.add(filter);
930
+ }
931
+ const backdrop = computed.backdropFilter;
932
+ if (backdrop && backdrop !== "none") {
933
+ effects.filters.add(backdrop);
934
+ if (backdrop.includes("blur")) {
935
+ const blurMatch = backdrop.match(/blur\(([^)]+)\)/);
936
+ if (blurMatch) effects.backdropBlur.add(blurMatch[1]);
937
+ }
938
+ if (backdrop.includes("saturate")) {
939
+ const satMatch = backdrop.match(/saturate\(([^)]+)\)/);
940
+ if (satMatch) effects.backdropSaturate.add(satMatch[1]);
941
+ }
942
+ if (backdrop.includes("brightness")) {
943
+ const brightMatch = backdrop.match(/brightness\(([^)]+)\)/);
944
+ if (brightMatch) effects.backdropBrightness.add(brightMatch[1]);
945
+ }
946
+ }
947
+ if (computed.opacity !== "1") {
948
+ addToSet(effects.opacity, computed.opacity);
949
+ }
950
+ if (filter && filter !== "none") {
951
+ effects.filters.add(filter);
952
+ }
953
+ if (computed.zIndex !== "auto") {
954
+ addToSet(zIndexes, computed.zIndex);
955
+ }
956
+ const transition = computed.transition;
957
+ if (transition && transition !== "all 0s ease 0s" && transition !== "none") {
958
+ transitions.full.add(transition);
959
+ addToSet(transitions.durations, computed.transitionDuration);
960
+ addToSet(transitions.timingFunctions, computed.transitionTimingFunction);
961
+ addToSet(transitions.delays, computed.transitionDelay);
962
+ addToSet(transitions.properties, computed.transitionProperty);
963
+ }
964
+ if (computed.transform !== "none") {
965
+ transforms.add(computed.transform);
966
+ }
967
+ addToSet(layout.displays, computed.display);
968
+ addToSet(layout.justifyContent, computed.justifyContent);
969
+ addToSet(layout.alignItems, computed.alignItems);
970
+ addToSet(layout.flexDirection, computed.flexDirection);
971
+ addToSet(layout.flexWrap, computed.flexWrap);
972
+ addToSet(layout.gridTemplateColumns, computed.gridTemplateColumns);
973
+ addToSet(layout.gridTemplateRows, computed.gridTemplateRows);
974
+ addToSet(layout.positions, computed.position);
975
+ if (rect.width > 0) addToSet(layout.widths, `${Math.round(rect.width)}px`);
976
+ if (rect.height > 0) addToSet(layout.heights, `${Math.round(rect.height)}px`);
977
+ addToSet(layout.maxWidths, computed.maxWidth);
978
+ addToSet(cursors, computed.cursor);
979
+ if (computed.outline !== "none") {
980
+ addToSet(outlines, computed.outline);
981
+ }
982
+ });
983
+ const extractFullStyles = (el) => {
984
+ const computed = window.getComputedStyle(el);
985
+ const rect = el.getBoundingClientRect();
986
+ let hoverStyles = null;
987
+ let activeStyles = null;
988
+ let focusStyles = null;
989
+ for (const sheet of document.styleSheets) {
990
+ try {
991
+ if (sheet.cssRules) {
992
+ for (const rule of sheet.cssRules) {
993
+ if (rule instanceof CSSStyleRule) {
994
+ const selector = rule.selectorText;
995
+ if (selector && el.matches(selector.replace(":hover", "").replace(":active", "").replace(":focus", ""))) {
996
+ if (selector.includes(":hover")) {
997
+ hoverStyles = {};
998
+ for (let i = 0; i < rule.style.length; i++) {
999
+ const prop = rule.style[i];
1000
+ hoverStyles[prop] = rule.style.getPropertyValue(prop);
1001
+ }
1002
+ }
1003
+ if (selector.includes(":active")) {
1004
+ activeStyles = {};
1005
+ for (let i = 0; i < rule.style.length; i++) {
1006
+ const prop = rule.style[i];
1007
+ activeStyles[prop] = rule.style.getPropertyValue(prop);
1008
+ }
1009
+ }
1010
+ if (selector.includes(":focus")) {
1011
+ focusStyles = {};
1012
+ for (let i = 0; i < rule.style.length; i++) {
1013
+ const prop = rule.style[i];
1014
+ focusStyles[prop] = rule.style.getPropertyValue(prop);
1015
+ }
1016
+ }
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ } catch (e) {
1022
+ }
1023
+ }
1024
+ const framerData = {};
1025
+ const dataAttrs = el.attributes;
1026
+ for (let i = 0; i < dataAttrs.length; i++) {
1027
+ const attr = dataAttrs[i];
1028
+ if (attr.name.startsWith("data-framer") || attr.name.includes("motion")) {
1029
+ framerData[attr.name] = attr.value;
1030
+ }
1031
+ }
1032
+ const styleAttr = el.getAttribute("style");
1033
+ return {
1034
+ tag: el.tagName.toLowerCase(),
1035
+ className: el.className,
1036
+ id: el.id,
1037
+ text: el.textContent?.trim().slice(0, 100),
1038
+ dimensions: { width: rect.width, height: rect.height },
1039
+ styles: {
1040
+ // Colors
1041
+ color: computed.color,
1042
+ backgroundColor: computed.backgroundColor,
1043
+ borderColor: computed.borderColor,
1044
+ // Typography
1045
+ fontFamily: computed.fontFamily,
1046
+ fontSize: computed.fontSize,
1047
+ fontWeight: computed.fontWeight,
1048
+ lineHeight: computed.lineHeight,
1049
+ letterSpacing: computed.letterSpacing,
1050
+ textAlign: computed.textAlign,
1051
+ textTransform: computed.textTransform,
1052
+ // Spacing
1053
+ padding: computed.padding,
1054
+ margin: computed.margin,
1055
+ gap: computed.gap,
1056
+ // Borders
1057
+ borderRadius: computed.borderRadius,
1058
+ borderWidth: computed.borderWidth,
1059
+ borderStyle: computed.borderStyle,
1060
+ // Shadows
1061
+ boxShadow: computed.boxShadow,
1062
+ textShadow: computed.textShadow,
1063
+ // Effects
1064
+ opacity: computed.opacity,
1065
+ backdropFilter: computed.backdropFilter,
1066
+ filter: computed.filter,
1067
+ // Transform
1068
+ transform: computed.transform,
1069
+ transformOrigin: computed.transformOrigin,
1070
+ // Transitions
1071
+ transition: computed.transition,
1072
+ transitionDuration: computed.transitionDuration,
1073
+ transitionTimingFunction: computed.transitionTimingFunction,
1074
+ // Layout
1075
+ display: computed.display,
1076
+ position: computed.position,
1077
+ justifyContent: computed.justifyContent,
1078
+ alignItems: computed.alignItems,
1079
+ flexDirection: computed.flexDirection,
1080
+ // Other
1081
+ cursor: computed.cursor,
1082
+ overflow: computed.overflow,
1083
+ zIndex: computed.zIndex,
1084
+ outline: computed.outline
1085
+ },
1086
+ states: {
1087
+ hover: hoverStyles,
1088
+ active: activeStyles,
1089
+ focus: focusStyles
1090
+ },
1091
+ framerMotion: framerData,
1092
+ inlineStyle: styleAttr
1093
+ };
1094
+ };
1095
+ document.querySelectorAll('button, a[class*="btn"], a[class*="button"], [role="button"], a[class*="cta"], a[class*="download"], [class*="Button"]').forEach((el, i) => {
1096
+ if (i < 50) buttonData.push(extractFullStyles(el));
1097
+ });
1098
+ document.querySelectorAll('input, textarea, select, [class*="input"], [class*="Input"], [class*="field"]').forEach((el, i) => {
1099
+ if (i < 30) inputData.push(extractFullStyles(el));
1100
+ });
1101
+ document.querySelectorAll('[class*="card"], [class*="Card"], article, [class*="feature"], [class*="Feature"], [class*="panel"], [class*="Panel"]').forEach((el, i) => {
1102
+ if (i < 30) cardData.push(extractFullStyles(el));
1103
+ });
1104
+ document.querySelectorAll('a:not([class*="btn"]):not([class*="button"])').forEach((el, i) => {
1105
+ if (i < 30) linkData.push(extractFullStyles(el));
1106
+ });
1107
+ document.querySelectorAll('h1, h2, h3, h4, h5, h6, [class*="heading"], [class*="Heading"], [class*="title"], [class*="Title"], [class*="headline"]').forEach((el, i) => {
1108
+ if (i < 30) headingData.push(extractFullStyles(el));
1109
+ });
1110
+ document.querySelectorAll('section, main, [class*="container"], [class*="Container"], [class*="wrapper"], [class*="Wrapper"], [class*="section"], [class*="Section"]').forEach((el, i) => {
1111
+ if (i < 20) containerData.push(extractFullStyles(el));
1112
+ });
1113
+ document.querySelectorAll('nav a, nav button, [class*="nav"] a, [class*="Nav"] a, [class*="menu"] a, [class*="Menu"] a').forEach((el, i) => {
1114
+ if (i < 30) navItemData.push(extractFullStyles(el));
1115
+ });
1116
+ document.querySelectorAll('[class*="badge"], [class*="Badge"], [class*="tag"], [class*="Tag"], [class*="chip"], [class*="Chip"]').forEach((el, i) => {
1117
+ if (i < 20) badgeData.push(extractFullStyles(el));
1118
+ });
1119
+ let selectionColor = "";
1120
+ let selectionBg = "";
1121
+ const selectionEl = document.querySelector("::selection");
1122
+ for (const sheet of document.styleSheets) {
1123
+ try {
1124
+ if (sheet.cssRules) {
1125
+ for (const rule of sheet.cssRules) {
1126
+ if (rule instanceof CSSStyleRule && rule.selectorText?.includes("::selection")) {
1127
+ selectionColor = rule.style.color || "";
1128
+ selectionBg = rule.style.backgroundColor || "";
1129
+ }
1130
+ }
1131
+ }
1132
+ } catch (e) {
1133
+ }
1134
+ }
1135
+ const breakpoints = {};
1136
+ mediaQueries.forEach((mq) => {
1137
+ const match = mq.match(/@media[^{]+\(min-width:\s*(\d+(?:\.\d+)?(?:px|em|rem))\)/);
1138
+ if (match) {
1139
+ breakpoints[match[1]] = mq;
1140
+ }
1141
+ const maxMatch = mq.match(/@media[^{]+\(max-width:\s*(\d+(?:\.\d+)?(?:px|em|rem))\)/);
1142
+ if (maxMatch) {
1143
+ breakpoints[`max-${maxMatch[1]}`] = mq;
1144
+ }
1145
+ });
1146
+ return {
1147
+ colors: {
1148
+ solid: Array.from(colors.solid),
1149
+ gradients: Array.from(colors.gradients),
1150
+ rgba: Array.from(colors.rgba)
1151
+ },
1152
+ typography: {
1153
+ fontFamilies: Array.from(typography.fontFamilies),
1154
+ fontSizes: Array.from(typography.fontSizes),
1155
+ fontWeights: Array.from(typography.fontWeights),
1156
+ lineHeights: Array.from(typography.lineHeights),
1157
+ letterSpacing: Array.from(typography.letterSpacing),
1158
+ textTransforms: Array.from(typography.textTransforms),
1159
+ textDecorations: Array.from(typography.textDecorations)
1160
+ },
1161
+ spacing: {
1162
+ padding: Array.from(spacing.padding),
1163
+ margin: Array.from(spacing.margin),
1164
+ gap: Array.from(spacing.gap),
1165
+ all: Array.from(spacing.all)
1166
+ },
1167
+ borders: {
1168
+ radius: Array.from(borders.radius),
1169
+ widths: Array.from(borders.widths),
1170
+ colors: Array.from(borders.colors),
1171
+ styles: Array.from(borders.styles)
1172
+ },
1173
+ shadows: {
1174
+ boxShadows: Array.from(shadows.boxShadows),
1175
+ textShadows: Array.from(shadows.textShadows),
1176
+ dropShadows: Array.from(shadows.dropShadows)
1177
+ },
1178
+ effects: {
1179
+ backdropBlur: Array.from(effects.backdropBlur),
1180
+ backdropSaturate: Array.from(effects.backdropSaturate),
1181
+ backdropBrightness: Array.from(effects.backdropBrightness),
1182
+ opacity: Array.from(effects.opacity),
1183
+ filters: Array.from(effects.filters)
1184
+ },
1185
+ zIndex: Array.from(zIndexes),
1186
+ transitions: {
1187
+ properties: Array.from(transitions.properties),
1188
+ durations: Array.from(transitions.durations),
1189
+ timingFunctions: Array.from(transitions.timingFunctions),
1190
+ delays: Array.from(transitions.delays),
1191
+ full: Array.from(transitions.full)
1192
+ },
1193
+ transforms: Array.from(transforms),
1194
+ layout: {
1195
+ displays: Array.from(layout.displays),
1196
+ justifyContent: Array.from(layout.justifyContent),
1197
+ alignItems: Array.from(layout.alignItems),
1198
+ flexDirection: Array.from(layout.flexDirection),
1199
+ flexWrap: Array.from(layout.flexWrap),
1200
+ gridTemplateColumns: Array.from(layout.gridTemplateColumns),
1201
+ gridTemplateRows: Array.from(layout.gridTemplateRows),
1202
+ positions: Array.from(layout.positions),
1203
+ maxWidths: Array.from(layout.maxWidths),
1204
+ widths: Array.from(layout.widths).slice(0, 50),
1205
+ heights: Array.from(layout.heights).slice(0, 50)
1206
+ },
1207
+ breakpoints,
1208
+ cursors: Array.from(cursors),
1209
+ outlines: Array.from(outlines),
1210
+ selection: { color: selectionColor, background: selectionBg },
1211
+ // Components
1212
+ components: {
1213
+ buttons: buttonData,
1214
+ inputs: inputData,
1215
+ cards: cardData,
1216
+ links: linkData,
1217
+ headings: headingData,
1218
+ containers: containerData,
1219
+ navItems: navItemData,
1220
+ badges: badgeData
1221
+ },
1222
+ // Raw CSS
1223
+ rawCSS: {
1224
+ stylesheets: stylesheetContents.slice(0, 2e3),
1225
+ cssVariables,
1226
+ mediaQueries: mediaQueries.slice(0, 100),
1227
+ keyframeRules
1228
+ }
1229
+ };
1230
+ });
1231
+ }
1232
+ async organizeWithAI(url, raw) {
1233
+ const designSystem = {
1234
+ name: new URL(url).hostname.replace(/\./g, "-") + "-design-system",
1235
+ sourceUrl: url,
1236
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
1237
+ colors: raw.colors,
1238
+ typography: raw.typography,
1239
+ spacing: raw.spacing,
1240
+ borders: raw.borders,
1241
+ shadows: raw.shadows,
1242
+ effects: raw.effects,
1243
+ zIndex: raw.zIndex,
1244
+ animations: {
1245
+ transitions: raw.transitions,
1246
+ keyframes: this.parseKeyframes(raw.rawCSS.keyframeRules),
1247
+ transforms: raw.transforms,
1248
+ framerMotion: this.extractFramerMotionPatterns(raw.components)
1249
+ },
1250
+ layout: {
1251
+ ...raw.layout,
1252
+ breakpoints: raw.breakpoints,
1253
+ containers: {
1254
+ maxWidths: raw.layout.maxWidths,
1255
+ widths: raw.layout.widths,
1256
+ heights: raw.layout.heights
1257
+ }
1258
+ },
1259
+ states: this.extractStates(raw.components),
1260
+ details: {
1261
+ cursors: raw.cursors,
1262
+ selectionColors: raw.selection.color ? [raw.selection.color, raw.selection.background] : [],
1263
+ scrollbarStyles: {},
1264
+ placeholderStyles: {},
1265
+ outlines: raw.outlines
1266
+ },
1267
+ components: raw.components,
1268
+ rawCSS: raw.rawCSS
1269
+ };
1270
+ return designSystem;
1271
+ }
1272
+ parseKeyframes(rules) {
1273
+ const keyframes = {};
1274
+ rules.forEach((rule, i) => {
1275
+ const nameMatch = rule.match(/@keyframes\s+([^\s{]+)/);
1276
+ if (nameMatch) {
1277
+ keyframes[nameMatch[1]] = rule;
1278
+ } else {
1279
+ keyframes[`animation-${i}`] = rule;
1280
+ }
1281
+ });
1282
+ return keyframes;
1283
+ }
1284
+ extractFramerMotionPatterns(components) {
1285
+ const patterns = {
1286
+ variants: [],
1287
+ initial: [],
1288
+ animate: [],
1289
+ exit: [],
1290
+ whileHover: [],
1291
+ whileTap: [],
1292
+ whileInView: []
1293
+ };
1294
+ Object.values(components).flat().forEach((comp) => {
1295
+ if (comp.framerMotion && Object.keys(comp.framerMotion).length > 0) {
1296
+ patterns.variants.push(comp.framerMotion);
1297
+ }
1298
+ if (comp.inlineStyle) {
1299
+ if (comp.inlineStyle.includes("transform")) {
1300
+ patterns.animate.push({ transform: comp.styles.transform });
1301
+ }
1302
+ }
1303
+ });
1304
+ return patterns;
1305
+ }
1306
+ extractStates(components) {
1307
+ const states = {
1308
+ hover: [],
1309
+ active: [],
1310
+ focus: [],
1311
+ disabled: [],
1312
+ loading: []
1313
+ };
1314
+ Object.values(components).flat().forEach((comp) => {
1315
+ if (comp.states) {
1316
+ if (comp.states.hover) states.hover.push({ component: comp.className, styles: comp.states.hover });
1317
+ if (comp.states.active) states.active.push({ component: comp.className, styles: comp.states.active });
1318
+ if (comp.states.focus) states.focus.push({ component: comp.className, styles: comp.states.focus });
1319
+ }
1320
+ });
1321
+ return states;
1322
+ }
1323
+ async generateCompleteOutput(ds, outputDir, raw) {
1324
+ await fs2.ensureDir(outputDir);
1325
+ await fs2.ensureDir(path2.join(outputDir, "tokens"));
1326
+ await fs2.ensureDir(path2.join(outputDir, "components"));
1327
+ await fs2.ensureDir(path2.join(outputDir, "animations"));
1328
+ await fs2.ensureDir(path2.join(outputDir, "raw"));
1329
+ const tokensCSS = this.generateCompleteTokensCSS(ds);
1330
+ await fs2.writeFile(path2.join(outputDir, "tokens", "all-tokens.css"), tokensCSS);
1331
+ await fs2.writeFile(path2.join(outputDir, "tokens", "colors.css"), this.generateColorsCSS(ds));
1332
+ await fs2.writeFile(path2.join(outputDir, "tokens", "typography.css"), this.generateTypographyCSS(ds));
1333
+ await fs2.writeFile(path2.join(outputDir, "tokens", "spacing.css"), this.generateSpacingCSS(ds));
1334
+ await fs2.writeFile(path2.join(outputDir, "tokens", "shadows.css"), this.generateShadowsCSS(ds));
1335
+ await fs2.writeFile(path2.join(outputDir, "tokens", "effects.css"), this.generateEffectsCSS(ds));
1336
+ await fs2.writeFile(path2.join(outputDir, "animations", "keyframes.css"), this.generateKeyframesCSS(ds));
1337
+ await fs2.writeFile(path2.join(outputDir, "animations", "transitions.css"), this.generateTransitionsCSS(ds));
1338
+ await fs2.writeFile(path2.join(outputDir, "tailwind.preset.js"), this.generateCompleteTailwindPreset(ds));
1339
+ await fs2.writeFile(path2.join(outputDir, "animations", "framer-variants.ts"), this.generateFramerMotionConfig(ds, raw));
1340
+ await this.generateComponentFiles(ds, outputDir, raw);
1341
+ await fs2.writeFile(path2.join(outputDir, "raw", "complete-extraction.json"), JSON.stringify(raw, null, 2));
1342
+ await fs2.writeFile(path2.join(outputDir, "raw", "design-system.json"), JSON.stringify(ds, null, 2));
1343
+ const pkg = {
1344
+ name: ds.name,
1345
+ version: "1.0.0",
1346
+ description: `Complete design system extracted from ${ds.sourceUrl}`,
1347
+ main: "index.js",
1348
+ exports: {
1349
+ ".": "./index.js",
1350
+ "./tailwind.preset": "./tailwind.preset.js",
1351
+ "./tokens/*": "./tokens/*",
1352
+ "./animations/*": "./animations/*",
1353
+ "./components/*": "./components/*"
1354
+ },
1355
+ peerDependencies: {
1356
+ react: ">=18.0.0",
1357
+ tailwindcss: ">=3.0.0",
1358
+ "framer-motion": ">=10.0.0"
1359
+ }
1360
+ };
1361
+ await fs2.writeFile(path2.join(outputDir, "package.json"), JSON.stringify(pkg, null, 2));
1362
+ await fs2.writeFile(path2.join(outputDir, "index.ts"), `// ${ds.name}
1363
+ // Extracted from ${ds.sourceUrl} at ${ds.extractedAt}
1364
+ //
1365
+ // USAGE:
1366
+ //
1367
+ // 1. Import CSS tokens:
1368
+ // import '${ds.name}/tokens/all-tokens.css'
1369
+ //
1370
+ // 2. Use Tailwind preset:
1371
+ // // tailwind.config.js
1372
+ // module.exports = { presets: [require('${ds.name}/tailwind.preset')] }
1373
+ //
1374
+ // 3. Use Framer Motion variants:
1375
+ // import { fadeInUp, staggerContainer } from '${ds.name}/animations/framer-variants'
1376
+
1377
+ export * from './components';
1378
+ export * from './animations/framer-variants';
1379
+ `);
1380
+ const stats = {
1381
+ colors: ds.colors.solid.length + ds.colors.gradients.length,
1382
+ fonts: ds.typography.fontFamilies.length,
1383
+ fontSizes: ds.typography.fontSizes.length,
1384
+ spacing: ds.spacing.all.length,
1385
+ borderRadius: ds.borders.radius.length,
1386
+ shadows: ds.shadows.boxShadows.length,
1387
+ transitions: ds.animations.transitions.full.length,
1388
+ keyframes: Object.keys(ds.animations.keyframes).length,
1389
+ transforms: ds.animations.transforms.length,
1390
+ breakpoints: Object.keys(ds.layout.breakpoints).length,
1391
+ zIndexLayers: ds.zIndex.length,
1392
+ buttons: ds.components.buttons.length,
1393
+ inputs: ds.components.inputs.length,
1394
+ cards: ds.components.cards.length,
1395
+ headings: ds.components.headings.length
1396
+ };
1397
+ await fs2.writeFile(path2.join(outputDir, "stats.json"), JSON.stringify(stats, null, 2));
1398
+ }
1399
+ generateCompleteTokensCSS(ds) {
1400
+ return `/* ============================================
1401
+ ${ds.name.toUpperCase()}
1402
+ Complete Design Tokens
1403
+ Extracted from ${ds.sourceUrl}
1404
+ ============================================ */
1405
+
1406
+ /* === COLORS === */
1407
+ :root {
1408
+ ${ds.colors.solid.map((c, i) => ` --color-${i + 1}: ${c};`).join("\n")}
1409
+ }
1410
+
1411
+ /* === GRADIENTS === */
1412
+ :root {
1413
+ ${ds.colors.gradients.map((g, i) => ` --gradient-${i + 1}: ${g};`).join("\n")}
1414
+ }
1415
+
1416
+ /* === TYPOGRAPHY === */
1417
+ :root {
1418
+ /* Font Families */
1419
+ ${ds.typography.fontFamilies.map((f, i) => ` --font-${i + 1}: ${f};`).join("\n")}
1420
+
1421
+ /* Font Sizes */
1422
+ ${ds.typography.fontSizes.map((s, i) => ` --text-${i + 1}: ${s};`).join("\n")}
1423
+
1424
+ /* Font Weights */
1425
+ ${ds.typography.fontWeights.map((w, i) => ` --weight-${i + 1}: ${w};`).join("\n")}
1426
+
1427
+ /* Line Heights */
1428
+ ${ds.typography.lineHeights.map((l, i) => ` --leading-${i + 1}: ${l};`).join("\n")}
1429
+
1430
+ /* Letter Spacing */
1431
+ ${ds.typography.letterSpacing.filter((l) => l !== "normal").map((l, i) => ` --tracking-${i + 1}: ${l};`).join("\n")}
1432
+ }
1433
+
1434
+ /* === SPACING === */
1435
+ :root {
1436
+ ${ds.spacing.all.map((s, i) => ` --space-${i + 1}: ${s};`).join("\n")}
1437
+ }
1438
+
1439
+ /* === BORDERS === */
1440
+ :root {
1441
+ /* Border Radius */
1442
+ ${ds.borders.radius.map((r, i) => ` --radius-${i + 1}: ${r};`).join("\n")}
1443
+
1444
+ /* Border Widths */
1445
+ ${ds.borders.widths.filter((w) => w !== "0px").map((w, i) => ` --border-${i + 1}: ${w};`).join("\n")}
1446
+ }
1447
+
1448
+ /* === SHADOWS === */
1449
+ :root {
1450
+ ${ds.shadows.boxShadows.map((s, i) => ` --shadow-${i + 1}: ${s};`).join("\n")}
1451
+ }
1452
+
1453
+ /* === EFFECTS === */
1454
+ :root {
1455
+ /* Backdrop Blur */
1456
+ ${ds.effects.backdropBlur.map((b, i) => ` --blur-${i + 1}: ${b};`).join("\n")}
1457
+
1458
+ /* Opacity */
1459
+ ${ds.effects.opacity.map((o, i) => ` --opacity-${i + 1}: ${o};`).join("\n")}
1460
+ }
1461
+
1462
+ /* === Z-INDEX === */
1463
+ :root {
1464
+ ${ds.zIndex.map((z, i) => ` --z-${i + 1}: ${z};`).join("\n")}
1465
+ }
1466
+
1467
+ /* === TRANSITIONS === */
1468
+ :root {
1469
+ /* Durations */
1470
+ ${ds.animations.transitions.durations.map((d, i) => ` --duration-${i + 1}: ${d};`).join("\n")}
1471
+
1472
+ /* Timing Functions */
1473
+ ${ds.animations.transitions.timingFunctions.map((t, i) => ` --ease-${i + 1}: ${t};`).join("\n")}
1474
+ }
1475
+ `;
1476
+ }
1477
+ generateColorsCSS(ds) {
1478
+ return `:root {
1479
+ /* Solid Colors */
1480
+ ${ds.colors.solid.map((c, i) => ` --color-${i + 1}: ${c};`).join("\n")}
1481
+
1482
+ /* RGBA Colors */
1483
+ ${ds.colors.rgba.map((c, i) => ` --color-alpha-${i + 1}: ${c};`).join("\n")}
1484
+
1485
+ /* Gradients */
1486
+ ${ds.colors.gradients.map((g, i) => ` --gradient-${i + 1}: ${g};`).join("\n")}
1487
+ }
1488
+ `;
1489
+ }
1490
+ generateTypographyCSS(ds) {
1491
+ return `:root {
1492
+ /* Font Families */
1493
+ ${ds.typography.fontFamilies.map((f, i) => ` --font-family-${i + 1}: "${f}";`).join("\n")}
1494
+
1495
+ /* Font Sizes */
1496
+ ${ds.typography.fontSizes.map((s, i) => ` --font-size-${i + 1}: ${s};`).join("\n")}
1497
+
1498
+ /* Font Weights */
1499
+ ${ds.typography.fontWeights.map((w, i) => ` --font-weight-${i + 1}: ${w};`).join("\n")}
1500
+
1501
+ /* Line Heights */
1502
+ ${ds.typography.lineHeights.map((l, i) => ` --line-height-${i + 1}: ${l};`).join("\n")}
1503
+
1504
+ /* Letter Spacing */
1505
+ ${ds.typography.letterSpacing.filter((l) => l !== "normal").map((l, i) => ` --letter-spacing-${i + 1}: ${l};`).join("\n")}
1506
+ }
1507
+ `;
1508
+ }
1509
+ generateSpacingCSS(ds) {
1510
+ const sorted = [...ds.spacing.all].sort((a, b) => {
1511
+ const aNum = parseFloat(a);
1512
+ const bNum = parseFloat(b);
1513
+ return aNum - bNum;
1514
+ });
1515
+ return `:root {
1516
+ /* All Spacing Values */
1517
+ ${sorted.map((s, i) => ` --space-${i + 1}: ${s};`).join("\n")}
1518
+ }
1519
+ `;
1520
+ }
1521
+ generateShadowsCSS(ds) {
1522
+ return `:root {
1523
+ /* Box Shadows */
1524
+ ${ds.shadows.boxShadows.map((s, i) => ` --shadow-${i + 1}: ${s};`).join("\n")}
1525
+
1526
+ /* Text Shadows */
1527
+ ${ds.shadows.textShadows.map((s, i) => ` --text-shadow-${i + 1}: ${s};`).join("\n")}
1528
+
1529
+ /* Drop Shadows */
1530
+ ${ds.shadows.dropShadows.map((s, i) => ` --drop-shadow-${i + 1}: ${s};`).join("\n")}
1531
+ }
1532
+ `;
1533
+ }
1534
+ generateEffectsCSS(ds) {
1535
+ return `:root {
1536
+ /* Backdrop Blur */
1537
+ ${ds.effects.backdropBlur.map((b, i) => ` --backdrop-blur-${i + 1}: blur(${b});`).join("\n")}
1538
+
1539
+ /* Backdrop Saturate */
1540
+ ${ds.effects.backdropSaturate.map((s, i) => ` --backdrop-saturate-${i + 1}: saturate(${s});`).join("\n")}
1541
+
1542
+ /* Opacity */
1543
+ ${ds.effects.opacity.map((o, i) => ` --opacity-${i + 1}: ${o};`).join("\n")}
1544
+
1545
+ /* All Filters */
1546
+ ${ds.effects.filters.map((f, i) => ` --filter-${i + 1}: ${f};`).join("\n")}
1547
+ }
1548
+ `;
1549
+ }
1550
+ generateKeyframesCSS(ds) {
1551
+ return `/* Keyframe Animations */
1552
+ ${Object.entries(ds.animations.keyframes).map(([name, rule]) => rule).join("\n\n")}
1553
+ `;
1554
+ }
1555
+ generateTransitionsCSS(ds) {
1556
+ return `:root {
1557
+ /* Transition Durations */
1558
+ ${ds.animations.transitions.durations.map((d, i) => ` --duration-${i + 1}: ${d};`).join("\n")}
1559
+
1560
+ /* Timing Functions */
1561
+ ${ds.animations.transitions.timingFunctions.map((t, i) => ` --ease-${i + 1}: ${t};`).join("\n")}
1562
+ }
1563
+
1564
+ /* Full Transitions */
1565
+ ${ds.animations.transitions.full.map((t, i) => `.transition-${i + 1} { transition: ${t}; }`).join("\n")}
1566
+ `;
1567
+ }
1568
+ generateCompleteTailwindPreset(ds) {
1569
+ const colorMap = {};
1570
+ ds.colors.solid.forEach((c, i) => {
1571
+ colorMap[`c${i + 1}`] = c;
1572
+ });
1573
+ const spacingMap = {};
1574
+ ds.spacing.all.forEach((s, i) => {
1575
+ spacingMap[`s${i + 1}`] = s;
1576
+ });
1577
+ const radiusMap = {};
1578
+ ds.borders.radius.forEach((r, i) => {
1579
+ radiusMap[`r${i + 1}`] = r;
1580
+ });
1581
+ const shadowMap = {};
1582
+ ds.shadows.boxShadows.forEach((s, i) => {
1583
+ shadowMap[`shadow${i + 1}`] = s;
1584
+ });
1585
+ return `// Tailwind CSS Complete Preset
1586
+ // Extracted from ${ds.sourceUrl}
1587
+ // ${ds.extractedAt}
1588
+
1589
+ /** @type {import('tailwindcss').Config} */
1590
+ module.exports = {
1591
+ theme: {
1592
+ extend: {
1593
+ // ${ds.colors.solid.length} Colors
1594
+ colors: ${JSON.stringify(colorMap, null, 8)},
1595
+
1596
+ // ${ds.typography.fontFamilies.length} Font Families
1597
+ fontFamily: {
1598
+ ${ds.typography.fontFamilies.map((f, i) => ` f${i + 1}: ['${f}'],`).join("\n")}
1599
+ },
1600
+
1601
+ // ${ds.typography.fontSizes.length} Font Sizes
1602
+ fontSize: {
1603
+ ${ds.typography.fontSizes.map((s, i) => ` t${i + 1}: '${s}',`).join("\n")}
1604
+ },
1605
+
1606
+ // ${ds.typography.fontWeights.length} Font Weights
1607
+ fontWeight: {
1608
+ ${ds.typography.fontWeights.map((w, i) => ` w${i + 1}: '${w}',`).join("\n")}
1609
+ },
1610
+
1611
+ // ${ds.typography.lineHeights.length} Line Heights
1612
+ lineHeight: {
1613
+ ${ds.typography.lineHeights.map((l, i) => ` lh${i + 1}: '${l}',`).join("\n")}
1614
+ },
1615
+
1616
+ // ${ds.typography.letterSpacing.length} Letter Spacing
1617
+ letterSpacing: {
1618
+ ${ds.typography.letterSpacing.filter((l) => l !== "normal").map((l, i) => ` ls${i + 1}: '${l}',`).join("\n")}
1619
+ },
1620
+
1621
+ // ${ds.spacing.all.length} Spacing Values
1622
+ spacing: ${JSON.stringify(spacingMap, null, 8)},
1623
+
1624
+ // ${ds.borders.radius.length} Border Radius Values
1625
+ borderRadius: ${JSON.stringify(radiusMap, null, 8)},
1626
+
1627
+ // ${ds.shadows.boxShadows.length} Box Shadows
1628
+ boxShadow: ${JSON.stringify(shadowMap, null, 8)},
1629
+
1630
+ // ${ds.animations.transitions.durations.length} Transition Durations
1631
+ transitionDuration: {
1632
+ ${ds.animations.transitions.durations.map((d, i) => ` d${i + 1}: '${d}',`).join("\n")}
1633
+ },
1634
+
1635
+ // ${ds.animations.transitions.timingFunctions.length} Timing Functions
1636
+ transitionTimingFunction: {
1637
+ ${ds.animations.transitions.timingFunctions.map((t, i) => ` e${i + 1}: '${t}',`).join("\n")}
1638
+ },
1639
+
1640
+ // ${ds.zIndex.length} Z-Index Values
1641
+ zIndex: {
1642
+ ${ds.zIndex.map((z, i) => ` z${i + 1}: '${z}',`).join("\n")}
1643
+ },
1644
+
1645
+ // Backdrop
1646
+ backdropBlur: {
1647
+ ${ds.effects.backdropBlur.map((b, i) => ` b${i + 1}: '${b}',`).join("\n")}
1648
+ },
1649
+
1650
+ // Opacity
1651
+ opacity: {
1652
+ ${ds.effects.opacity.map((o, i) => ` o${i + 1}: '${o}',`).join("\n")}
1653
+ },
1654
+ },
1655
+ },
1656
+ plugins: [],
1657
+ };
1658
+ `;
1659
+ }
1660
+ generateFramerMotionConfig(ds, raw) {
1661
+ const transitions = ds.animations.transitions;
1662
+ const defaultDuration = transitions.durations[0] || "0.3s";
1663
+ const defaultEasing = transitions.timingFunctions[0] || "ease";
1664
+ return `// Framer Motion Variants
1665
+ // Extracted from ${ds.sourceUrl}
1666
+
1667
+ // Transition presets
1668
+ export const transitions = {
1669
+ fast: { duration: 0.15 },
1670
+ normal: { duration: 0.3 },
1671
+ slow: { duration: 0.5 },
1672
+ spring: { type: 'spring', stiffness: 300, damping: 30 },
1673
+ springBouncy: { type: 'spring', stiffness: 400, damping: 20 },
1674
+ };
1675
+
1676
+ // Fade animations
1677
+ export const fadeIn = {
1678
+ initial: { opacity: 0 },
1679
+ animate: { opacity: 1 },
1680
+ exit: { opacity: 0 },
1681
+ };
1682
+
1683
+ export const fadeInUp = {
1684
+ initial: { opacity: 0, y: 20 },
1685
+ animate: { opacity: 1, y: 0 },
1686
+ exit: { opacity: 0, y: -20 },
1687
+ };
1688
+
1689
+ export const fadeInDown = {
1690
+ initial: { opacity: 0, y: -20 },
1691
+ animate: { opacity: 1, y: 0 },
1692
+ exit: { opacity: 0, y: 20 },
1693
+ };
1694
+
1695
+ export const fadeInLeft = {
1696
+ initial: { opacity: 0, x: -20 },
1697
+ animate: { opacity: 1, x: 0 },
1698
+ exit: { opacity: 0, x: 20 },
1699
+ };
1700
+
1701
+ export const fadeInRight = {
1702
+ initial: { opacity: 0, x: 20 },
1703
+ animate: { opacity: 1, x: 0 },
1704
+ exit: { opacity: 0, x: -20 },
1705
+ };
1706
+
1707
+ // Scale animations
1708
+ export const scaleIn = {
1709
+ initial: { opacity: 0, scale: 0.9 },
1710
+ animate: { opacity: 1, scale: 1 },
1711
+ exit: { opacity: 0, scale: 0.9 },
1712
+ };
1713
+
1714
+ export const scaleInBounce = {
1715
+ initial: { opacity: 0, scale: 0.8 },
1716
+ animate: { opacity: 1, scale: 1, transition: transitions.springBouncy },
1717
+ exit: { opacity: 0, scale: 0.8 },
1718
+ };
1719
+
1720
+ // Stagger containers
1721
+ export const staggerContainer = {
1722
+ initial: {},
1723
+ animate: {
1724
+ transition: {
1725
+ staggerChildren: 0.1,
1726
+ delayChildren: 0.1,
1727
+ },
1728
+ },
1729
+ };
1730
+
1731
+ export const staggerContainerFast = {
1732
+ initial: {},
1733
+ animate: {
1734
+ transition: {
1735
+ staggerChildren: 0.05,
1736
+ delayChildren: 0.05,
1737
+ },
1738
+ },
1739
+ };
1740
+
1741
+ // Hover states (extracted from site)
1742
+ export const hoverScale = {
1743
+ whileHover: { scale: 1.02 },
1744
+ whileTap: { scale: 0.98 },
1745
+ };
1746
+
1747
+ export const hoverScaleLarge = {
1748
+ whileHover: { scale: 1.05 },
1749
+ whileTap: { scale: 0.95 },
1750
+ };
1751
+
1752
+ export const hoverLift = {
1753
+ whileHover: { y: -4, transition: { duration: 0.2 } },
1754
+ whileTap: { y: 0 },
1755
+ };
1756
+
1757
+ export const hoverGlow = {
1758
+ whileHover: {
1759
+ boxShadow: '0 10px 40px rgba(0, 0, 0, 0.15)',
1760
+ transition: { duration: 0.3 }
1761
+ },
1762
+ };
1763
+
1764
+ // Button variants
1765
+ export const buttonVariants = {
1766
+ initial: { scale: 1 },
1767
+ hover: { scale: 1.02 },
1768
+ tap: { scale: 0.98 },
1769
+ disabled: { opacity: 0.5 },
1770
+ };
1771
+
1772
+ // Card variants
1773
+ export const cardVariants = {
1774
+ initial: { opacity: 0, y: 20 },
1775
+ animate: { opacity: 1, y: 0 },
1776
+ hover: { y: -4, boxShadow: '0 20px 40px rgba(0, 0, 0, 0.1)' },
1777
+ };
1778
+
1779
+ // Modal variants
1780
+ export const modalVariants = {
1781
+ initial: { opacity: 0, scale: 0.95 },
1782
+ animate: { opacity: 1, scale: 1 },
1783
+ exit: { opacity: 0, scale: 0.95 },
1784
+ };
1785
+
1786
+ export const overlayVariants = {
1787
+ initial: { opacity: 0 },
1788
+ animate: { opacity: 1 },
1789
+ exit: { opacity: 0 },
1790
+ };
1791
+
1792
+ // Scroll reveal
1793
+ export const scrollReveal = {
1794
+ initial: { opacity: 0, y: 60 },
1795
+ whileInView: { opacity: 1, y: 0 },
1796
+ viewport: { once: true, margin: '-100px' },
1797
+ };
1798
+
1799
+ // Navigation
1800
+ export const navItemVariants = {
1801
+ initial: { opacity: 0, y: -10 },
1802
+ animate: { opacity: 1, y: 0 },
1803
+ hover: { color: 'var(--color-primary)' },
1804
+ };
1805
+
1806
+ // Export all as default
1807
+ export default {
1808
+ transitions,
1809
+ fadeIn,
1810
+ fadeInUp,
1811
+ fadeInDown,
1812
+ fadeInLeft,
1813
+ fadeInRight,
1814
+ scaleIn,
1815
+ scaleInBounce,
1816
+ staggerContainer,
1817
+ staggerContainerFast,
1818
+ hoverScale,
1819
+ hoverScaleLarge,
1820
+ hoverLift,
1821
+ hoverGlow,
1822
+ buttonVariants,
1823
+ cardVariants,
1824
+ modalVariants,
1825
+ overlayVariants,
1826
+ scrollReveal,
1827
+ navItemVariants,
1828
+ };
1829
+ `;
1830
+ }
1831
+ async generateComponentFiles(ds, outputDir, raw) {
1832
+ const componentsDir = path2.join(outputDir, "components");
1833
+ if (ds.components.buttons.length > 0) {
1834
+ const buttonComponent = this.generateButtonComponent(ds.components.buttons);
1835
+ await fs2.writeFile(path2.join(componentsDir, "Button.tsx"), buttonComponent);
1836
+ }
1837
+ if (ds.components.cards.length > 0) {
1838
+ const cardComponent = this.generateCardComponent(ds.components.cards);
1839
+ await fs2.writeFile(path2.join(componentsDir, "Card.tsx"), cardComponent);
1840
+ }
1841
+ if (ds.components.inputs.length > 0) {
1842
+ const inputComponent = this.generateInputComponent(ds.components.inputs);
1843
+ await fs2.writeFile(path2.join(componentsDir, "Input.tsx"), inputComponent);
1844
+ }
1845
+ if (ds.components.headings.length > 0) {
1846
+ const headingComponent = this.generateHeadingComponent(ds.components.headings);
1847
+ await fs2.writeFile(path2.join(componentsDir, "Heading.tsx"), headingComponent);
1848
+ }
1849
+ if (ds.components.containers.length > 0) {
1850
+ const containerComponent = this.generateContainerComponent(ds.components.containers);
1851
+ await fs2.writeFile(path2.join(componentsDir, "Container.tsx"), containerComponent);
1852
+ }
1853
+ if (ds.components.navItems.length > 0) {
1854
+ const navItemComponent = this.generateNavItemComponent(ds.components.navItems);
1855
+ await fs2.writeFile(path2.join(componentsDir, "NavItem.tsx"), navItemComponent);
1856
+ }
1857
+ if (ds.components.badges.length > 0) {
1858
+ const badgeComponent = this.generateBadgeComponent(ds.components.badges);
1859
+ await fs2.writeFile(path2.join(componentsDir, "Badge.tsx"), badgeComponent);
1860
+ }
1861
+ const exports = [
1862
+ ds.components.buttons.length > 0 ? "export { Button } from './Button';" : "",
1863
+ ds.components.cards.length > 0 ? "export { Card } from './Card';" : "",
1864
+ ds.components.inputs.length > 0 ? "export { Input } from './Input';" : "",
1865
+ ds.components.headings.length > 0 ? "export { Heading } from './Heading';" : "",
1866
+ ds.components.containers.length > 0 ? "export { Container } from './Container';" : "",
1867
+ ds.components.navItems.length > 0 ? "export { NavItem } from './NavItem';" : "",
1868
+ ds.components.badges.length > 0 ? "export { Badge } from './Badge';" : ""
1869
+ ].filter((e) => e).join("\n");
1870
+ await fs2.writeFile(path2.join(componentsDir, "index.ts"), exports);
1871
+ }
1872
+ generateButtonComponent(buttons) {
1873
+ const styles = buttons[0]?.styles || {};
1874
+ return `import React from 'react';
1875
+ import { motion, HTMLMotionProps } from 'framer-motion';
1876
+
1877
+ export interface ButtonProps extends Omit<HTMLMotionProps<'button'>, 'children'> {
1878
+ children: React.ReactNode;
1879
+ variant?: 'primary' | 'secondary' | 'ghost' | 'outline';
1880
+ size?: 'sm' | 'md' | 'lg';
1881
+ loading?: boolean;
1882
+ disabled?: boolean;
1883
+ }
1884
+
1885
+ const baseStyles = \`
1886
+ inline-flex items-center justify-center
1887
+ font-semibold
1888
+ transition-all duration-300
1889
+ cursor-pointer
1890
+ disabled:opacity-50 disabled:cursor-not-allowed
1891
+ \`;
1892
+
1893
+ const variants = {
1894
+ primary: 'bg-[var(--color-1)] text-white hover:opacity-90',
1895
+ secondary: 'bg-[var(--color-2)] text-white hover:opacity-90',
1896
+ ghost: 'bg-transparent hover:bg-[var(--color-1)]/10',
1897
+ outline: 'border-2 border-current bg-transparent hover:bg-[var(--color-1)]/10',
1898
+ };
1899
+
1900
+ const sizes = {
1901
+ sm: 'px-4 py-2 text-sm rounded-[var(--radius-1)]',
1902
+ md: 'px-6 py-3 text-base rounded-[var(--radius-2)]',
1903
+ lg: 'px-8 py-4 text-lg rounded-[var(--radius-3)]',
1904
+ };
1905
+
1906
+ export const Button: React.FC<ButtonProps> = ({
1907
+ children,
1908
+ variant = 'primary',
1909
+ size = 'md',
1910
+ loading = false,
1911
+ disabled = false,
1912
+ className = '',
1913
+ ...props
1914
+ }) => {
1915
+ return (
1916
+ <motion.button
1917
+ className={\`\${baseStyles} \${variants[variant]} \${sizes[size]} \${className}\`}
1918
+ whileHover={{ scale: disabled ? 1 : 1.02 }}
1919
+ whileTap={{ scale: disabled ? 1 : 0.98 }}
1920
+ disabled={disabled || loading}
1921
+ {...props}
1922
+ >
1923
+ {loading ? (
1924
+ <span className="animate-spin mr-2">\u23F3</span>
1925
+ ) : null}
1926
+ {children}
1927
+ </motion.button>
1928
+ );
1929
+ };
1930
+
1931
+ export default Button;
1932
+ `;
1933
+ }
1934
+ generateCardComponent(cards) {
1935
+ return `import React from 'react';
1936
+ import { motion, HTMLMotionProps } from 'framer-motion';
1937
+
1938
+ export interface CardProps extends Omit<HTMLMotionProps<'div'>, 'children'> {
1939
+ children: React.ReactNode;
1940
+ variant?: 'default' | 'elevated' | 'bordered' | 'glass';
1941
+ hover?: boolean;
1942
+ padding?: 'none' | 'sm' | 'md' | 'lg';
1943
+ }
1944
+
1945
+ const variants = {
1946
+ default: 'bg-white',
1947
+ elevated: 'bg-white shadow-[var(--shadow-1)]',
1948
+ bordered: 'bg-white border border-[var(--color-1)]/20',
1949
+ glass: 'bg-white/80 backdrop-blur-[var(--blur-1)]',
1950
+ };
1951
+
1952
+ const paddings = {
1953
+ none: '',
1954
+ sm: 'p-4',
1955
+ md: 'p-6',
1956
+ lg: 'p-8',
1957
+ };
1958
+
1959
+ export const Card: React.FC<CardProps> = ({
1960
+ children,
1961
+ variant = 'default',
1962
+ hover = true,
1963
+ padding = 'md',
1964
+ className = '',
1965
+ ...props
1966
+ }) => {
1967
+ return (
1968
+ <motion.div
1969
+ className={\`rounded-[var(--radius-2)] \${variants[variant]} \${paddings[padding]} \${className}\`}
1970
+ whileHover={hover ? { y: -4, boxShadow: 'var(--shadow-2)' } : undefined}
1971
+ transition={{ duration: 0.2 }}
1972
+ {...props}
1973
+ >
1974
+ {children}
1975
+ </motion.div>
1976
+ );
1977
+ };
1978
+
1979
+ export default Card;
1980
+ `;
1981
+ }
1982
+ generateInputComponent(inputs) {
1983
+ return `import React from 'react';
1984
+
1985
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
1986
+ label?: string;
1987
+ error?: string;
1988
+ variant?: 'default' | 'filled' | 'outlined';
1989
+ }
1990
+
1991
+ const variants = {
1992
+ default: 'border border-[var(--color-1)]/20 bg-white focus:border-[var(--color-1)]',
1993
+ filled: 'border-0 bg-[var(--color-1)]/5 focus:bg-[var(--color-1)]/10',
1994
+ outlined: 'border-2 border-[var(--color-1)]/30 bg-transparent focus:border-[var(--color-1)]',
1995
+ };
1996
+
1997
+ export const Input: React.FC<InputProps> = ({
1998
+ label,
1999
+ error,
2000
+ variant = 'default',
2001
+ className = '',
2002
+ ...props
2003
+ }) => {
2004
+ return (
2005
+ <div className="flex flex-col gap-1">
2006
+ {label && <label className="text-sm font-medium">{label}</label>}
2007
+ <input
2008
+ className={\`
2009
+ px-4 py-3
2010
+ rounded-[var(--radius-1)]
2011
+ transition-all duration-200
2012
+ outline-none
2013
+ focus:ring-2 focus:ring-[var(--color-1)]/20
2014
+ \${variants[variant]}
2015
+ \${error ? 'border-red-500' : ''}
2016
+ \${className}
2017
+ \`}
2018
+ {...props}
2019
+ />
2020
+ {error && <span className="text-sm text-red-500">{error}</span>}
2021
+ </div>
2022
+ );
2023
+ };
2024
+
2025
+ export default Input;
2026
+ `;
2027
+ }
2028
+ generateHeadingComponent(headings) {
2029
+ return `import React from 'react';
2030
+ import { motion, HTMLMotionProps } from 'framer-motion';
2031
+
2032
+ export interface HeadingProps extends Omit<HTMLMotionProps<'h1'>, 'children'> {
2033
+ children: React.ReactNode;
2034
+ as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
2035
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'hero';
2036
+ animate?: boolean;
2037
+ }
2038
+
2039
+ const sizes = {
2040
+ xs: 'text-[var(--text-1)]',
2041
+ sm: 'text-[var(--text-2)]',
2042
+ md: 'text-[var(--text-3)]',
2043
+ lg: 'text-[var(--text-4)]',
2044
+ xl: 'text-[var(--text-5)]',
2045
+ '2xl': 'text-[var(--text-6)]',
2046
+ '3xl': 'text-[var(--text-7)]',
2047
+ hero: 'text-[var(--text-8)]',
2048
+ };
2049
+
2050
+ export const Heading: React.FC<HeadingProps> = ({
2051
+ children,
2052
+ as: Component = 'h2',
2053
+ size = 'lg',
2054
+ animate = false,
2055
+ className = '',
2056
+ ...props
2057
+ }) => {
2058
+ const MotionComponent = motion[Component] as any;
2059
+
2060
+ return (
2061
+ <MotionComponent
2062
+ className={\`font-[var(--font-1)] font-bold leading-tight \${sizes[size]} \${className}\`}
2063
+ initial={animate ? { opacity: 0, y: 20 } : undefined}
2064
+ animate={animate ? { opacity: 1, y: 0 } : undefined}
2065
+ {...props}
2066
+ >
2067
+ {children}
2068
+ </MotionComponent>
2069
+ );
2070
+ };
2071
+
2072
+ export default Heading;
2073
+ `;
2074
+ }
2075
+ generateContainerComponent(containers) {
2076
+ return `import React from 'react';
2077
+
2078
+ export interface ContainerProps {
2079
+ children: React.ReactNode;
2080
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
2081
+ padding?: boolean;
2082
+ center?: boolean;
2083
+ className?: string;
2084
+ }
2085
+
2086
+ const sizes = {
2087
+ sm: 'max-w-2xl',
2088
+ md: 'max-w-4xl',
2089
+ lg: 'max-w-6xl',
2090
+ xl: 'max-w-7xl',
2091
+ full: 'max-w-full',
2092
+ };
2093
+
2094
+ export const Container: React.FC<ContainerProps> = ({
2095
+ children,
2096
+ size = 'lg',
2097
+ padding = true,
2098
+ center = true,
2099
+ className = '',
2100
+ }) => {
2101
+ return (
2102
+ <div
2103
+ className={\`
2104
+ w-full
2105
+ \${sizes[size]}
2106
+ \${padding ? 'px-4 sm:px-6 lg:px-8' : ''}
2107
+ \${center ? 'mx-auto' : ''}
2108
+ \${className}
2109
+ \`}
2110
+ >
2111
+ {children}
2112
+ </div>
2113
+ );
2114
+ };
2115
+
2116
+ export default Container;
2117
+ `;
2118
+ }
2119
+ generateNavItemComponent(navItems) {
2120
+ return `import React from 'react';
2121
+ import { motion, HTMLMotionProps } from 'framer-motion';
2122
+
2123
+ export interface NavItemProps extends Omit<HTMLMotionProps<'a'>, 'children'> {
2124
+ children: React.ReactNode;
2125
+ active?: boolean;
2126
+ variant?: 'default' | 'pill' | 'underline';
2127
+ }
2128
+
2129
+ const variants = {
2130
+ default: 'hover:text-[var(--color-1)]',
2131
+ pill: 'px-4 py-2 rounded-full hover:bg-[var(--color-1)]/10',
2132
+ underline: 'relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-0 after:bg-current hover:after:w-full after:transition-all',
2133
+ };
2134
+
2135
+ export const NavItem: React.FC<NavItemProps> = ({
2136
+ children,
2137
+ active = false,
2138
+ variant = 'default',
2139
+ className = '',
2140
+ ...props
2141
+ }) => {
2142
+ return (
2143
+ <motion.a
2144
+ className={\`
2145
+ cursor-pointer
2146
+ transition-colors duration-200
2147
+ \${variants[variant]}
2148
+ \${active ? 'text-[var(--color-1)]' : ''}
2149
+ \${className}
2150
+ \`}
2151
+ whileHover={{ scale: 1.02 }}
2152
+ {...props}
2153
+ >
2154
+ {children}
2155
+ </motion.a>
2156
+ );
2157
+ };
2158
+
2159
+ export default NavItem;
2160
+ `;
2161
+ }
2162
+ generateBadgeComponent(badges) {
2163
+ return `import React from 'react';
2164
+
2165
+ export interface BadgeProps {
2166
+ children: React.ReactNode;
2167
+ variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
2168
+ size?: 'sm' | 'md' | 'lg';
2169
+ className?: string;
2170
+ }
2171
+
2172
+ const variants = {
2173
+ default: 'bg-[var(--color-1)]/10 text-[var(--color-1)]',
2174
+ success: 'bg-green-100 text-green-800',
2175
+ warning: 'bg-yellow-100 text-yellow-800',
2176
+ error: 'bg-red-100 text-red-800',
2177
+ info: 'bg-blue-100 text-blue-800',
2178
+ };
2179
+
2180
+ const sizes = {
2181
+ sm: 'px-2 py-0.5 text-xs',
2182
+ md: 'px-3 py-1 text-sm',
2183
+ lg: 'px-4 py-1.5 text-base',
2184
+ };
2185
+
2186
+ export const Badge: React.FC<BadgeProps> = ({
2187
+ children,
2188
+ variant = 'default',
2189
+ size = 'md',
2190
+ className = '',
2191
+ }) => {
2192
+ return (
2193
+ <span
2194
+ className={\`
2195
+ inline-flex items-center
2196
+ font-medium
2197
+ rounded-full
2198
+ \${variants[variant]}
2199
+ \${sizes[size]}
2200
+ \${className}
2201
+ \`}
2202
+ >
2203
+ {children}
2204
+ </span>
2205
+ );
2206
+ };
2207
+
2208
+ export default Badge;
2209
+ `;
2210
+ }
2211
+ };
2212
+
2213
+ // src/cli/index.ts
2214
+ import fs3 from "fs-extra";
2215
+ var program = new Command();
2216
+ program.name("extraktor").description("Extract complete UI from any website").version("1.0.0");
2217
+ program.command("extract <url>").description("Extract UI from a website URL").option("-o, --output <dir>", "Output directory", "./extraktor-output").option("-f, --format <formats...>", "Output formats (tailwind, tokens, css-vars, figma, tsx)", ["tailwind", "tokens", "css-vars"]).option("-c, --config <file>", "Config file path").option("--spa <framework>", "SPA framework (auto, react, vue, angular, svelte)", "auto").option("--no-cache", "Disable caching").option("--headless", "Run browser in headless mode", true).option("--no-headless", "Run browser in visible mode").option("--timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--scroll", "Scroll page to capture lazy-loaded content", true).option("--no-scroll", "Disable page scrolling").option("--hover-states", "Capture hover states", false).option("--devtools [endpoint]", "Connect to existing Chrome via DevTools Protocol (default: http://localhost:9222)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (url, options) => {
2218
+ const spinner = ora();
2219
+ const logger = createLogger({
2220
+ level: options.quiet ? "error" : options.verbose ? "debug" : "info"
2221
+ });
2222
+ try {
2223
+ let parsedUrl;
2224
+ try {
2225
+ parsedUrl = new URL(url);
2226
+ } catch {
2227
+ try {
2228
+ parsedUrl = new URL(`https://${url}`);
2229
+ url = parsedUrl.toString();
2230
+ } catch {
2231
+ logger.error(`Invalid URL: ${url}`);
2232
+ process.exit(1);
2233
+ }
2234
+ }
2235
+ spinner.start(chalk.blue("Loading configuration..."));
2236
+ let config = getDefaultConfig();
2237
+ if (options.config) {
2238
+ const loaded = await loadConfig(options.config);
2239
+ if (loaded) {
2240
+ config = { ...config, ...loaded };
2241
+ }
2242
+ }
2243
+ config.browser.headless = options.headless;
2244
+ config.browser.timeout = parseInt(options.timeout);
2245
+ config.spa.framework = options.spa;
2246
+ config.spa.scrollToLoad = options.scroll;
2247
+ config.extraction.hoverStates = options.hoverStates;
2248
+ if (options.devtools) {
2249
+ config.browser.cdpEndpoint = typeof options.devtools === "string" ? options.devtools : "http://localhost:9222";
2250
+ config.browser.headless = false;
2251
+ }
2252
+ const formatMap = {
2253
+ tailwind: "tailwind",
2254
+ tokens: "design-tokens",
2255
+ "css-vars": "css-variables",
2256
+ figma: "figma",
2257
+ tsx: "tsx"
2258
+ };
2259
+ config.output.formats = options.format.map((f) => formatMap[f] || f);
2260
+ spinner.succeed(chalk.green("Configuration loaded"));
2261
+ const outputDir = path3.resolve(options.output);
2262
+ await fs3.ensureDir(outputDir);
2263
+ const orchestrator = new Orchestrator({ config, logger });
2264
+ spinner.start(chalk.blue(`Extracting UI from ${chalk.cyan(url)}...`));
2265
+ const startTime = Date.now();
2266
+ const result = await orchestrator.extract(url, outputDir);
2267
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(2);
2268
+ spinner.succeed(chalk.green(`Extraction complete in ${duration}s`));
2269
+ console.log("\n" + chalk.bold("\u{1F4CA} Extraction Summary:"));
2270
+ console.log(chalk.gray("\u2500".repeat(40)));
2271
+ if (result.colors) {
2272
+ console.log(` ${chalk.yellow("\u{1F3A8} Colors:")} ${result.colors.palette.length} colors, ${result.colors.gradients.length} gradients`);
2273
+ }
2274
+ if (result.typography) {
2275
+ console.log(` ${chalk.yellow("\u{1F4DD} Typography:")} ${result.typography.fontFamilies.length} fonts, ${result.typography.fontSizes.length} sizes`);
2276
+ }
2277
+ if (result.spacing) {
2278
+ console.log(` ${chalk.yellow("\u{1F4D0} Spacing:")} ${result.spacing.scale.length} values`);
2279
+ }
2280
+ if (result.effects) {
2281
+ console.log(` ${chalk.yellow("\u2728 Effects:")} ${result.effects.shadows.length} shadows, ${result.effects.borderRadii.length} radii`);
2282
+ }
2283
+ if (result.animations) {
2284
+ console.log(` ${chalk.yellow("\u{1F3AC} Animations:")} ${result.animations.keyframes.length} keyframes, ${result.animations.transitions.length} transitions`);
2285
+ }
2286
+ console.log(chalk.gray("\u2500".repeat(40)));
2287
+ console.log(`
2288
+ ${chalk.bold("\u{1F4C1} Output:")} ${chalk.cyan(outputDir)}`);
2289
+ const files = await fs3.readdir(outputDir);
2290
+ for (const file of files) {
2291
+ const stat = await fs3.stat(path3.join(outputDir, file));
2292
+ if (stat.isFile()) {
2293
+ console.log(` ${chalk.green("\u2713")} ${file}`);
2294
+ } else if (stat.isDirectory()) {
2295
+ const subfiles = await fs3.readdir(path3.join(outputDir, file));
2296
+ console.log(` ${chalk.green("\u2713")} ${file}/ (${subfiles.length} files)`);
2297
+ }
2298
+ }
2299
+ console.log("\n" + chalk.green.bold("\u2705 Done!"));
2300
+ } catch (error) {
2301
+ spinner.fail(chalk.red("Extraction failed"));
2302
+ logger.error(error instanceof Error ? error.message : String(error));
2303
+ if (options.verbose && error instanceof Error) {
2304
+ console.error(error.stack);
2305
+ }
2306
+ process.exit(1);
2307
+ }
2308
+ });
2309
+ program.command("init").description("Initialize extraktor configuration").option("--preset <preset>", "Configuration preset (minimal, standard, full)", "standard").option("-f, --force", "Overwrite existing config").action(async (options) => {
2310
+ const configPath = "extraktor.config.json";
2311
+ if (await fs3.pathExists(configPath) && !options.force) {
2312
+ console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
2313
+ return;
2314
+ }
2315
+ const config = getDefaultConfig();
2316
+ if (options.preset === "minimal") {
2317
+ config.output.formats = ["tailwind", "css-variables"];
2318
+ config.optimization.images.enabled = false;
2319
+ config.optimization.svgs.enabled = false;
2320
+ config.optimization.fonts.enabled = false;
2321
+ } else if (options.preset === "full") {
2322
+ config.output.formats = ["tailwind", "design-tokens", "css-variables", "figma", "tsx"];
2323
+ config.optimization.images.enabled = true;
2324
+ config.optimization.svgs.enabled = true;
2325
+ config.optimization.fonts.enabled = true;
2326
+ }
2327
+ await fs3.writeJSON(configPath, config, { spaces: 2 });
2328
+ console.log(chalk.green(`\u2705 Created ${configPath} with ${options.preset} preset`));
2329
+ });
2330
+ program.command("generate <url>").description("Extract UI and generate a complete Next.js project").option("-o, --output <dir>", "Output directory", "./extraktor-output").option("-n, --name <name>", "Project name (defaults to domain name)").option("--spa <framework>", "SPA framework (auto, react, vue, angular, svelte)", "auto").option("--timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--headless", "Run browser in headless mode", true).option("--no-headless", "Run browser in visible mode").option("-v, --verbose", "Verbose output").action(async (url, options) => {
2331
+ const spinner = ora();
2332
+ const logger = createLogger({
2333
+ level: options.verbose ? "debug" : "info"
2334
+ });
2335
+ try {
2336
+ let parsedUrl;
2337
+ try {
2338
+ parsedUrl = new URL(url);
2339
+ } catch {
2340
+ try {
2341
+ parsedUrl = new URL(`https://${url}`);
2342
+ url = parsedUrl.toString();
2343
+ } catch {
2344
+ logger.error(`Invalid URL: ${url}`);
2345
+ process.exit(1);
2346
+ }
2347
+ }
2348
+ spinner.start(chalk.blue("Loading configuration..."));
2349
+ const config = getDefaultConfig();
2350
+ config.browser.headless = options.headless;
2351
+ config.browser.timeout = parseInt(options.timeout);
2352
+ config.spa.framework = options.spa;
2353
+ config.output.formats = ["tailwind", "css-variables", "design-tokens"];
2354
+ spinner.succeed(chalk.green("Configuration loaded"));
2355
+ const outputDir = path3.resolve(options.output);
2356
+ await fs3.ensureDir(outputDir);
2357
+ const orchestrator = new Orchestrator({ config, logger });
2358
+ spinner.start(chalk.blue(`Extracting UI from ${chalk.cyan(url)} and generating Next.js project...`));
2359
+ const startTime = Date.now();
2360
+ const projectDir = await orchestrator.extractAndGenerate(url, outputDir, options.name);
2361
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(2);
2362
+ spinner.succeed(chalk.green(`Next.js project generated in ${duration}s`));
2363
+ console.log("\n" + chalk.bold("\u{1F680} Next.js Project Generated:"));
2364
+ console.log(chalk.gray("\u2500".repeat(40)));
2365
+ console.log(` ${chalk.yellow("\u{1F4C1} Location:")} ${chalk.cyan(projectDir)}`);
2366
+ const listDir = async (dir, prefix = "") => {
2367
+ const items = await fs3.readdir(dir);
2368
+ for (const item of items.slice(0, 10)) {
2369
+ const itemPath = path3.join(dir, item);
2370
+ const stat = await fs3.stat(itemPath);
2371
+ if (stat.isDirectory()) {
2372
+ console.log(` ${prefix}${chalk.blue(item + "/")}`);
2373
+ if (prefix.length < 4) {
2374
+ await listDir(itemPath, prefix + " ");
2375
+ }
2376
+ } else {
2377
+ console.log(` ${prefix}${chalk.green(item)}`);
2378
+ }
2379
+ }
2380
+ if (items.length > 10) {
2381
+ console.log(` ${prefix}${chalk.gray(`... and ${items.length - 10} more`)}`);
2382
+ }
2383
+ };
2384
+ await listDir(projectDir);
2385
+ console.log(chalk.gray("\u2500".repeat(40)));
2386
+ console.log("\n" + chalk.bold("\u{1F4CB} Next Steps:"));
2387
+ console.log(` ${chalk.cyan("1.")} cd ${path3.relative(process.cwd(), projectDir)}`);
2388
+ console.log(` ${chalk.cyan("2.")} npm install`);
2389
+ console.log(` ${chalk.cyan("3.")} npm run dev`);
2390
+ console.log("\n" + chalk.green.bold("\u2705 Done!"));
2391
+ } catch (error) {
2392
+ spinner.fail(chalk.red("Generation failed"));
2393
+ logger.error(error instanceof Error ? error.message : String(error));
2394
+ if (options.verbose && error instanceof Error) {
2395
+ console.error(error.stack);
2396
+ }
2397
+ process.exit(1);
2398
+ }
2399
+ });
2400
+ program.command("analyze <url>").description("Analyze a website without generating output").option("--spa <framework>", "SPA framework (auto, react, vue, angular, svelte)", "auto").option("--timeout <ms>", "Navigation timeout", "30000").action(async (url, options) => {
2401
+ const spinner = ora();
2402
+ const logger = createLogger({ level: "info" });
2403
+ try {
2404
+ spinner.start(chalk.blue(`Analyzing ${url}...`));
2405
+ const config = getDefaultConfig();
2406
+ config.spa.framework = options.spa;
2407
+ config.browser.timeout = parseInt(options.timeout);
2408
+ config.output.formats = [];
2409
+ const orchestrator = new Orchestrator({ config, logger });
2410
+ const result = await orchestrator.analyze(url);
2411
+ spinner.succeed(chalk.green("Analysis complete"));
2412
+ console.log("\n" + chalk.bold("\u{1F4CA} Page Analysis:"));
2413
+ console.log(chalk.gray("\u2500".repeat(40)));
2414
+ console.log(` ${chalk.yellow("URL:")} ${result.metadata.url}`);
2415
+ console.log(` ${chalk.yellow("Title:")} ${result.metadata.title}`);
2416
+ console.log(` ${chalk.yellow("SPA Framework:")} ${result.metadata.spaFramework || "None detected"}`);
2417
+ console.log(` ${chalk.yellow("Elements Analyzed:")} ${result.metadata.elementsAnalyzed}`);
2418
+ console.log(` ${chalk.yellow("CSS Variables:")} ${Object.keys(result.cssVariables).length}`);
2419
+ console.log(chalk.gray("\u2500".repeat(40)));
2420
+ console.log("\n" + chalk.bold("\u{1F3A8} Colors:"));
2421
+ console.log(` Palette: ${result.colors.palette.length}`);
2422
+ console.log(` Gradients: ${result.colors.gradients.length}`);
2423
+ console.log("\n" + chalk.bold("\u{1F4DD} Typography:"));
2424
+ console.log(` Font Families: ${result.typography.fontFamilies.length}`);
2425
+ console.log(` Font Sizes: ${result.typography.fontSizes.length}`);
2426
+ console.log(` Text Styles: ${result.typography.textStyles.length}`);
2427
+ console.log("\n" + chalk.bold("\u{1F4D0} Layout:"));
2428
+ console.log(` Spacing Values: ${result.spacing.scale.length}`);
2429
+ console.log("\n" + chalk.bold("\u2728 Effects:"));
2430
+ console.log(` Shadows: ${result.effects.shadows.length}`);
2431
+ console.log(` Border Radii: ${result.effects.borderRadii.length}`);
2432
+ console.log("\n" + chalk.bold("\u{1F3AC} Animations:"));
2433
+ console.log(` Keyframes: ${result.animations.keyframes.length}`);
2434
+ console.log(` Transitions: ${result.animations.transitions.length}`);
2435
+ } catch (error) {
2436
+ spinner.fail(chalk.red("Analysis failed"));
2437
+ logger.error(error instanceof Error ? error.message : String(error));
2438
+ process.exit(1);
2439
+ }
2440
+ });
2441
+ program.command("clone <url>").description("Create a pixel-perfect Next.js clone of a website").option("-o, --output <dir>", "Output directory", "./extraktor-output").option("-n, --name <name>", "Project name").option("--timeout <ms>", "Navigation timeout", "60000").option("--content", "Extract content to content.json for easy editing", false).option("--crawl", "Crawl and clone all linked pages", false).option("--handover", "Create complete handover package (tokens, git, zip)", false).option("--no-ai", "Disable AI component generation (use raw HTML fallback)").option("--api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env var) - REQUIRED").option("-v, --verbose", "Verbose output").action(async (url, options) => {
2442
+ const spinner = ora();
2443
+ const logger = createLogger({
2444
+ level: options.verbose ? "debug" : "info"
2445
+ });
2446
+ try {
2447
+ let parsedUrl;
2448
+ try {
2449
+ parsedUrl = new URL(url);
2450
+ } catch {
2451
+ try {
2452
+ parsedUrl = new URL(`https://${url}`);
2453
+ url = parsedUrl.toString();
2454
+ } catch {
2455
+ console.error(chalk.red(`Invalid URL: ${url}`));
2456
+ process.exit(1);
2457
+ }
2458
+ }
2459
+ const projectName = options.name || parsedUrl.hostname.replace(/\./g, "-");
2460
+ const outputDir = path3.resolve(options.output);
2461
+ await fs3.ensureDir(outputDir);
2462
+ spinner.start(chalk.blue(`Cloning ${chalk.cyan(url)}...`));
2463
+ const config = getDefaultConfig();
2464
+ config.browser.timeout = parseInt(options.timeout);
2465
+ const driver = new PlaywrightDriver({
2466
+ browserConfig: config.browser,
2467
+ spaConfig: config.spa,
2468
+ logger
2469
+ });
2470
+ await driver.initialize();
2471
+ const navResult = await driver.navigate(url);
2472
+ if (!navResult.success) {
2473
+ throw new Error(`Navigation failed: ${navResult.error}`);
2474
+ }
2475
+ const page = driver.getPage();
2476
+ if (!page) {
2477
+ throw new Error("Failed to get page");
2478
+ }
2479
+ await page.waitForTimeout(3e3);
2480
+ const cloner = new SiteCloner();
2481
+ const startTime = Date.now();
2482
+ const result = await cloner.clone(page, {
2483
+ url,
2484
+ outputDir,
2485
+ projectName,
2486
+ downloadAssets: true,
2487
+ inlineStyles: true,
2488
+ extractContent: options.content,
2489
+ crawlPages: options.crawl
2490
+ });
2491
+ await driver.close();
2492
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(2);
2493
+ spinner.succeed(chalk.green(`Cloned in ${duration}s`));
2494
+ console.log("\n" + chalk.bold("\u{1F3AF} Pixel-Perfect Clone Created:"));
2495
+ console.log(chalk.gray("\u2500".repeat(40)));
2496
+ console.log(` ${chalk.yellow("\u{1F4C1} Location:")} ${chalk.cyan(result.projectDir)}`);
2497
+ console.log(` ${chalk.yellow("\u{1F310} Platform:")} ${chalk.magenta(result.platform)}`);
2498
+ console.log(` ${chalk.yellow("\u{1F5BC}\uFE0F Images:")} ${result.assets.images}`);
2499
+ console.log(` ${chalk.yellow("\u{1F3AC} Videos:")} ${result.assets.videos}`);
2500
+ console.log(` ${chalk.yellow("\u{1F524} Fonts:")} ${result.assets.fonts}`);
2501
+ console.log(` ${chalk.yellow("\u{1F4C4} Pages:")} ${result.pagesCloned}`);
2502
+ if (result.contentExtracted) {
2503
+ console.log(` ${chalk.yellow("\u{1F4DD} Content:")} ${chalk.green("Extracted to content.json")}`);
2504
+ }
2505
+ console.log(chalk.gray("\u2500".repeat(40)));
2506
+ console.log("\n" + chalk.bold("\u{1F4CB} Next Steps:"));
2507
+ console.log(` ${chalk.cyan("1.")} cd ${path3.relative(process.cwd(), result.projectDir)}`);
2508
+ console.log(` ${chalk.cyan("2.")} npm install`);
2509
+ console.log(` ${chalk.cyan("3.")} npm run dev`);
2510
+ if (result.contentExtracted) {
2511
+ console.log(` ${chalk.cyan("4.")} Edit ${chalk.cyan("public/content.json")} to customize text & images`);
2512
+ }
2513
+ if (options.ai !== false) {
2514
+ const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
2515
+ if (!apiKey) {
2516
+ spinner.fail(chalk.red("ANTHROPIC_API_KEY is required"));
2517
+ console.error(chalk.red("\n\u274C API key required for AI component generation"));
2518
+ console.error(chalk.gray(" Set ANTHROPIC_API_KEY environment variable or use --api-key flag"));
2519
+ console.error(chalk.gray(" Example: ANTHROPIC_API_KEY=sk-ant-... extraktor clone <url>"));
2520
+ console.error(chalk.gray("\n Use --no-ai flag to skip AI generation (not recommended)"));
2521
+ process.exit(1);
2522
+ } else {
2523
+ spinner.start(chalk.blue("Transforming to clean React components using AI (Claude)..."));
2524
+ try {
2525
+ const transformer = new Transformer();
2526
+ const transformResult = await transformer.transform({
2527
+ inputDir: result.projectDir,
2528
+ components: true,
2529
+ tokens: true,
2530
+ tailwind: true,
2531
+ useAI: true,
2532
+ apiKey,
2533
+ onProgress: (msg) => {
2534
+ spinner.text = chalk.blue(msg);
2535
+ }
2536
+ });
2537
+ if (transformResult.aiGenerated && transformResult.components.length > 0) {
2538
+ spinner.succeed(chalk.green("AI transformation complete!"));
2539
+ console.log("\n" + chalk.bold("\u{1F916} AI-Generated Components:"));
2540
+ console.log(chalk.gray("\u2500".repeat(40)));
2541
+ transformResult.components.forEach((name) => {
2542
+ console.log(` ${chalk.cyan("\u2022")} ${name}.tsx`);
2543
+ });
2544
+ console.log(chalk.gray("\u2500".repeat(40)));
2545
+ if (transformResult.errors.length > 0) {
2546
+ console.log(chalk.yellow(`\u26A0 ${transformResult.errors.length} warnings: ${transformResult.errors.slice(0, 3).join(", ")}${transformResult.errors.length > 3 ? "..." : ""}`));
2547
+ }
2548
+ console.log(chalk.green("\u2728 Clean, editable React components generated!"));
2549
+ } else {
2550
+ spinner.warn(chalk.yellow("AI transformation had issues, components may use fallback"));
2551
+ if (transformResult.errors && transformResult.errors.length > 0) {
2552
+ console.log(chalk.yellow(" Errors: " + transformResult.errors.slice(0, 5).join(", ")));
2553
+ }
2554
+ }
2555
+ } catch (transformError) {
2556
+ spinner.warn(chalk.yellow("AI transformation failed"));
2557
+ console.log(chalk.yellow(` Error: ${transformError instanceof Error ? transformError.message : transformError}`));
2558
+ console.log(chalk.gray(` Clone is still available at: ${result.projectDir}`));
2559
+ console.log(chalk.gray(` Run 'extraktor transform ${result.projectDir}' to retry`));
2560
+ }
2561
+ }
2562
+ }
2563
+ if (options.handover) {
2564
+ spinner.start(chalk.blue("Creating handover package..."));
2565
+ const transformer = new Transformer();
2566
+ await transformer.transform({
2567
+ inputDir: result.projectDir,
2568
+ components: false,
2569
+ // Skip components for clean handover
2570
+ tokens: true,
2571
+ tailwind: true
2572
+ });
2573
+ const contentJson = {
2574
+ brand: {
2575
+ name: "Your Brand",
2576
+ tagline: "Your tagline here",
2577
+ logo: "/images/logo.png"
2578
+ },
2579
+ hero: {
2580
+ headline: "Your headline here",
2581
+ subheadline: "Your description here",
2582
+ cta: { text: "Get Started", href: "#" }
2583
+ },
2584
+ features: [
2585
+ { title: "Feature 1", description: "Description here" },
2586
+ { title: "Feature 2", description: "Description here" },
2587
+ { title: "Feature 3", description: "Description here" }
2588
+ ],
2589
+ footer: {
2590
+ copyright: "\xA9 2025 Your Brand. All rights reserved.",
2591
+ links: [
2592
+ { label: "About", href: "/about" },
2593
+ { label: "Contact", href: "/contact" }
2594
+ ]
2595
+ }
2596
+ };
2597
+ await fs3.writeJSON(path3.join(result.projectDir, "content.json"), contentJson, { spaces: 2 });
2598
+ const handoverMd = `# Project Handover
2599
+
2600
+ ## Quick Start
2601
+ \`\`\`bash
2602
+ npm install
2603
+ npm run dev
2604
+ \`\`\`
2605
+ Visit http://localhost:3000
2606
+
2607
+ ## How to Customize
2608
+
2609
+ ### Colors & Fonts
2610
+ Edit \`src/styles/tokens.css\` - all colors and fonts are CSS variables.
2611
+
2612
+ ### Content
2613
+ Edit \`content.json\` - or ask AI to help: *"Change the headline to X"*
2614
+
2615
+ ### Images
2616
+ Replace files in \`public/images/\`
2617
+
2618
+ ## Deploy to Vercel
2619
+ \`\`\`bash
2620
+ git add . && git commit -m "Ready to deploy"
2621
+ git push
2622
+ \`\`\`
2623
+ Then import repo at vercel.com
2624
+
2625
+ ## Need Help?
2626
+ Ask AI: "Change the primary color to blue" or "Update all button text"
2627
+ `;
2628
+ await fs3.writeFile(path3.join(result.projectDir, "HANDOVER.md"), handoverMd);
2629
+ const gitignore = `node_modules/
2630
+ .next/
2631
+ .env
2632
+ .env.local
2633
+ .DS_Store
2634
+ `;
2635
+ await fs3.writeFile(path3.join(result.projectDir, ".gitignore"), gitignore);
2636
+ const { execSync } = await import("child_process");
2637
+ try {
2638
+ execSync('git init && git add . && git commit -m "Initial clone - ready for customization"', {
2639
+ cwd: result.projectDir,
2640
+ stdio: "pipe"
2641
+ });
2642
+ } catch (e) {
2643
+ }
2644
+ const zipName = `${projectName}.zip`;
2645
+ const zipPath = path3.join(outputDir, zipName);
2646
+ try {
2647
+ execSync(`zip -r "${zipPath}" "${projectName}" -x "*.git*" -x "*node_modules*" -x "*.next*"`, {
2648
+ cwd: outputDir,
2649
+ stdio: "pipe"
2650
+ });
2651
+ } catch (e) {
2652
+ }
2653
+ spinner.succeed(chalk.green("Handover package created!"));
2654
+ console.log("\n" + chalk.bold("\u{1F4E6} Handover Package:"));
2655
+ console.log(chalk.gray("\u2500".repeat(40)));
2656
+ console.log(` ${chalk.yellow("\u{1F4C1} Project:")} ${chalk.cyan(result.projectDir)}`);
2657
+ if (await fs3.pathExists(zipPath)) {
2658
+ const zipStats = await fs3.stat(zipPath);
2659
+ const sizeMB = (zipStats.size / 1024 / 1024).toFixed(1);
2660
+ console.log(` ${chalk.yellow("\u{1F4E6} Zip:")} ${chalk.cyan(zipPath)} (${sizeMB}MB)`);
2661
+ }
2662
+ console.log(` ${chalk.yellow("\u{1F3A8} Tokens:")} ${chalk.cyan("src/styles/tokens.css")}`);
2663
+ console.log(` ${chalk.yellow("\u{1F4DD} Content:")} ${chalk.cyan("content.json")}`);
2664
+ console.log(` ${chalk.yellow("\u{1F4D6} Guide:")} ${chalk.cyan("HANDOVER.md")}`);
2665
+ console.log(chalk.gray("\u2500".repeat(40)));
2666
+ console.log("\n" + chalk.bold("\u{1F680} Share with your team:"));
2667
+ console.log(` ${chalk.cyan("unzip")} ${zipName} && cd ${projectName} && npm install && npm run dev`);
2668
+ }
2669
+ console.log("\n" + chalk.green.bold("\u2705 Done!"));
2670
+ if (!options.handover) {
2671
+ console.log(chalk.gray(`
2672
+ Tip: Run 'extraktor clone <url> --handover' to create a complete handover package`));
2673
+ }
2674
+ } catch (error) {
2675
+ spinner.fail(chalk.red("Clone failed"));
2676
+ console.error(error instanceof Error ? error.message : String(error));
2677
+ if (options.verbose && error instanceof Error) {
2678
+ console.error(error.stack);
2679
+ }
2680
+ process.exit(1);
2681
+ }
2682
+ });
2683
+ program.command("handover <dir>").description("Create a handover package from an existing clone").option("-o, --output <dir>", "Output directory for zip file").option("--no-zip", "Skip zip file creation").option("-v, --verbose", "Verbose output").action(async (dir, options) => {
2684
+ const spinner = ora();
2685
+ try {
2686
+ const projectDir = path3.resolve(dir);
2687
+ const projectName = path3.basename(projectDir);
2688
+ if (!await fs3.pathExists(path3.join(projectDir, "public/page-content.html"))) {
2689
+ console.error(chalk.red(`Error: ${dir} is not a valid extraktor clone.`));
2690
+ process.exit(1);
2691
+ }
2692
+ spinner.start(chalk.blue("Creating handover package..."));
2693
+ if (!await fs3.pathExists(path3.join(projectDir, "src/styles/tokens.css"))) {
2694
+ const transformer = new Transformer();
2695
+ await transformer.transform({
2696
+ inputDir: projectDir,
2697
+ components: false,
2698
+ tokens: true,
2699
+ tailwind: true
2700
+ });
2701
+ }
2702
+ if (!await fs3.pathExists(path3.join(projectDir, "content.json"))) {
2703
+ const contentJson = {
2704
+ brand: { name: "Your Brand", tagline: "Your tagline", logo: "/images/logo.png" },
2705
+ hero: { headline: "Your headline", subheadline: "Your description", cta: { text: "Get Started", href: "#" } },
2706
+ features: [
2707
+ { title: "Feature 1", description: "Description" },
2708
+ { title: "Feature 2", description: "Description" }
2709
+ ],
2710
+ footer: { copyright: "\xA9 2025 Your Brand", links: [] }
2711
+ };
2712
+ await fs3.writeJSON(path3.join(projectDir, "content.json"), contentJson, { spaces: 2 });
2713
+ }
2714
+ if (!await fs3.pathExists(path3.join(projectDir, "HANDOVER.md"))) {
2715
+ const handoverMd = `# Project Handover
2716
+
2717
+ ## Quick Start
2718
+ \`\`\`bash
2719
+ npm install && npm run dev
2720
+ \`\`\`
2721
+
2722
+ ## Customize
2723
+ - Colors: \`src/styles/tokens.css\`
2724
+ - Content: \`content.json\`
2725
+ - Images: \`public/images/\`
2726
+ `;
2727
+ await fs3.writeFile(path3.join(projectDir, "HANDOVER.md"), handoverMd);
2728
+ }
2729
+ if (!await fs3.pathExists(path3.join(projectDir, ".gitignore"))) {
2730
+ await fs3.writeFile(path3.join(projectDir, ".gitignore"), "node_modules/\n.next/\n.env\n.DS_Store\n");
2731
+ }
2732
+ const { execSync } = await import("child_process");
2733
+ if (!await fs3.pathExists(path3.join(projectDir, ".git"))) {
2734
+ try {
2735
+ execSync('git init && git add . && git commit -m "Initial clone"', { cwd: projectDir, stdio: "pipe" });
2736
+ } catch (e) {
2737
+ }
2738
+ }
2739
+ let zipPath = "";
2740
+ if (options.zip !== false) {
2741
+ const outputDir = options.output ? path3.resolve(options.output) : path3.dirname(projectDir);
2742
+ const zipName = `${projectName}.zip`;
2743
+ zipPath = path3.join(outputDir, zipName);
2744
+ try {
2745
+ if (await fs3.pathExists(zipPath)) await fs3.remove(zipPath);
2746
+ execSync(`zip -r "${zipPath}" "${projectName}" -x "*.git*" -x "*node_modules*" -x "*.next*"`, {
2747
+ cwd: path3.dirname(projectDir),
2748
+ stdio: "pipe"
2749
+ });
2750
+ } catch (e) {
2751
+ console.log(chalk.yellow("Note: Could not create zip file"));
2752
+ }
2753
+ }
2754
+ spinner.succeed(chalk.green("Handover package ready!"));
2755
+ console.log("\n" + chalk.bold("\u{1F4E6} Handover Package:"));
2756
+ console.log(chalk.gray("\u2500".repeat(40)));
2757
+ console.log(` ${chalk.yellow("\u{1F4C1} Project:")} ${chalk.cyan(projectDir)}`);
2758
+ if (zipPath && await fs3.pathExists(zipPath)) {
2759
+ const zipStats = await fs3.stat(zipPath);
2760
+ const sizeMB = (zipStats.size / 1024 / 1024).toFixed(1);
2761
+ console.log(` ${chalk.yellow("\u{1F4E6} Zip:")} ${chalk.cyan(zipPath)} (${sizeMB}MB)`);
2762
+ }
2763
+ console.log(chalk.gray("\u2500".repeat(40)));
2764
+ console.log("\n" + chalk.bold("\u{1F680} Share:"));
2765
+ if (zipPath) {
2766
+ console.log(` ${chalk.cyan(`unzip ${path3.basename(zipPath)} && cd ${projectName} && npm i && npm run dev`)}`);
2767
+ }
2768
+ console.log("\n" + chalk.green.bold("\u2705 Done!"));
2769
+ } catch (error) {
2770
+ spinner.fail(chalk.red("Handover failed"));
2771
+ console.error(error instanceof Error ? error.message : String(error));
2772
+ process.exit(1);
2773
+ }
2774
+ });
2775
+ program.command("transform <dir>").description("Transform a cloned site into React components + design tokens").option("--components", "Generate React components (Header, Hero, Footer, etc.)", true).option("--no-components", "Skip component generation").option("--tokens", "Extract design tokens to CSS variables", true).option("--no-tokens", "Skip token extraction").option("--tailwind", "Generate Tailwind config with extracted colors", true).option("--no-tailwind", "Skip Tailwind config generation").option("--no-ai", "Disable AI component generation (use raw HTML fallback)").option("--api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env var) - REQUIRED").option("-o, --output <dir>", "Output directory (default: same as input)").option("-v, --verbose", "Verbose output").action(async (dir, options) => {
2776
+ const spinner = ora();
2777
+ try {
2778
+ const inputDir = path3.resolve(dir);
2779
+ if (!await fs3.pathExists(path3.join(inputDir, "public/page-content.html"))) {
2780
+ console.error(chalk.red(`Error: ${dir} is not a valid extraktor clone.`));
2781
+ console.error(chalk.gray(`Run 'extraktor clone <url>' first to create a clone.`));
2782
+ process.exit(1);
2783
+ }
2784
+ const useAI = options.ai !== false;
2785
+ const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
2786
+ if (useAI && !apiKey) {
2787
+ console.error(chalk.red("\n\u274C ANTHROPIC_API_KEY is required for AI component generation"));
2788
+ console.error(chalk.gray(" Set ANTHROPIC_API_KEY environment variable or use --api-key flag"));
2789
+ console.error(chalk.gray(" Example: ANTHROPIC_API_KEY=sk-ant-... extraktor transform <dir>"));
2790
+ console.error(chalk.gray("\n Use --no-ai flag to skip AI generation (not recommended)"));
2791
+ process.exit(1);
2792
+ }
2793
+ if (useAI) {
2794
+ spinner.start(chalk.blue("Transforming clone into React components using AI (Claude)..."));
2795
+ } else {
2796
+ spinner.start(chalk.blue("Transforming clone into React components (fallback mode)..."));
2797
+ }
2798
+ const transformer = new Transformer();
2799
+ const result = await transformer.transform({
2800
+ inputDir,
2801
+ outputDir: options.output,
2802
+ components: options.components,
2803
+ tokens: options.tokens,
2804
+ tailwind: options.tailwind,
2805
+ useAI,
2806
+ apiKey,
2807
+ onProgress: useAI ? (msg) => {
2808
+ spinner.text = chalk.blue(msg);
2809
+ } : void 0
2810
+ });
2811
+ if (useAI && result.aiGenerated) {
2812
+ spinner.succeed(chalk.green("AI transformation complete!"));
2813
+ } else if (useAI && !result.aiGenerated) {
2814
+ spinner.warn(chalk.yellow("AI transformation failed, fell back to standard generation"));
2815
+ if (result.errors && result.errors.length > 0) {
2816
+ console.log(chalk.yellow(" Errors: " + result.errors.join(", ")));
2817
+ }
2818
+ } else {
2819
+ spinner.succeed(chalk.green("Transformation complete!"));
2820
+ }
2821
+ console.log("\n" + chalk.bold("\u{1F504} Transformation Results:"));
2822
+ console.log(chalk.gray("\u2500".repeat(40)));
2823
+ if (result.components.length > 0) {
2824
+ console.log(` ${chalk.yellow("\u{1F4E6} Components:")} ${result.components.length}`);
2825
+ result.components.forEach((name) => {
2826
+ console.log(` ${chalk.cyan("\u2022")} ${name}.tsx`);
2827
+ });
2828
+ }
2829
+ if (result.tokensFile) {
2830
+ console.log(` ${chalk.yellow("\u{1F3A8} Tokens:")} ${chalk.cyan(path3.relative(process.cwd(), result.tokensFile))}`);
2831
+ }
2832
+ if (result.tailwindConfig) {
2833
+ console.log(` ${chalk.yellow("\u{1F30A} Tailwind:")} ${chalk.cyan(path3.relative(process.cwd(), result.tailwindConfig))}`);
2834
+ }
2835
+ console.log(chalk.gray("\u2500".repeat(40)));
2836
+ console.log("\n" + chalk.bold("\u{1F4CB} Next Steps:"));
2837
+ console.log(` ${chalk.cyan("1.")} Review generated components in ${chalk.cyan("src/components/")}`);
2838
+ console.log(` ${chalk.cyan("2.")} Edit ${chalk.cyan("src/styles/tokens.css")} to customize colors/fonts`);
2839
+ console.log(` ${chalk.cyan("3.")} Import components in your pages as needed`);
2840
+ console.log("\n" + chalk.green.bold("\u2705 Ready for handover!"));
2841
+ } catch (error) {
2842
+ spinner.fail(chalk.red("Transform failed"));
2843
+ console.error(error instanceof Error ? error.message : String(error));
2844
+ if (options.verbose && error instanceof Error) {
2845
+ console.error(error.stack);
2846
+ }
2847
+ process.exit(1);
2848
+ }
2849
+ });
2850
+ program.command("theme <url>").description("Generate a theme package from a website that can be installed in existing projects").option("-o, --output <dir>", "Output directory", "./extraktor-theme").option("-n, --name <name>", "Package name", "@rainbow/theme").option("--dark-mode", "Generate dark mode variants", false).option("--no-components", "Exclude component utilities").option("--no-tailwind", "Exclude Tailwind preset").option("--no-css", "Exclude CSS variables").option("--timeout <ms>", "Navigation timeout", "60000").option("--custom-color <name:value...>", "Add custom colors (e.g., brand:#FF6B35)").option("-v, --verbose", "Verbose output").action(async (url, options) => {
2851
+ const spinner = ora();
2852
+ const logger = createLogger({
2853
+ level: options.verbose ? "debug" : "info"
2854
+ });
2855
+ try {
2856
+ let parsedUrl;
2857
+ try {
2858
+ parsedUrl = new URL(url);
2859
+ } catch {
2860
+ try {
2861
+ parsedUrl = new URL(`https://${url}`);
2862
+ url = parsedUrl.toString();
2863
+ } catch {
2864
+ logger.error(`Invalid URL: ${url}`);
2865
+ process.exit(1);
2866
+ }
2867
+ }
2868
+ spinner.start(chalk.blue(`Extracting theme from ${chalk.cyan(url)}...`));
2869
+ const customColors = {};
2870
+ if (options.customColor) {
2871
+ for (const colorDef of Array.isArray(options.customColor) ? options.customColor : [options.customColor]) {
2872
+ const [name, value] = colorDef.split(":");
2873
+ if (name && value) {
2874
+ customColors[name] = value;
2875
+ }
2876
+ }
2877
+ }
2878
+ const config = getDefaultConfig();
2879
+ config.browser.headless = true;
2880
+ config.browser.timeout = parseInt(options.timeout);
2881
+ config.output.formats = ["design-tokens"];
2882
+ const orchestrator = new Orchestrator({ config, logger });
2883
+ const startTime = Date.now();
2884
+ const tempDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "extraktor-"));
2885
+ const result = await orchestrator.extract(url, tempDir);
2886
+ const extractDuration = ((Date.now() - startTime) / 1e3).toFixed(2);
2887
+ spinner.text = chalk.blue(`Extracted theme in ${extractDuration}s. Generating package...`);
2888
+ const simpleTokens = {
2889
+ colors: result.colors.palette.map((c) => ({ name: c.name, value: c.value, type: "color" })),
2890
+ fonts: result.typography.fontFamilies.map((f) => ({ name: f.name, value: f.value, type: "font" })),
2891
+ spacing: result.spacing.scale.map((s) => ({ name: s.name, value: s.value, type: "spacing" })),
2892
+ borderRadius: result.effects.borderRadii.map((br) => ({ name: br.name, value: br.value, type: "radius" })),
2893
+ shadows: result.effects.shadows.map((sh) => ({ name: sh.name, value: sh.value, type: "shadow" }))
2894
+ };
2895
+ const { ThemePackageGenerator } = await import("../theme-package-generator-E55BBBZN.js");
2896
+ const generator = new ThemePackageGenerator();
2897
+ const outputDir = path3.resolve(options.output);
2898
+ await generator.generate(simpleTokens, {
2899
+ packageName: options.name,
2900
+ outputDir,
2901
+ includeComponents: options.components !== false,
2902
+ includeTailwindPreset: options.tailwind !== false,
2903
+ includeCSSVariables: options.css !== false,
2904
+ darkMode: options.darkMode,
2905
+ customColors: Object.keys(customColors).length > 0 ? customColors : void 0
2906
+ });
2907
+ await fs3.remove(tempDir);
2908
+ const totalDuration = ((Date.now() - startTime) / 1e3).toFixed(2);
2909
+ spinner.succeed(chalk.green(`Theme package generated in ${totalDuration}s`));
2910
+ console.log("\n" + chalk.bold("\u{1F4E6} Theme Package Created:"));
2911
+ console.log(chalk.gray("\u2500".repeat(40)));
2912
+ console.log(` ${chalk.yellow("\u{1F4C1} Location:")} ${chalk.cyan(outputDir)}`);
2913
+ console.log(` ${chalk.yellow("\u{1F4E6} Package:")} ${chalk.cyan(options.name)}`);
2914
+ console.log(` ${chalk.yellow("\u{1F3A8} Colors:")} ${simpleTokens.colors.length}`);
2915
+ console.log(` ${chalk.yellow("\u{1F524} Fonts:")} ${simpleTokens.fonts.length}`);
2916
+ if (options.darkMode) {
2917
+ console.log(` ${chalk.yellow("\u{1F319} Dark Mode:")} ${chalk.green("Enabled")}`);
2918
+ }
2919
+ if (Object.keys(customColors).length > 0) {
2920
+ console.log(` ${chalk.yellow("\u{1F3AF} Custom Colors:")} ${chalk.cyan(Object.keys(customColors).join(", "))}`);
2921
+ }
2922
+ console.log(chalk.gray("\u2500".repeat(40)));
2923
+ console.log("\n" + chalk.bold("\u{1F4CB} Next Steps:"));
2924
+ console.log(` ${chalk.cyan("1.")} cd ${path3.relative(process.cwd(), outputDir)}`);
2925
+ console.log(` ${chalk.cyan("2.")} npm install`);
2926
+ console.log(` ${chalk.cyan("3.")} npm run build`);
2927
+ console.log(` ${chalk.cyan("4.")} npm publish ${chalk.gray("(or npm link for local development)")}`);
2928
+ console.log("\n" + chalk.bold("\u{1F4D6} Usage in Your Project:"));
2929
+ console.log(` ${chalk.gray("$")} npm install ${options.name}`);
2930
+ console.log(` ${chalk.gray("#")} Then import in your code:`);
2931
+ console.log(` ${chalk.gray("import")} { theme } ${chalk.gray("from")} '${options.name}'`);
2932
+ console.log(` ${chalk.gray("import")} '${options.name}/css'`);
2933
+ console.log("\n" + chalk.green.bold("\u2705 Done!"));
2934
+ } catch (error) {
2935
+ spinner.fail(chalk.red("Theme package generation failed"));
2936
+ console.error(error instanceof Error ? error.message : String(error));
2937
+ if (options.verbose && error instanceof Error) {
2938
+ console.error(error.stack);
2939
+ }
2940
+ process.exit(1);
2941
+ }
2942
+ });
2943
+ program.command("design <url>").description("Extract a complete design system from a website (colors, typography, components, Tailwind preset)").option("-o, --output <dir>", "Output directory", "./design-system").option("-n, --name <name>", "Package name (auto-generated from URL if not provided)").option("--api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env var) - REQUIRED").option("--timeout <ms>", "Navigation timeout", "60000").option("-v, --verbose", "Verbose output").action(async (url, options) => {
2944
+ const spinner = ora();
2945
+ try {
2946
+ let parsedUrl;
2947
+ try {
2948
+ parsedUrl = new URL(url);
2949
+ } catch {
2950
+ try {
2951
+ parsedUrl = new URL(`https://${url}`);
2952
+ url = parsedUrl.toString();
2953
+ } catch {
2954
+ console.error(chalk.red(`Invalid URL: ${url}`));
2955
+ process.exit(1);
2956
+ }
2957
+ }
2958
+ const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
2959
+ const packageName = options.name || `${parsedUrl.hostname.replace(/\./g, "-")}-design-system`;
2960
+ const outputDir = path3.resolve(options.output);
2961
+ spinner.start(chalk.blue(`Extracting design system from ${chalk.cyan(url)}...`));
2962
+ const extractor = new DesignExtractor(apiKey);
2963
+ const startTime = Date.now();
2964
+ const designSystem = await extractor.extract(url, outputDir);
2965
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(2);
2966
+ spinner.succeed(chalk.green(`Design system extracted in ${duration}s`));
2967
+ console.log("\n" + chalk.bold("\u{1F3A8} Design System Package Created:"));
2968
+ console.log(chalk.gray("\u2500".repeat(50)));
2969
+ console.log(` ${chalk.yellow("\u{1F4C1} Location:")} ${chalk.cyan(outputDir)}`);
2970
+ console.log(` ${chalk.yellow("\u{1F4E6} Package:")} ${chalk.cyan(packageName)}`);
2971
+ console.log(` ${chalk.yellow("\u{1F310} Source:")} ${chalk.gray(url)}`);
2972
+ console.log(chalk.gray("\u2500".repeat(50)));
2973
+ console.log("\n" + chalk.bold("\u{1F4CA} Design Tokens:"));
2974
+ const colorCount = Object.keys(designSystem.colors.solid).length + designSystem.colors.gradients.length + designSystem.colors.rgba.length;
2975
+ console.log(` ${chalk.yellow("\u{1F3A8} Colors:")} ${colorCount} (${Object.keys(designSystem.colors.solid).length} solid, ${designSystem.colors.gradients.length} gradients, ${designSystem.colors.rgba.length} rgba)`);
2976
+ Object.entries(designSystem.colors.solid).slice(0, 5).forEach(([name, value]) => {
2977
+ try {
2978
+ console.log(` ${chalk.gray("\u2022")} ${name}: ${chalk.hex(value)(value)}`);
2979
+ } catch {
2980
+ console.log(` ${chalk.gray("\u2022")} ${name}: ${value}`);
2981
+ }
2982
+ });
2983
+ if (Object.keys(designSystem.colors.solid).length > 5) {
2984
+ console.log(` ${chalk.gray(`... and ${Object.keys(designSystem.colors.solid).length - 5} more`)}`);
2985
+ }
2986
+ console.log(` ${chalk.yellow("\u{1F524} Fonts:")} ${designSystem.typography.fontFamilies.length} families, ${designSystem.typography.fontSizes.length} sizes`);
2987
+ console.log(` ${chalk.yellow("\u{1F4D0} Spacing:")} ${designSystem.spacing.all.length} values`);
2988
+ console.log(` ${chalk.yellow("\u2B1C Radius:")} ${designSystem.borders.radius.length} values`);
2989
+ console.log(` ${chalk.yellow("\u{1F32B}\uFE0F Shadows:")} ${designSystem.shadows.boxShadows.length} box, ${designSystem.shadows.dropShadows.length} drop`);
2990
+ console.log(` ${chalk.yellow("\u{1F3AC} Transitions:")} ${designSystem.animations.transitions.full.length} unique`);
2991
+ console.log(` ${chalk.yellow("\u{1F4D0} Layout:")} ${designSystem.layout.displays.length} displays, ${Object.keys(designSystem.layout.breakpoints).length} breakpoints`);
2992
+ console.log("\n" + chalk.bold("\u{1F4E6} Component Patterns:"));
2993
+ console.log(` ${chalk.cyan("\u2022")} Buttons: ${designSystem.components.buttons.length}`);
2994
+ console.log(` ${chalk.cyan("\u2022")} Inputs: ${designSystem.components.inputs.length}`);
2995
+ console.log(` ${chalk.cyan("\u2022")} Cards: ${designSystem.components.cards.length}`);
2996
+ console.log(` ${chalk.cyan("\u2022")} Links: ${designSystem.components.links.length}`);
2997
+ console.log(` ${chalk.cyan("\u2022")} Headings: ${designSystem.components.headings.length}`);
2998
+ console.log(` ${chalk.cyan("\u2022")} Nav Items: ${designSystem.components.navItems.length}`);
2999
+ console.log(` ${chalk.cyan("\u2022")} Badges: ${designSystem.components.badges.length}`);
3000
+ console.log(chalk.gray("\u2500".repeat(50)));
3001
+ console.log("\n" + chalk.bold("\u{1F4C4} Files Generated:"));
3002
+ console.log(` ${chalk.green("\u2713")} design-tokens/colors.css`);
3003
+ console.log(` ${chalk.green("\u2713")} design-tokens/typography.css`);
3004
+ console.log(` ${chalk.green("\u2713")} design-tokens/spacing.css`);
3005
+ console.log(` ${chalk.green("\u2713")} tailwind.preset.js`);
3006
+ console.log(` ${chalk.green("\u2713")} components/*.tsx`);
3007
+ console.log(` ${chalk.green("\u2713")} package.json`);
3008
+ console.log(` ${chalk.green("\u2713")} design-system.json`);
3009
+ console.log(chalk.gray("\u2500".repeat(50)));
3010
+ console.log("\n" + chalk.bold("\u{1F4CB} Usage:"));
3011
+ console.log(chalk.gray(" Option 1: Copy files to your project"));
3012
+ console.log(` ${chalk.cyan("cp -r")} ${outputDir}/design-tokens ${chalk.cyan("your-project/src/")}`);
3013
+ console.log(` ${chalk.cyan("cp")} ${outputDir}/tailwind.preset.js ${chalk.cyan("your-project/")}`);
3014
+ console.log(chalk.gray("\n Option 2: Use as Tailwind preset"));
3015
+ console.log(` ${chalk.gray("// tailwind.config.js")}`);
3016
+ console.log(` ${chalk.cyan("module.exports")} = {`);
3017
+ console.log(` ${chalk.cyan("presets")}: [require('${outputDir}/tailwind.preset')],`);
3018
+ console.log(` }`);
3019
+ console.log(chalk.gray("\n Option 3: Import CSS variables"));
3020
+ console.log(` ${chalk.gray("@import '")}${outputDir}/design-tokens/colors.css${chalk.gray("';")}`);
3021
+ console.log(` ${chalk.gray("@import '")}${outputDir}/design-tokens/typography.css${chalk.gray("';")}`);
3022
+ console.log("\n" + chalk.green.bold("\u2705 Done!"));
3023
+ console.log(chalk.gray(`
3024
+ Tip: Run 'cat ${outputDir}/design-system.json' to see the full extracted design system`));
3025
+ } catch (error) {
3026
+ spinner.fail(chalk.red("Design extraction failed"));
3027
+ console.error(error instanceof Error ? error.message : String(error));
3028
+ if (options.verbose && error instanceof Error) {
3029
+ console.error(error.stack);
3030
+ }
3031
+ process.exit(1);
3032
+ }
3033
+ });
3034
+ program.command("apply <source>").description("Apply styles from a website to your existing project components").requiredOption("--to <project>", "Target project directory").option("--dry-run", "Show what would be changed without making changes").option("-v, --verbose", "Verbose output").action(async (source, options) => {
3035
+ const spinner = ora();
3036
+ try {
3037
+ let sourceUrl;
3038
+ try {
3039
+ const parsed = new URL(source);
3040
+ sourceUrl = parsed.toString();
3041
+ } catch {
3042
+ try {
3043
+ const parsed = new URL(`https://${source}`);
3044
+ sourceUrl = parsed.toString();
3045
+ } catch {
3046
+ console.error(chalk.red(`Invalid source URL: ${source}`));
3047
+ process.exit(1);
3048
+ }
3049
+ }
3050
+ const targetProject = path3.resolve(options.to);
3051
+ if (!await fs3.pathExists(targetProject)) {
3052
+ console.error(chalk.red(`Target project not found: ${targetProject}`));
3053
+ process.exit(1);
3054
+ }
3055
+ const hasPackageJson = await fs3.pathExists(path3.join(targetProject, "package.json"));
3056
+ const hasSrcFolder = await fs3.pathExists(path3.join(targetProject, "src"));
3057
+ if (!hasPackageJson && !hasSrcFolder) {
3058
+ console.error(chalk.red(`Invalid project directory: ${targetProject}`));
3059
+ console.error(chalk.gray(" Expected a project with package.json or src/ folder"));
3060
+ process.exit(1);
3061
+ }
3062
+ console.log("\n" + chalk.bold("\u{1F3A8} Extraktor Apply"));
3063
+ console.log(chalk.gray("\u2500".repeat(50)));
3064
+ console.log(` ${chalk.yellow("\u{1F4E5} Source:")} ${chalk.cyan(sourceUrl)}`);
3065
+ console.log(` ${chalk.yellow("\u{1F4C1} Target:")} ${chalk.cyan(targetProject)}`);
3066
+ console.log(chalk.gray("\u2500".repeat(50)));
3067
+ if (options.dryRun) {
3068
+ console.log(chalk.yellow("\n\u26A0\uFE0F Dry run mode - no changes will be made\n"));
3069
+ }
3070
+ const { StyleApplier } = await import("../style-applier-BMHP6V57.js");
3071
+ const applier = new StyleApplier();
3072
+ const startTime = Date.now();
3073
+ await applier.apply(sourceUrl, targetProject);
3074
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(2);
3075
+ console.log(chalk.gray("\u2500".repeat(50)));
3076
+ console.log(`
3077
+ ${chalk.green.bold("\u2705 Styles applied in " + duration + "s")}`);
3078
+ console.log(chalk.gray("\nYour components now have the style of " + sourceUrl));
3079
+ console.log(chalk.gray("Run your dev server to see the changes!"));
3080
+ } catch (error) {
3081
+ spinner.fail(chalk.red("Style application failed"));
3082
+ console.error(error instanceof Error ? error.message : String(error));
3083
+ if (options.verbose && error instanceof Error) {
3084
+ console.error(error.stack);
3085
+ }
3086
+ process.exit(1);
3087
+ }
3088
+ });
3089
+ program.command("genome <url>").description("Reverse-engineer a website into a portable design system using Vision AI").option("-o, --output <dir>", "Output directory").option("--api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY)").option("--model <model>", "Claude model to use", "claude-sonnet-4-6-20250514").option("--max-components <n>", "Max components to synthesize", "20").option("--skip-storybook", "Skip Storybook generation").option("--skip-screenshots", "Skip saving screenshots to output").option("--headless", "Run browser in headless mode", true).option("--no-headless", "Run browser in visible mode").option("--devtools [endpoint]", "Connect to existing Chrome via DevTools Protocol (default: http://localhost:9222)").option("--timeout <ms>", "Navigation timeout", "30000").option("-v, --verbose", "Verbose output").action(async (url, options) => {
3090
+ const spinner = ora();
3091
+ const logger = createLogger({
3092
+ level: options.verbose ? "debug" : "info"
3093
+ });
3094
+ try {
3095
+ try {
3096
+ new URL(url);
3097
+ } catch {
3098
+ try {
3099
+ url = new URL(`https://${url}`).toString();
3100
+ } catch {
3101
+ console.error(chalk.red(`Invalid URL: ${url}`));
3102
+ process.exit(1);
3103
+ }
3104
+ }
3105
+ const hostname = new URL(url).hostname.replace(/\./g, "-").replace(/^www-/, "");
3106
+ const outputDir = options.output || `./${hostname}-genome`;
3107
+ let cdpEndpoint;
3108
+ if (options.devtools) {
3109
+ cdpEndpoint = typeof options.devtools === "string" ? options.devtools : "http://localhost:9222";
3110
+ spinner.start(chalk.blue(`Connecting to Chrome DevTools at ${chalk.cyan(cdpEndpoint)}...`));
3111
+ } else {
3112
+ spinner.start(chalk.blue(`Extracting design genome from ${chalk.cyan(url)}...`));
3113
+ }
3114
+ const engine = new GenomeEngine({ logger });
3115
+ const startTime = Date.now();
3116
+ const genome = await engine.genome({
3117
+ url,
3118
+ outputDir,
3119
+ cdpEndpoint,
3120
+ apiKey: options.apiKey,
3121
+ model: options.model,
3122
+ maxComponents: parseInt(options.maxComponents),
3123
+ skipStorybook: options.skipStorybook,
3124
+ skipScreenshots: options.skipScreenshots,
3125
+ headless: options.headless,
3126
+ timeout: parseInt(options.timeout),
3127
+ verbose: options.verbose
3128
+ });
3129
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
3130
+ spinner.succeed(chalk.green(`Genome extracted in ${duration}s`));
3131
+ console.log("\n" + chalk.bold("Genome Extracted:"));
3132
+ console.log(chalk.gray("-".repeat(50)));
3133
+ console.log(` ${chalk.yellow("Source:")} ${chalk.cyan(genome.meta.sourceUrl)}`);
3134
+ console.log(` ${chalk.yellow("Components:")} ${genome.meta.componentCount}`);
3135
+ console.log(` ${chalk.yellow("Colors:")} ${Object.keys(genome.theme.colors).length}`);
3136
+ console.log(` ${chalk.yellow("Fonts:")} ${Object.keys(genome.theme.typography.families).length}`);
3137
+ console.log(` ${chalk.yellow("Sections:")} ${genome.layout.sectionOrder.join(" -> ")}`);
3138
+ console.log("\n" + chalk.bold("Components:"));
3139
+ genome.components.forEach((c) => {
3140
+ console.log(` ${chalk.green("*")} ${chalk.bold(c.name)} (${c.type}) - ${c.description}`);
3141
+ });
3142
+ console.log(chalk.gray("-".repeat(50)));
3143
+ console.log(`
3144
+ ${chalk.bold("Output:")} ${chalk.cyan(path3.resolve(outputDir))}`);
3145
+ console.log("\n" + chalk.bold("Next Steps:"));
3146
+ console.log(` ${chalk.cyan("1.")} Browse components in ${chalk.cyan(outputDir + "/components/")}`);
3147
+ if (!options.skipStorybook) {
3148
+ console.log(` ${chalk.cyan("2.")} Run Storybook: ${chalk.gray(`cd ${outputDir}/stories && npx storybook dev`)}`);
3149
+ }
3150
+ console.log(` ${chalk.cyan("3.")} Regen a new page: ${chalk.gray(`extraktor regen ${outputDir} --prompt "Your page idea"`)}`);
3151
+ console.log("\n" + chalk.green.bold("Done!"));
3152
+ } catch (error) {
3153
+ spinner.fail(chalk.red("Genome extraction failed"));
3154
+ logger.error(error instanceof Error ? error.message : String(error));
3155
+ if (options.verbose && error instanceof Error) {
3156
+ console.error(error.stack);
3157
+ }
3158
+ process.exit(1);
3159
+ }
3160
+ });
3161
+ program.command("regen <genome-dir>").description("Generate a new page using a previously extracted design genome").requiredOption("--prompt <text>", "Content/purpose description for the new page").option("-o, --output <dir>", "Output directory", "./regen-output").option("--api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY)").option("--model <model>", "Claude model to use", "claude-sonnet-4-6-20250514").option("--sections <types>", "Specific sections to generate (comma-separated)").option("-v, --verbose", "Verbose output").action(async (genomeDir, options) => {
3162
+ const spinner = ora();
3163
+ const logger = createLogger({
3164
+ level: options.verbose ? "debug" : "info"
3165
+ });
3166
+ try {
3167
+ spinner.start(chalk.blue("Regenerating page from genome..."));
3168
+ const engine = new GenomeEngine({ logger });
3169
+ const startTime = Date.now();
3170
+ const outputDir = await engine.regen({
3171
+ genomeDir,
3172
+ prompt: options.prompt,
3173
+ outputDir: options.output,
3174
+ apiKey: options.apiKey,
3175
+ model: options.model,
3176
+ sections: options.sections?.split(",").map((s) => s.trim()),
3177
+ verbose: options.verbose
3178
+ });
3179
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
3180
+ spinner.succeed(chalk.green(`Page regenerated in ${duration}s`));
3181
+ console.log("\n" + chalk.bold("Page Generated:"));
3182
+ console.log(chalk.gray("-".repeat(40)));
3183
+ console.log(` ${chalk.yellow("Output:")} ${chalk.cyan(path3.resolve(outputDir))}`);
3184
+ console.log(` ${chalk.yellow("Prompt:")} ${options.prompt}`);
3185
+ console.log(chalk.gray("-".repeat(40)));
3186
+ console.log("\n" + chalk.bold("Next Steps:"));
3187
+ console.log(` ${chalk.cyan("1.")} cd ${path3.relative(process.cwd(), outputDir)}`);
3188
+ console.log(` ${chalk.cyan("2.")} npm install`);
3189
+ console.log(` ${chalk.cyan("3.")} npm run dev`);
3190
+ console.log("\n" + chalk.green.bold("Done!"));
3191
+ } catch (error) {
3192
+ spinner.fail(chalk.red("Regeneration failed"));
3193
+ logger.error(error instanceof Error ? error.message : String(error));
3194
+ if (options.verbose && error instanceof Error) {
3195
+ console.error(error.stack);
3196
+ }
3197
+ process.exit(1);
3198
+ }
3199
+ });
3200
+ program.command("design-md <url>").description("Generate a DESIGN.md file (Stitch format) from any website").option("-o, --output <file>", "Output file path", "./DESIGN.md").option("--timeout <ms>", "Navigation timeout", "30000").option("--headless", "Run browser in headless mode", true).option("--no-headless", "Run browser in visible mode").option("--devtools [endpoint]", "Connect to existing Chrome via DevTools Protocol").option("-v, --verbose", "Verbose output").action(async (url, options) => {
3201
+ const spinner = ora();
3202
+ const logger = createLogger({
3203
+ level: options.verbose ? "debug" : "info"
3204
+ });
3205
+ try {
3206
+ try {
3207
+ new URL(url);
3208
+ } catch {
3209
+ try {
3210
+ url = new URL(`https://${url}`).toString();
3211
+ } catch {
3212
+ console.error(chalk.red(`Invalid URL: ${url}`));
3213
+ process.exit(1);
3214
+ }
3215
+ }
3216
+ spinner.start(chalk.blue(`Generating DESIGN.md from ${chalk.cyan(url)}...`));
3217
+ const config = getDefaultConfig();
3218
+ config.browser.headless = options.headless;
3219
+ config.browser.timeout = parseInt(options.timeout);
3220
+ config.output.formats = ["tailwind", "css-variables", "design-tokens"];
3221
+ if (options.devtools) {
3222
+ config.browser.cdpEndpoint = typeof options.devtools === "string" ? options.devtools : "http://localhost:9222";
3223
+ config.browser.headless = false;
3224
+ }
3225
+ const orchestrator = new Orchestrator({ config, logger });
3226
+ const startTime = Date.now();
3227
+ const tmpDir = path3.join("/tmp", `extraktor-design-md-${Date.now()}`);
3228
+ await fs3.ensureDir(tmpDir);
3229
+ const result = await orchestrator.extract(url, tmpDir);
3230
+ const { DesignMdGenerator } = await import("../design-md-generator-YMQOE2IW.js");
3231
+ const generator = new DesignMdGenerator({ logger });
3232
+ const outputPath = path3.resolve(options.output);
3233
+ await generator.generate(result, outputPath, url);
3234
+ await fs3.remove(tmpDir);
3235
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
3236
+ spinner.succeed(chalk.green(`DESIGN.md generated in ${duration}s`));
3237
+ console.log("\n" + chalk.bold("DESIGN.md Generated:"));
3238
+ console.log(chalk.gray("-".repeat(40)));
3239
+ console.log(` ${chalk.yellow("File:")} ${chalk.cyan(outputPath)}`);
3240
+ console.log(` ${chalk.yellow("Colors:")} ${result.colors.palette.length}`);
3241
+ console.log(` ${chalk.yellow("Fonts:")} ${result.typography.fontFamilies.length}`);
3242
+ console.log(` ${chalk.yellow("Animations:")} ${result.animations.keyframes.length}`);
3243
+ console.log(chalk.gray("-".repeat(40)));
3244
+ console.log(`
3245
+ Drop this file into any project and tell your AI agent:`);
3246
+ console.log(chalk.cyan(` "Build me a page that follows DESIGN.md"`));
3247
+ console.log("\n" + chalk.green.bold("Done!"));
3248
+ } catch (error) {
3249
+ spinner.fail(chalk.red("DESIGN.md generation failed"));
3250
+ logger.error(error instanceof Error ? error.message : String(error));
3251
+ if (options.verbose && error instanceof Error) {
3252
+ console.error(error.stack);
3253
+ }
3254
+ process.exit(1);
3255
+ }
3256
+ });
3257
+ program.command("mcp", { hidden: true }).action(async () => {
3258
+ const { startMcpServer } = await import("../server-DR7RCM5S.js");
3259
+ await startMcpServer();
3260
+ });
3261
+ program.parse();