design-clone 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +42 -20
  2. package/SKILL.md +74 -0
  3. package/bin/commands/clone-site.js +75 -10
  4. package/bin/commands/init.js +33 -1
  5. package/bin/commands/verify.js +5 -3
  6. package/bin/utils/validate.js +24 -8
  7. package/docs/cli-reference.md +224 -2
  8. package/docs/codebase-summary.md +309 -0
  9. package/docs/design-clone-architecture.md +290 -45
  10. package/docs/pixel-perfect.md +35 -4
  11. package/docs/project-roadmap.md +382 -0
  12. package/docs/troubleshooting.md +5 -4
  13. package/package.json +12 -6
  14. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  15. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  16. package/src/ai/analyze-structure.py +73 -3
  17. package/src/ai/extract-design-tokens.py +356 -13
  18. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  20. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  21. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  22. package/src/ai/prompts/design_tokens.py +133 -0
  23. package/src/ai/prompts/structure_analysis.py +329 -10
  24. package/src/ai/prompts/ux_audit.py +198 -0
  25. package/src/ai/ux-audit.js +596 -0
  26. package/src/core/animation-extractor.js +526 -0
  27. package/src/core/app-state-snapshot.js +511 -0
  28. package/src/core/content-counter.js +342 -0
  29. package/src/core/cookie-handler.js +1 -1
  30. package/src/core/css-extractor.js +4 -4
  31. package/src/core/dimension-extractor.js +93 -21
  32. package/src/core/dimension-output.js +103 -6
  33. package/src/core/discover-pages.js +242 -14
  34. package/src/core/dom-tree-analyzer.js +298 -0
  35. package/src/core/extract-assets.js +1 -1
  36. package/src/core/framework-detector.js +538 -0
  37. package/src/core/html-extractor.js +45 -4
  38. package/src/core/lazy-loader.js +7 -7
  39. package/src/core/multi-page-screenshot.js +9 -6
  40. package/src/core/page-readiness.js +8 -8
  41. package/src/core/screenshot.js +311 -7
  42. package/src/core/section-cropper.js +209 -0
  43. package/src/core/section-detector.js +386 -0
  44. package/src/core/semantic-enhancer.js +492 -0
  45. package/src/core/state-capture.js +598 -0
  46. package/src/core/tests/test-section-cropper.js +177 -0
  47. package/src/core/tests/test-section-detector.js +55 -0
  48. package/src/core/video-capture.js +546 -0
  49. package/src/route-discoverers/angular-discoverer.js +157 -0
  50. package/src/route-discoverers/astro-discoverer.js +123 -0
  51. package/src/route-discoverers/base-discoverer.js +242 -0
  52. package/src/route-discoverers/index.js +106 -0
  53. package/src/route-discoverers/next-discoverer.js +130 -0
  54. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  55. package/src/route-discoverers/react-discoverer.js +139 -0
  56. package/src/route-discoverers/svelte-discoverer.js +109 -0
  57. package/src/route-discoverers/universal-discoverer.js +227 -0
  58. package/src/route-discoverers/vue-discoverer.js +118 -0
  59. package/src/utils/__init__.py +1 -1
  60. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  61. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  62. package/src/utils/browser.js +11 -37
  63. package/src/utils/playwright.js +213 -0
  64. package/src/verification/generate-audit-report.js +398 -0
  65. package/src/verification/verify-footer.js +493 -0
  66. package/src/verification/verify-header.js +486 -0
  67. package/src/verification/verify-layout.js +2 -2
  68. package/src/verification/verify-menu.js +4 -20
  69. package/src/verification/verify-slider.js +533 -0
  70. package/src/utils/puppeteer.js +0 -281
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Vue Route Discoverer
3
+ *
4
+ * Extracts routes from Vue 2 and Vue 3 applications using:
5
+ * - Vue 3: app.__vue_app__.$router
6
+ * - Vue 2: window.Vue.prototype.$router
7
+ * - Fallback: data-v-* attributes and router-link elements
8
+ */
9
+
10
+ import { BaseDiscoverer } from './base-discoverer.js';
11
+
12
+ export class VueDiscoverer extends BaseDiscoverer {
13
+ /**
14
+ * Discover routes from a Vue application
15
+ * @returns {Promise<import('./base-discoverer.js').DiscoveredRoute[]>}
16
+ */
17
+ async discover() {
18
+ const rawRoutes = await this.page.evaluate(() => {
19
+ const routes = [];
20
+
21
+ /**
22
+ * Recursively extract routes from Vue Router config
23
+ */
24
+ function extractRoutes(routeList, prefix = '') {
25
+ if (!Array.isArray(routeList)) return;
26
+
27
+ routeList.forEach(r => {
28
+ if (!r.path && r.path !== '') return;
29
+
30
+ let path = r.path;
31
+ if (!path.startsWith('/') && prefix) {
32
+ path = prefix + (prefix.endsWith('/') ? '' : '/') + path;
33
+ } else if (!path.startsWith('/') && path !== '') {
34
+ path = '/' + path;
35
+ }
36
+
37
+ // Handle root path
38
+ if (path === '') path = '/';
39
+
40
+ routes.push({
41
+ path,
42
+ name: r.name || '',
43
+ component: r.component?.name || r.name || '',
44
+ source: 'framework'
45
+ });
46
+
47
+ if (r.children) {
48
+ extractRoutes(r.children, path);
49
+ }
50
+ });
51
+ }
52
+
53
+ // Method 1: Vue 3 - __vue_app__ on root element
54
+ const appElements = document.querySelectorAll('[data-v-app], #app, #__nuxt');
55
+ for (const el of appElements) {
56
+ const vueApp = el.__vue_app__;
57
+ if (vueApp?.config?.globalProperties?.$router?.options?.routes) {
58
+ extractRoutes(vueApp.config.globalProperties.$router.options.routes);
59
+ break;
60
+ }
61
+ }
62
+
63
+ // Method 2: Vue 2 - window.Vue.prototype.$router
64
+ if (routes.length === 0 && window.Vue?.prototype?.$router?.options?.routes) {
65
+ extractRoutes(window.Vue.prototype.$router.options.routes);
66
+ }
67
+
68
+ // Method 3: __VUE_ROUTER__ global (some configurations)
69
+ if (routes.length === 0 && window.__VUE_ROUTER__?.options?.routes) {
70
+ extractRoutes(window.__VUE_ROUTER__.options.routes);
71
+ }
72
+
73
+ // Method 4: router-link elements
74
+ document.querySelectorAll('router-link, a[href]').forEach(link => {
75
+ let href = link.getAttribute('to') || link.getAttribute('href');
76
+ if (href && href.startsWith('/')) {
77
+ const text = link.textContent?.trim();
78
+ const isVueComponent = link.tagName.toLowerCase() === 'router-link' ||
79
+ link.closest('[data-v-]');
80
+
81
+ if (!routes.some(r => r.path === href)) {
82
+ routes.push({
83
+ path: href,
84
+ name: text || '',
85
+ source: isVueComponent ? 'framework' : 'link-scrape'
86
+ });
87
+ }
88
+ }
89
+ });
90
+
91
+ // Method 5: Links with data-v-* attributes (scoped styles indicate Vue components)
92
+ if (routes.length === 0) {
93
+ document.querySelectorAll('nav a, header a, [role="navigation"] a').forEach(link => {
94
+ const href = link.getAttribute('href');
95
+ if (href && href.startsWith('/')) {
96
+ routes.push({
97
+ path: href,
98
+ name: link.textContent?.trim() || '',
99
+ source: 'link-scrape'
100
+ });
101
+ }
102
+ });
103
+ }
104
+
105
+ return routes;
106
+ });
107
+
108
+ const processedRoutes = rawRoutes.map(route => ({
109
+ ...route,
110
+ name: route.name || this.extractPageName(route.path, route.component),
111
+ path: this.normalizeRoute(route.path)
112
+ }));
113
+
114
+ return this.deduplicateRoutes(processedRoutes);
115
+ }
116
+ }
117
+
118
+ export default VueDiscoverer;
@@ -3,7 +3,7 @@ Design Clone skill library modules.
3
3
 
4
4
  JavaScript modules:
5
5
  - browser.js: Browser abstraction facade
6
- - puppeteer.js: Standalone Puppeteer wrapper
6
+ - playwright.js: Playwright browser wrapper
7
7
  - utils.js: CLI utilities
8
8
  - env.js: Environment variable resolution
9
9
 
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * Browser abstraction facade for design-clone scripts
3
3
  *
4
- * Auto-detects and uses:
5
- * 1. chrome-devtools skill (if installed) - Preferred
6
- * 2. Standalone puppeteer wrapper - Fallback
4
+ * Uses Playwright wrapper for browser automation.
7
5
  *
8
- * Exports same API regardless of provider:
6
+ * Exports same API:
9
7
  * - getBrowser(options)
10
8
  * - getPage(browser)
11
9
  * - closeBrowser()
@@ -15,18 +13,6 @@
15
13
  * - outputError(error)
16
14
  */
17
15
 
18
- import fs from 'fs';
19
- import path from 'path';
20
- import { fileURLToPath } from 'url';
21
-
22
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
-
24
- // Chrome DevTools skill path
25
- const CHROME_DEVTOOLS_PATH = path.join(
26
- process.env.HOME,
27
- '.claude/skills/chrome-devtools/scripts/lib/browser.js'
28
- );
29
-
30
16
  let browserModule = null;
31
17
  let providerName = 'unknown';
32
18
 
@@ -36,22 +22,9 @@ let providerName = 'unknown';
36
22
  async function initProvider() {
37
23
  if (browserModule) return;
38
24
 
39
- // Check for chrome-devtools skill
40
- if (fs.existsSync(CHROME_DEVTOOLS_PATH)) {
41
- try {
42
- browserModule = await import(CHROME_DEVTOOLS_PATH);
43
- providerName = 'chrome-devtools';
44
- console.error('[browser] Using chrome-devtools skill');
45
- return;
46
- } catch (e) {
47
- console.error('[browser] chrome-devtools found but failed to load:', e.message);
48
- }
49
- }
50
-
51
- // Fall back to standalone puppeteer wrapper
52
- browserModule = await import('./puppeteer.js');
53
- providerName = 'standalone';
54
- console.error('[browser] Using standalone puppeteer wrapper');
25
+ browserModule = await import('./playwright.js');
26
+ providerName = 'playwright';
27
+ console.error('[browser] Using Playwright wrapper');
55
28
  }
56
29
 
57
30
  // Import utilities (always use local helpers)
@@ -60,7 +33,7 @@ export { parseArgs, outputJSON, outputError };
60
33
 
61
34
  /**
62
35
  * Get current browser provider name
63
- * @returns {string} 'chrome-devtools' or 'standalone'
36
+ * @returns {string} 'playwright'
64
37
  */
65
38
  export function getProviderName() {
66
39
  return providerName;
@@ -79,15 +52,16 @@ export async function getBrowser(options = {}) {
79
52
  /**
80
53
  * Get page from browser
81
54
  * @param {Browser} browser - Browser instance
55
+ * @param {Object} [options] - Page options
82
56
  * @returns {Promise<Page>} Page instance
83
57
  */
84
- export async function getPage(browser) {
58
+ export async function getPage(browser, options = {}) {
85
59
  await initProvider();
86
- return browserModule.getPage(browser);
60
+ return browserModule.getPage(browser, options);
87
61
  }
88
62
 
89
63
  /**
90
- * Close browser and clear session
64
+ * Close browser
91
65
  */
92
66
  export async function closeBrowser() {
93
67
  await initProvider();
@@ -95,7 +69,7 @@ export async function closeBrowser() {
95
69
  }
96
70
 
97
71
  /**
98
- * Disconnect from browser without closing
72
+ * Disconnect from browser (alias for close in Playwright)
99
73
  */
100
74
  export async function disconnectBrowser() {
101
75
  await initProvider();
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Standalone Playwright browser wrapper for design-clone scripts
3
+ * Provides browser automation with Playwright
4
+ *
5
+ * Features:
6
+ * - Auto-detects Chrome installation path (macOS, Linux, Windows)
7
+ * - Fast browser launch (no session persistence needed)
8
+ * - Compatible API with previous Puppeteer wrapper
9
+ */
10
+
11
+ import fs from 'fs';
12
+
13
+ /** @type {import('playwright').Browser|null} */
14
+ let browserInstance = null;
15
+ /** @type {import('playwright').Page|null} */
16
+ let pageInstance = null;
17
+ /** @type {typeof import('playwright')|null} */
18
+ let playwright = null;
19
+
20
+ /** Default viewport dimensions */
21
+ const DEFAULT_VIEWPORT = { width: 1920, height: 1080 };
22
+
23
+ /**
24
+ * Detect Chrome executable path by platform
25
+ * Used for playwright-core fallback when full playwright is not installed
26
+ * @returns {string|null} Chrome path or null if not found
27
+ */
28
+ function detectChromePath() {
29
+ const platform = process.platform;
30
+
31
+ const paths = {
32
+ darwin: [
33
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
34
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
35
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
36
+ ],
37
+ linux: [
38
+ '/usr/bin/google-chrome',
39
+ '/usr/bin/google-chrome-stable',
40
+ '/usr/bin/chromium',
41
+ '/usr/bin/chromium-browser',
42
+ '/snap/bin/chromium'
43
+ ],
44
+ win32: [
45
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
46
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
47
+ ...(process.env.LOCALAPPDATA ? [`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`] : [])
48
+ ]
49
+ };
50
+
51
+ const candidates = paths[platform] || [];
52
+ for (const chromePath of candidates) {
53
+ if (fs.existsSync(chromePath)) {
54
+ return chromePath;
55
+ }
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Load playwright module (try playwright first, then playwright-core)
63
+ * @returns {Promise<Object>} Playwright module with chromium browser type
64
+ * @throws {Error} If neither playwright nor playwright-core is installed
65
+ */
66
+ async function loadPlaywright() {
67
+ if (playwright) return playwright;
68
+
69
+ try {
70
+ // Try full playwright first (includes bundled browsers)
71
+ playwright = await import('playwright');
72
+ return playwright;
73
+ } catch (e1) {
74
+ try {
75
+ // Fall back to playwright-core (requires Chrome)
76
+ playwright = await import('playwright-core');
77
+ return playwright;
78
+ } catch (e2) {
79
+ throw new Error(
80
+ 'Playwright not found. Install with: npm install playwright\n' +
81
+ 'Or for smaller install: npm install playwright-core\n' +
82
+ `Details: playwright: ${e1.message}, playwright-core: ${e2.message}`
83
+ );
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Launch browser instance
90
+ *
91
+ * @param {Object} options - Browser options
92
+ * @param {boolean} [options.headless=true] - Run in headless mode
93
+ * @param {Object} [options.viewport] - Default viewport dimensions (applied per context)
94
+ * @param {string} [options.executablePath] - Chrome executable path override
95
+ * @param {string[]} [options.args] - Additional Chrome arguments
96
+ * @returns {Promise<Browser>} Playwright browser instance
97
+ * @throws {Error} If Chrome not found and no executablePath provided (playwright-core)
98
+ */
99
+ export async function getBrowser(options = {}) {
100
+ const pw = await loadPlaywright();
101
+
102
+ // Reuse existing browser if connected
103
+ if (browserInstance && browserInstance.isConnected()) {
104
+ return browserInstance;
105
+ }
106
+
107
+ // Determine executable path for playwright-core
108
+ let executablePath = options.executablePath;
109
+ if (!executablePath) {
110
+ // Check if we're using playwright-core (no bundled browser)
111
+ const isCore = !pw.chromium?.executablePath;
112
+ if (isCore) {
113
+ executablePath = detectChromePath();
114
+ if (!executablePath) {
115
+ throw new Error(
116
+ 'Chrome not found. Either:\n' +
117
+ '1. Install Google Chrome\n' +
118
+ '2. Use full playwright (npm install playwright)\n' +
119
+ '3. Set executablePath option'
120
+ );
121
+ }
122
+ }
123
+ }
124
+
125
+ // Build launch options
126
+ const launchOptions = {
127
+ headless: options.headless !== false,
128
+ args: [
129
+ '--no-sandbox',
130
+ '--disable-setuid-sandbox',
131
+ '--disable-dev-shm-usage',
132
+ ...(options.args || [])
133
+ ]
134
+ };
135
+
136
+ // Only set executablePath if needed (playwright-core or override)
137
+ if (executablePath) {
138
+ launchOptions.executablePath = executablePath;
139
+ }
140
+
141
+ // Launch browser
142
+ browserInstance = await pw.chromium.launch(launchOptions);
143
+ console.error('[browser] Launched Playwright browser');
144
+
145
+ return browserInstance;
146
+ }
147
+
148
+ /**
149
+ * Get current page or create new one
150
+ * Reuses existing page if available
151
+ *
152
+ * @param {import('playwright').Browser} browser - Playwright browser instance
153
+ * @param {Object} [options] - Page options
154
+ * @param {{width: number, height: number}} [options.viewport] - Viewport dimensions
155
+ * @returns {Promise<import('playwright').Page>} Playwright page instance
156
+ * @throws {Error} If browser is null or disconnected
157
+ */
158
+ export async function getPage(browser, options = {}) {
159
+ if (!browser || !browser.isConnected()) {
160
+ throw new Error('Browser not connected. Call getBrowser() first.');
161
+ }
162
+
163
+ if (pageInstance && !pageInstance.isClosed()) {
164
+ return pageInstance;
165
+ }
166
+
167
+ // Get existing pages or create new context + page
168
+ const contexts = browser.contexts();
169
+ if (contexts.length > 0) {
170
+ const pages = contexts[0].pages();
171
+ if (pages.length > 0) {
172
+ pageInstance = pages[0];
173
+ return pageInstance;
174
+ }
175
+ }
176
+
177
+ // Create new context with default viewport
178
+ const contextOptions = {
179
+ viewport: options.viewport || DEFAULT_VIEWPORT
180
+ };
181
+
182
+ const context = await browser.newContext(contextOptions);
183
+ pageInstance = await context.newPage();
184
+
185
+ return pageInstance;
186
+ }
187
+
188
+ /**
189
+ * Close browser
190
+ * Use when completely done with browser
191
+ */
192
+ export async function closeBrowser() {
193
+ if (browserInstance) {
194
+ try {
195
+ await browserInstance.close();
196
+ } catch (err) {
197
+ console.error(`[browser] Error closing browser: ${err.message}`);
198
+ }
199
+ browserInstance = null;
200
+ pageInstance = null;
201
+ console.error('[browser] Closed browser');
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Disconnect from browser (alias for closeBrowser in Playwright)
207
+ * Playwright doesn't support disconnect without close, so this is an alias
208
+ */
209
+ export async function disconnectBrowser() {
210
+ // Playwright doesn't have disconnect concept like Puppeteer
211
+ // Just close the browser for API compatibility
212
+ return closeBrowser();
213
+ }