design-clone 1.0.2 → 1.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.
- package/SKILL.md +53 -0
- package/bin/cli.js +16 -0
- package/bin/commands/clone-site.js +324 -0
- package/bin/commands/help.js +16 -4
- package/docs/troubleshooting.md +72 -0
- package/package.json +1 -1
- package/src/core/css-extractor.js +38 -13
- package/src/core/design-tokens.js +103 -0
- package/src/core/discover-pages.js +314 -0
- package/src/core/html-extractor.js +72 -3
- package/src/core/merge-css.js +407 -0
- package/src/core/multi-page-screenshot.js +377 -0
- package/src/core/rewrite-links.js +226 -0
- package/src/core/screenshot.js +18 -1
package/SKILL.md
CHANGED
|
@@ -104,6 +104,54 @@ python3 $HOME/.claude/skills/ui-ux-pro-max/scripts/search.py "animation hover" -
|
|
|
104
104
|
|
|
105
105
|
**Output:** desktop.png, tablet.png, mobile.png, source.html, source.css, source-raw.css
|
|
106
106
|
|
|
107
|
+
### design:clone-site
|
|
108
|
+
|
|
109
|
+
Multi-page site cloning with shared CSS and working navigation.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
/design:clone-site https://example.com
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Workflow:**
|
|
116
|
+
```bash
|
|
117
|
+
# Auto-discover pages from navigation
|
|
118
|
+
design-clone clone-site https://example.com
|
|
119
|
+
|
|
120
|
+
# Or specify pages manually
|
|
121
|
+
design-clone clone-site https://example.com --pages /,/about,/contact
|
|
122
|
+
|
|
123
|
+
# Options:
|
|
124
|
+
# --pages <paths> Comma-separated paths
|
|
125
|
+
# --max-pages <n> Limit pages (default: 10)
|
|
126
|
+
# --viewports <list> Viewports (default: desktop,tablet,mobile)
|
|
127
|
+
# --yes Skip confirmation
|
|
128
|
+
# --output <dir> Custom output directory
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Output Structure:**
|
|
132
|
+
```
|
|
133
|
+
cloned-designs/{timestamp}-{domain}/
|
|
134
|
+
├── analysis/ # Screenshots by viewport
|
|
135
|
+
│ ├── desktop/*.png
|
|
136
|
+
│ ├── tablet/*.png
|
|
137
|
+
│ └── mobile/*.png
|
|
138
|
+
├── pages/ # HTML with rewritten links
|
|
139
|
+
│ ├── index.html
|
|
140
|
+
│ ├── about.html
|
|
141
|
+
│ └── contact.html
|
|
142
|
+
├── styles.css # Merged + deduplicated CSS
|
|
143
|
+
└── manifest.json # Page metadata + mapping
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Features:**
|
|
147
|
+
- Auto-discovers pages from navigation (SPA-aware)
|
|
148
|
+
- Shared CSS with deduplication (15-30% reduction)
|
|
149
|
+
- Working internal links
|
|
150
|
+
- Progress reporting
|
|
151
|
+
- Graceful error handling (continues on page failures)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
107
155
|
### design:clone-px
|
|
108
156
|
|
|
109
157
|
Pixel-perfect clone with full asset extraction and AI analysis.
|
|
@@ -223,6 +271,11 @@ GEMINI_API_KEY=your-key # Optional: enables AI structure analysis
|
|
|
223
271
|
| screenshot.js | src/core/ | Capture screenshots + extract HTML/CSS |
|
|
224
272
|
| filter-css.js | src/core/ | Filter unused CSS rules |
|
|
225
273
|
| extract-assets.js | src/core/ | Download images, fonts, icons |
|
|
274
|
+
| discover-pages.js | src/core/ | Discover navigation links |
|
|
275
|
+
| multi-page-screenshot.js | src/core/ | Capture multiple pages |
|
|
276
|
+
| merge-css.js | src/core/ | Merge + deduplicate CSS |
|
|
277
|
+
| rewrite-links.js | src/core/ | Rewrite internal links |
|
|
278
|
+
| clone-site.js | bin/commands/ | Multi-page clone CLI |
|
|
226
279
|
| analyze-structure.py | src/ai/ | AI-powered structure analysis |
|
|
227
280
|
| extract-design-tokens.py | src/ai/ | Extract colors, typography, spacing |
|
|
228
281
|
| verify-menu.js | src/verification/ | Validate navigation structure |
|
package/bin/cli.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* Usage:
|
|
6
6
|
* design-clone init [--force] Install skill to ~/.claude/skills/
|
|
7
7
|
* design-clone verify Check installation status
|
|
8
|
+
* design-clone clone-site <url> [options] Clone multiple pages
|
|
8
9
|
* design-clone help Show help
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { init } from './commands/init.js';
|
|
12
13
|
import { verify } from './commands/verify.js';
|
|
13
14
|
import { help } from './commands/help.js';
|
|
15
|
+
import { cloneSite, parseArgs as parseCloneSiteArgs, showHelp as showCloneSiteHelp } from './commands/clone-site.js';
|
|
14
16
|
|
|
15
17
|
const [,, command, ...args] = process.argv;
|
|
16
18
|
|
|
@@ -25,6 +27,20 @@ async function main() {
|
|
|
25
27
|
case 'check':
|
|
26
28
|
await verify();
|
|
27
29
|
break;
|
|
30
|
+
case 'clone-site':
|
|
31
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
32
|
+
showCloneSiteHelp();
|
|
33
|
+
} else {
|
|
34
|
+
const options = parseCloneSiteArgs(args);
|
|
35
|
+
if (!options.url) {
|
|
36
|
+
console.error('Error: URL is required');
|
|
37
|
+
showCloneSiteHelp();
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const result = await cloneSite(options.url, options);
|
|
41
|
+
console.log(JSON.stringify(result, null, 2));
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
28
44
|
case 'help':
|
|
29
45
|
case '--help':
|
|
30
46
|
case '-h':
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clone Site Command
|
|
3
|
+
*
|
|
4
|
+
* Clone multiple pages from a website with shared CSS and working navigation.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* design-clone clone-site <url> [options]
|
|
8
|
+
*
|
|
9
|
+
* Options:
|
|
10
|
+
* --pages <paths> Comma-separated paths (e.g., /,/about,/contact)
|
|
11
|
+
* --max-pages <n> Maximum pages to clone (default: 10)
|
|
12
|
+
* --viewports <list> Viewport list (default: desktop,tablet,mobile)
|
|
13
|
+
* --yes Skip confirmation prompt
|
|
14
|
+
* --output <dir> Custom output directory
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs/promises';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
|
|
21
|
+
import { discoverPages } from '../../src/core/discover-pages.js';
|
|
22
|
+
import { captureMultiplePages } from '../../src/core/multi-page-screenshot.js';
|
|
23
|
+
import { mergeCssFiles } from '../../src/core/merge-css.js';
|
|
24
|
+
import { rewriteLinks, createPageManifest, rewriteAllLinks } from '../../src/core/rewrite-links.js';
|
|
25
|
+
import { extractDesignTokens } from '../../src/core/design-tokens.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate output directory name
|
|
29
|
+
* @param {string} url - Target URL
|
|
30
|
+
* @returns {string} Output directory path
|
|
31
|
+
*/
|
|
32
|
+
function generateOutputDir(url) {
|
|
33
|
+
const urlObj = new URL(url);
|
|
34
|
+
const domain = urlObj.hostname.replace(/^www\./, '');
|
|
35
|
+
const timestamp = new Date().toISOString()
|
|
36
|
+
.replace(/[-:]/g, '')
|
|
37
|
+
.replace('T', '-')
|
|
38
|
+
.slice(0, 13);
|
|
39
|
+
|
|
40
|
+
return `./cloned-designs/${timestamp}-${domain}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse CLI arguments
|
|
45
|
+
* @param {string[]} args - CLI arguments
|
|
46
|
+
* @returns {Object} Parsed options
|
|
47
|
+
*/
|
|
48
|
+
export function parseArgs(args) {
|
|
49
|
+
const options = {
|
|
50
|
+
url: null,
|
|
51
|
+
pages: null,
|
|
52
|
+
maxPages: 10,
|
|
53
|
+
viewports: ['desktop', 'tablet', 'mobile'],
|
|
54
|
+
skipConfirm: false,
|
|
55
|
+
output: null,
|
|
56
|
+
ai: false
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < args.length; i++) {
|
|
60
|
+
const arg = args[i];
|
|
61
|
+
|
|
62
|
+
if (arg === '--pages' && args[i + 1]) {
|
|
63
|
+
options.pages = args[++i].split(',').map(p => p.trim());
|
|
64
|
+
} else if (arg === '--max-pages' && args[i + 1]) {
|
|
65
|
+
options.maxPages = parseInt(args[++i], 10);
|
|
66
|
+
} else if (arg === '--viewports' && args[i + 1]) {
|
|
67
|
+
options.viewports = args[++i].split(',').map(v => v.trim());
|
|
68
|
+
} else if (arg === '--yes' || arg === '-y') {
|
|
69
|
+
options.skipConfirm = true;
|
|
70
|
+
} else if (arg === '--output' && args[i + 1]) {
|
|
71
|
+
options.output = args[++i];
|
|
72
|
+
} else if (arg === '--ai') {
|
|
73
|
+
options.ai = true;
|
|
74
|
+
} else if (!arg.startsWith('--') && !options.url) {
|
|
75
|
+
options.url = arg;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clone multiple pages from a website
|
|
84
|
+
* @param {string} url - Target URL
|
|
85
|
+
* @param {Object} options - Clone options
|
|
86
|
+
* @returns {Promise<Object>} Clone result
|
|
87
|
+
*/
|
|
88
|
+
export async function cloneSite(url, options = {}) {
|
|
89
|
+
const startTime = Date.now();
|
|
90
|
+
const {
|
|
91
|
+
pages: manualPages,
|
|
92
|
+
maxPages = 10,
|
|
93
|
+
viewports = ['desktop', 'tablet', 'mobile'],
|
|
94
|
+
skipConfirm = false,
|
|
95
|
+
output,
|
|
96
|
+
ai = false
|
|
97
|
+
} = options;
|
|
98
|
+
|
|
99
|
+
// Validate URL
|
|
100
|
+
let baseUrl;
|
|
101
|
+
try {
|
|
102
|
+
baseUrl = new URL(url);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Generate output directory
|
|
108
|
+
const outputDir = output || generateOutputDir(url);
|
|
109
|
+
|
|
110
|
+
console.error(`\n[clone-site] Target: ${url}`);
|
|
111
|
+
console.error(`[clone-site] Output: ${outputDir}`);
|
|
112
|
+
|
|
113
|
+
// Step 1: Discover or use manual pages
|
|
114
|
+
console.error('\n[1/6] Discovering pages...');
|
|
115
|
+
|
|
116
|
+
let pageList;
|
|
117
|
+
if (manualPages && manualPages.length > 0) {
|
|
118
|
+
// Use manual page list
|
|
119
|
+
pageList = {
|
|
120
|
+
success: true,
|
|
121
|
+
pages: manualPages.map(p => ({
|
|
122
|
+
path: p,
|
|
123
|
+
name: p === '/' ? 'Home' : p.replace(/^\//, '').replace(/-/g, ' '),
|
|
124
|
+
url: new URL(p, url).href
|
|
125
|
+
}))
|
|
126
|
+
};
|
|
127
|
+
console.error(` Using ${pageList.pages.length} manual pages`);
|
|
128
|
+
} else {
|
|
129
|
+
// Auto-discover
|
|
130
|
+
pageList = await discoverPages(url, { maxPages });
|
|
131
|
+
if (!pageList.success) {
|
|
132
|
+
console.error(` Warning: Discovery failed - ${pageList.error}`);
|
|
133
|
+
console.error(' Falling back to homepage only');
|
|
134
|
+
}
|
|
135
|
+
console.error(` Found ${pageList.pages.length} pages`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Show discovered pages
|
|
139
|
+
for (const page of pageList.pages) {
|
|
140
|
+
console.error(` - ${page.path} (${page.name})`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Step 2: Capture all pages
|
|
144
|
+
console.error('\n[2/6] Capturing pages...');
|
|
145
|
+
|
|
146
|
+
const captureResult = await captureMultiplePages(pageList.pages, {
|
|
147
|
+
outputDir,
|
|
148
|
+
viewports,
|
|
149
|
+
onProgress: (current, total, info) => {
|
|
150
|
+
console.error(` [${current}/${total}] ${info.status}: ${info.name}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!captureResult.success) {
|
|
155
|
+
throw new Error(`Capture failed: ${captureResult.error}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.error(` Captured ${captureResult.stats.successfulPages}/${captureResult.stats.totalPages} pages`);
|
|
159
|
+
console.error(` Screenshots: ${captureResult.stats.totalScreenshots}`);
|
|
160
|
+
|
|
161
|
+
// Step 3: Merge CSS files (prefer filtered CSS)
|
|
162
|
+
console.error('\n[3/6] Merging CSS...');
|
|
163
|
+
|
|
164
|
+
const mergedCssPath = path.join(outputDir, 'styles.css');
|
|
165
|
+
let mergeResult = { success: false };
|
|
166
|
+
|
|
167
|
+
// Use filtered CSS if available, fallback to raw CSS
|
|
168
|
+
const cssToMerge = captureResult.cssFilesFiltered?.length > 0
|
|
169
|
+
? captureResult.cssFilesFiltered
|
|
170
|
+
: captureResult.cssFiles;
|
|
171
|
+
|
|
172
|
+
const cssType = captureResult.cssFilesFiltered?.length > 0 ? 'filtered' : 'raw';
|
|
173
|
+
|
|
174
|
+
if (cssToMerge.length > 0) {
|
|
175
|
+
mergeResult = await mergeCssFiles(cssToMerge, mergedCssPath);
|
|
176
|
+
if (mergeResult.success) {
|
|
177
|
+
console.error(` Merged ${mergeResult.input.fileCount} ${cssType} files`);
|
|
178
|
+
console.error(` Reduction: ${mergeResult.stats.reduction}`);
|
|
179
|
+
} else {
|
|
180
|
+
console.error(` Warning: Merge failed - ${mergeResult.error}`);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
console.error(' No CSS files to merge');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Step 4: Extract design tokens (if --ai flag)
|
|
187
|
+
console.error('\n[4/6] Extracting design tokens...');
|
|
188
|
+
|
|
189
|
+
let hasTokens = false;
|
|
190
|
+
if (ai) {
|
|
191
|
+
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
192
|
+
const tokenResult = await extractDesignTokens(outputDir, mergedCssPath);
|
|
193
|
+
if (tokenResult.success) {
|
|
194
|
+
hasTokens = true;
|
|
195
|
+
console.error(` Created: tokens.css, design-tokens.json`);
|
|
196
|
+
} else {
|
|
197
|
+
console.error(` Warning: Token extraction failed - ${tokenResult.error}`);
|
|
198
|
+
if (tokenResult.hint) {
|
|
199
|
+
console.error(` Hint: ${tokenResult.hint}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
console.error(' Skipped: GEMINI_API_KEY not set');
|
|
204
|
+
console.error(' Hint: Set GEMINI_API_KEY in ~/.claude/.env for AI token extraction');
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
console.error(' Skipped (use --ai flag to enable)');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Step 5: Rewrite links
|
|
211
|
+
console.error('\n[5/6] Rewriting links...');
|
|
212
|
+
|
|
213
|
+
const manifest = createPageManifest(pageList.pages, {
|
|
214
|
+
hasTokens,
|
|
215
|
+
stats: {
|
|
216
|
+
totalPages: pageList.pages.length,
|
|
217
|
+
totalScreenshots: captureResult.stats.totalScreenshots,
|
|
218
|
+
cssReduction: mergeResult.stats?.reduction || '0%',
|
|
219
|
+
captureTimeMs: captureResult.stats.totalTimeMs
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Copy HTML files to pages/ directory and rewrite links
|
|
224
|
+
const pagesDir = path.join(outputDir, 'pages');
|
|
225
|
+
await fs.mkdir(pagesDir, { recursive: true });
|
|
226
|
+
|
|
227
|
+
for (const page of manifest.pages) {
|
|
228
|
+
const sourceHtml = path.join(outputDir, 'html', page.file);
|
|
229
|
+
const destHtml = path.join(pagesDir, page.file);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
let html = await fs.readFile(sourceHtml, 'utf-8');
|
|
233
|
+
html = rewriteLinks(html, manifest, {
|
|
234
|
+
baseUrl: url,
|
|
235
|
+
injectTokensCss: hasTokens
|
|
236
|
+
});
|
|
237
|
+
await fs.writeFile(destHtml, html, 'utf-8');
|
|
238
|
+
console.error(` Rewritten: ${page.file}`);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(` Warning: Failed to rewrite ${page.file}: ${err.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Step 6: Generate manifest
|
|
245
|
+
console.error('\n[6/6] Generating manifest...');
|
|
246
|
+
|
|
247
|
+
const manifestPath = path.join(outputDir, 'manifest.json');
|
|
248
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
249
|
+
console.error(` Created: manifest.json`);
|
|
250
|
+
|
|
251
|
+
// Summary
|
|
252
|
+
const totalTime = Date.now() - startTime;
|
|
253
|
+
console.error(`\n[clone-site] Complete!`);
|
|
254
|
+
console.error(` Output: ${path.resolve(outputDir)}`);
|
|
255
|
+
console.error(` Pages: ${manifest.pages.length}`);
|
|
256
|
+
console.error(` Time: ${(totalTime / 1000).toFixed(1)}s`);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
outputDir: path.resolve(outputDir),
|
|
261
|
+
manifest,
|
|
262
|
+
captureResult,
|
|
263
|
+
mergeResult,
|
|
264
|
+
totalTimeMs: totalTime
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Show help message
|
|
270
|
+
*/
|
|
271
|
+
export function showHelp() {
|
|
272
|
+
console.log(`
|
|
273
|
+
Usage: design-clone clone-site <url> [options]
|
|
274
|
+
|
|
275
|
+
Clone multiple pages from a website with shared CSS and working navigation.
|
|
276
|
+
|
|
277
|
+
Options:
|
|
278
|
+
--pages <paths> Comma-separated paths (e.g., /,/about,/contact)
|
|
279
|
+
--max-pages <n> Maximum pages to auto-discover (default: 10)
|
|
280
|
+
--viewports <list> Viewport list (default: desktop,tablet,mobile)
|
|
281
|
+
--yes Skip confirmation prompt
|
|
282
|
+
--output <dir> Custom output directory
|
|
283
|
+
--ai Extract design tokens using Gemini AI (requires GEMINI_API_KEY)
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
design-clone clone-site https://example.com
|
|
287
|
+
design-clone clone-site https://example.com --max-pages 5
|
|
288
|
+
design-clone clone-site https://example.com --pages /,/about,/contact
|
|
289
|
+
design-clone clone-site https://example.com --ai
|
|
290
|
+
`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// CLI entry point
|
|
294
|
+
const isMainModule = process.argv[1] && (
|
|
295
|
+
process.argv[1].endsWith('clone-site.js') ||
|
|
296
|
+
process.argv[1].includes('clone-site')
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (isMainModule) {
|
|
300
|
+
const args = process.argv.slice(2);
|
|
301
|
+
|
|
302
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
303
|
+
showHelp();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const options = parseArgs(args);
|
|
308
|
+
|
|
309
|
+
if (!options.url) {
|
|
310
|
+
console.error('Error: URL is required');
|
|
311
|
+
showHelp();
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
cloneSite(options.url, options)
|
|
316
|
+
.then(result => {
|
|
317
|
+
console.log(JSON.stringify(result, null, 2));
|
|
318
|
+
process.exit(0);
|
|
319
|
+
})
|
|
320
|
+
.catch(err => {
|
|
321
|
+
console.error(`\n[ERROR] ${err.message}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
});
|
|
324
|
+
}
|
package/bin/commands/help.js
CHANGED
|
@@ -9,16 +9,28 @@ design-clone - Claude Code skill for website design cloning
|
|
|
9
9
|
Usage:
|
|
10
10
|
design-clone init [options] Install skill to ~/.claude/skills/
|
|
11
11
|
design-clone verify Check installation status
|
|
12
|
+
design-clone clone-site <url> Clone multiple pages from a website
|
|
12
13
|
design-clone help Show this help
|
|
13
14
|
|
|
14
|
-
Options:
|
|
15
|
+
Init Options:
|
|
15
16
|
--force, -f Overwrite existing installation
|
|
16
17
|
--skip-deps Skip dependency installation
|
|
17
18
|
|
|
19
|
+
Clone-site Options:
|
|
20
|
+
--pages <paths> Comma-separated paths (e.g., /,/about,/contact)
|
|
21
|
+
--max-pages <n> Maximum pages to auto-discover (default: 10)
|
|
22
|
+
--viewports <list> Viewport list (default: desktop,tablet,mobile)
|
|
23
|
+
--yes Skip confirmation prompt
|
|
24
|
+
--output <dir> Custom output directory
|
|
25
|
+
--ai Extract design tokens using Gemini AI (requires GEMINI_API_KEY)
|
|
26
|
+
|
|
18
27
|
Examples:
|
|
19
|
-
design-clone init
|
|
20
|
-
design-clone init --force
|
|
21
|
-
design-clone verify
|
|
28
|
+
design-clone init # Install skill
|
|
29
|
+
design-clone init --force # Reinstall, overwrite existing
|
|
30
|
+
design-clone verify # Check if installed correctly
|
|
31
|
+
design-clone clone-site https://example.com
|
|
32
|
+
design-clone clone-site https://example.com --max-pages 5
|
|
33
|
+
design-clone clone-site https://example.com --pages /,/about,/contact
|
|
22
34
|
|
|
23
35
|
After installation:
|
|
24
36
|
1. Set GEMINI_API_KEY in ~/.claude/.env (optional, for AI analysis)
|
package/docs/troubleshooting.md
CHANGED
|
@@ -95,3 +95,75 @@ GEMINI_API_KEY=your-api-key-here
|
|
|
95
95
|
```bash
|
|
96
96
|
--viewports '[{"width":1440,"height":900,"name":"custom"}]'
|
|
97
97
|
```
|
|
98
|
+
|
|
99
|
+
## clone-site Issues
|
|
100
|
+
|
|
101
|
+
### No pages discovered
|
|
102
|
+
|
|
103
|
+
**Symptom:** Only homepage cloned, other pages not found.
|
|
104
|
+
|
|
105
|
+
**Causes:**
|
|
106
|
+
- Site uses JS-rendered navigation (React/Vue/Angular)
|
|
107
|
+
- Navigation not in standard selectors (header nav, footer nav)
|
|
108
|
+
|
|
109
|
+
**Solutions:**
|
|
110
|
+
```bash
|
|
111
|
+
# Specify pages manually
|
|
112
|
+
design-clone clone-site https://example.com --pages /,/about,/contact,/services
|
|
113
|
+
|
|
114
|
+
# Increase max pages if hitting limit
|
|
115
|
+
design-clone clone-site https://example.com --max-pages 20
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Links not working in cloned pages
|
|
119
|
+
|
|
120
|
+
**Symptom:** Internal links point to original URLs.
|
|
121
|
+
|
|
122
|
+
**Causes:**
|
|
123
|
+
- Page not in discovered list
|
|
124
|
+
- HTML file not found for rewriting
|
|
125
|
+
|
|
126
|
+
**Solutions:**
|
|
127
|
+
1. Check `manifest.json` for page list
|
|
128
|
+
2. Ensure all pages captured successfully (check `capture-results.json`)
|
|
129
|
+
3. Re-run with manual `--pages` flag including missing pages
|
|
130
|
+
|
|
131
|
+
### CSS broken on some pages
|
|
132
|
+
|
|
133
|
+
**Symptom:** Styling differs between cloned pages.
|
|
134
|
+
|
|
135
|
+
**Causes:**
|
|
136
|
+
- Page-specific CSS not merged
|
|
137
|
+
- CSS extraction failed for some pages
|
|
138
|
+
|
|
139
|
+
**Solutions:**
|
|
140
|
+
1. Check `css/` folder for per-page CSS files
|
|
141
|
+
2. Review merge stats in output
|
|
142
|
+
3. Try with fewer pages to isolate issue
|
|
143
|
+
|
|
144
|
+
### Timeout during capture
|
|
145
|
+
|
|
146
|
+
**Error:** `Navigation timeout`
|
|
147
|
+
|
|
148
|
+
**Causes:**
|
|
149
|
+
- Large pages
|
|
150
|
+
- Slow server
|
|
151
|
+
- Too many pages
|
|
152
|
+
|
|
153
|
+
**Solutions:**
|
|
154
|
+
```bash
|
|
155
|
+
# Reduce pages
|
|
156
|
+
design-clone clone-site https://example.com --max-pages 5
|
|
157
|
+
|
|
158
|
+
# Use specific viewports only
|
|
159
|
+
design-clone clone-site https://example.com --viewports desktop
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Memory issues
|
|
163
|
+
|
|
164
|
+
**Symptom:** Process crashes or hangs.
|
|
165
|
+
|
|
166
|
+
**Solutions:**
|
|
167
|
+
1. Reduce `--max-pages` to 5 or fewer
|
|
168
|
+
2. Clone in batches
|
|
169
|
+
3. Close other applications
|
package/package.json
CHANGED
|
@@ -9,6 +9,27 @@
|
|
|
9
9
|
export const MAX_CSS_SIZE = 5 * 1024 * 1024; // 5MB limit
|
|
10
10
|
export const MAX_CSS_RULES_WARN = 5000; // Warn on large stylesheets
|
|
11
11
|
|
|
12
|
+
// Layout-critical properties for accurate cloning
|
|
13
|
+
export const LAYOUT_PROPERTIES = {
|
|
14
|
+
// Display & Flex
|
|
15
|
+
display: ['display', 'flexDirection', 'flexWrap', 'justifyContent',
|
|
16
|
+
'alignItems', 'alignContent', 'gap', 'rowGap', 'columnGap'],
|
|
17
|
+
// Grid
|
|
18
|
+
grid: ['gridTemplateColumns', 'gridTemplateRows', 'gridGap', 'gridAutoFlow'],
|
|
19
|
+
// Position
|
|
20
|
+
position: ['position', 'top', 'right', 'bottom', 'left', 'zIndex'],
|
|
21
|
+
// Sizing
|
|
22
|
+
sizing: ['width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight'],
|
|
23
|
+
// Box Model
|
|
24
|
+
box: ['boxSizing', 'overflow', 'overflowX', 'overflowY', 'borderWidth', 'borderStyle'],
|
|
25
|
+
// Visual (existing)
|
|
26
|
+
visual: ['color', 'backgroundColor', 'fontFamily', 'fontSize',
|
|
27
|
+
'fontWeight', 'lineHeight', 'padding', 'margin', 'borderRadius']
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Flatten for iteration
|
|
31
|
+
export const ALL_PROPERTIES = Object.values(LAYOUT_PROPERTIES).flat();
|
|
32
|
+
|
|
12
33
|
/**
|
|
13
34
|
* Extract all CSS from page
|
|
14
35
|
* @param {Page} page - Puppeteer page
|
|
@@ -16,7 +37,7 @@ export const MAX_CSS_RULES_WARN = 5000; // Warn on large stylesheets
|
|
|
16
37
|
* @returns {Promise<{cssBlocks: Array, corsBlocked: Array, computedStyles: Object, totalRules: number, warnings: Array}>}
|
|
17
38
|
*/
|
|
18
39
|
export async function extractAllCss(page, baseUrl) {
|
|
19
|
-
return await page.evaluate((url) => {
|
|
40
|
+
return await page.evaluate((url, allProps) => {
|
|
20
41
|
const cssBlocks = [];
|
|
21
42
|
const corsBlocked = [];
|
|
22
43
|
const warnings = [];
|
|
@@ -77,17 +98,21 @@ export async function extractAllCss(page, baseUrl) {
|
|
|
77
98
|
const el = document.querySelector(selector);
|
|
78
99
|
if (el) {
|
|
79
100
|
const style = getComputedStyle(el);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
const styles = {};
|
|
102
|
+
|
|
103
|
+
// Extract all layout + visual properties
|
|
104
|
+
allProps.forEach(prop => {
|
|
105
|
+
const value = style[prop];
|
|
106
|
+
// Skip empty/default values to reduce payload (except display)
|
|
107
|
+
if (prop === 'display') {
|
|
108
|
+
styles[prop] = value; // Always include display for inline strategy
|
|
109
|
+
} else if (value && value !== 'none' && value !== 'auto' &&
|
|
110
|
+
value !== 'normal' && value !== '0px' && value !== 'static') {
|
|
111
|
+
styles[prop] = value;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
computedStyles[selector] = styles;
|
|
91
116
|
}
|
|
92
117
|
} catch (e) {
|
|
93
118
|
// Ignore invalid selectors
|
|
@@ -103,5 +128,5 @@ export async function extractAllCss(page, baseUrl) {
|
|
|
103
128
|
totalRules,
|
|
104
129
|
warnings
|
|
105
130
|
};
|
|
106
|
-
}, baseUrl);
|
|
131
|
+
}, baseUrl, ALL_PROPERTIES);
|
|
107
132
|
}
|