design-clone 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.
Files changed (47) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/SKILL.md +239 -0
  5. package/bin/cli.js +45 -0
  6. package/bin/commands/help.js +29 -0
  7. package/bin/commands/init.js +126 -0
  8. package/bin/commands/verify.js +99 -0
  9. package/bin/utils/copy.js +65 -0
  10. package/bin/utils/validate.js +122 -0
  11. package/docs/basic-clone.md +63 -0
  12. package/docs/cli-reference.md +94 -0
  13. package/docs/design-clone-architecture.md +247 -0
  14. package/docs/pixel-perfect.md +86 -0
  15. package/docs/troubleshooting.md +97 -0
  16. package/package.json +57 -0
  17. package/requirements.txt +5 -0
  18. package/src/ai/analyze-structure.py +305 -0
  19. package/src/ai/extract-design-tokens.py +439 -0
  20. package/src/ai/prompts/__init__.py +2 -0
  21. package/src/ai/prompts/design_tokens.py +183 -0
  22. package/src/ai/prompts/structure_analysis.py +273 -0
  23. package/src/core/cookie-handler.js +76 -0
  24. package/src/core/css-extractor.js +107 -0
  25. package/src/core/dimension-extractor.js +366 -0
  26. package/src/core/dimension-output.js +208 -0
  27. package/src/core/extract-assets.js +468 -0
  28. package/src/core/filter-css.js +499 -0
  29. package/src/core/html-extractor.js +102 -0
  30. package/src/core/lazy-loader.js +188 -0
  31. package/src/core/page-readiness.js +161 -0
  32. package/src/core/screenshot.js +380 -0
  33. package/src/post-process/enhance-assets.js +157 -0
  34. package/src/post-process/fetch-images.js +398 -0
  35. package/src/post-process/inject-icons.js +311 -0
  36. package/src/utils/__init__.py +16 -0
  37. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  39. package/src/utils/browser.js +103 -0
  40. package/src/utils/env.js +153 -0
  41. package/src/utils/env.py +134 -0
  42. package/src/utils/helpers.js +71 -0
  43. package/src/utils/puppeteer.js +281 -0
  44. package/src/verification/verify-layout.js +424 -0
  45. package/src/verification/verify-menu.js +422 -0
  46. package/templates/base.css +705 -0
  47. package/templates/base.html +293 -0
@@ -0,0 +1,16 @@
1
+ """
2
+ Design Clone skill library modules.
3
+
4
+ JavaScript modules:
5
+ - browser.js: Browser abstraction facade
6
+ - puppeteer.js: Standalone Puppeteer wrapper
7
+ - utils.js: CLI utilities
8
+ - env.js: Environment variable resolution
9
+
10
+ Python modules:
11
+ - env.py: Environment variable resolution
12
+ """
13
+
14
+ from .env import resolve_env, load_env, require_env, get_skill_dir
15
+
16
+ __all__ = ['resolve_env', 'load_env', 'require_env', 'get_skill_dir']
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Browser abstraction facade for design-clone scripts
3
+ *
4
+ * Auto-detects and uses:
5
+ * 1. chrome-devtools skill (if installed) - Preferred
6
+ * 2. Standalone puppeteer wrapper - Fallback
7
+ *
8
+ * Exports same API regardless of provider:
9
+ * - getBrowser(options)
10
+ * - getPage(browser)
11
+ * - closeBrowser()
12
+ * - disconnectBrowser()
13
+ * - parseArgs(argv)
14
+ * - outputJSON(data)
15
+ * - outputError(error)
16
+ */
17
+
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
+ let browserModule = null;
31
+ let providerName = 'unknown';
32
+
33
+ /**
34
+ * Initialize browser provider (lazy-loaded)
35
+ */
36
+ async function initProvider() {
37
+ if (browserModule) return;
38
+
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');
55
+ }
56
+
57
+ // Import utilities (always use local helpers)
58
+ import { parseArgs, outputJSON, outputError } from './helpers.js';
59
+ export { parseArgs, outputJSON, outputError };
60
+
61
+ /**
62
+ * Get current browser provider name
63
+ * @returns {string} 'chrome-devtools' or 'standalone'
64
+ */
65
+ export function getProviderName() {
66
+ return providerName;
67
+ }
68
+
69
+ /**
70
+ * Launch or connect to browser
71
+ * @param {Object} options - Browser options
72
+ * @returns {Promise<Browser>} Browser instance
73
+ */
74
+ export async function getBrowser(options = {}) {
75
+ await initProvider();
76
+ return browserModule.getBrowser(options);
77
+ }
78
+
79
+ /**
80
+ * Get page from browser
81
+ * @param {Browser} browser - Browser instance
82
+ * @returns {Promise<Page>} Page instance
83
+ */
84
+ export async function getPage(browser) {
85
+ await initProvider();
86
+ return browserModule.getPage(browser);
87
+ }
88
+
89
+ /**
90
+ * Close browser and clear session
91
+ */
92
+ export async function closeBrowser() {
93
+ await initProvider();
94
+ return browserModule.closeBrowser();
95
+ }
96
+
97
+ /**
98
+ * Disconnect from browser without closing
99
+ */
100
+ export async function disconnectBrowser() {
101
+ await initProvider();
102
+ return browserModule.disconnectBrowser();
103
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Environment variable resolution for design-clone scripts
3
+ *
4
+ * Search order (first found wins, process.env takes precedence):
5
+ * 1. process.env (already set)
6
+ * 2. .env in current working directory
7
+ * 3. .env in skill directory (scripts/design-clone/)
8
+ * 4. .env in ~/.claude/skills/
9
+ * 5. .env in ~/.claude/
10
+ *
11
+ * @module lib/env
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ // From src/utils/ -> go up 2 levels to reach skill root (design-clone/)
20
+ const SKILL_DIR = path.resolve(__dirname, '../..');
21
+
22
+ /**
23
+ * Get user home directory (cross-platform: HOME on Unix, USERPROFILE on Windows)
24
+ * @returns {string} Home directory path
25
+ */
26
+ function getHomeDir() {
27
+ return process.env.HOME || process.env.USERPROFILE || '';
28
+ }
29
+
30
+ /**
31
+ * Get list of .env file search paths
32
+ * @returns {string[]} Array of directory paths to search for .env
33
+ */
34
+ function getEnvSearchPaths() {
35
+ const home = getHomeDir();
36
+ return [
37
+ process.cwd(),
38
+ SKILL_DIR,
39
+ home ? path.join(home, '.claude/skills') : null,
40
+ home ? path.join(home, '.claude') : null
41
+ ].filter(Boolean);
42
+ }
43
+
44
+ /**
45
+ * Parse .env file content into key-value object
46
+ * Handles: KEY=value, KEY="quoted value", KEY='quoted', comments (#), empty lines
47
+ * Quote handling: Only strips matching outer quotes (both same type, length >= 2)
48
+ *
49
+ * @param {string} content - .env file content
50
+ * @returns {Object} Parsed key-value pairs
51
+ */
52
+ function parseEnvContent(content) {
53
+ const result = {};
54
+
55
+ content.split('\n').forEach(line => {
56
+ // Skip empty lines and comments
57
+ const trimmed = line.trim();
58
+ if (!trimmed || trimmed.startsWith('#')) return;
59
+
60
+ // Parse KEY=value using partition approach (handles = in value)
61
+ const eqIndex = trimmed.indexOf('=');
62
+ if (eqIndex === -1) return;
63
+
64
+ const key = trimmed.slice(0, eqIndex).trim();
65
+ if (!key) return;
66
+
67
+ let value = trimmed.slice(eqIndex + 1).trim();
68
+
69
+ // Remove matching outer quotes only (double or single)
70
+ // Must have same quote type at both ends, length >= 2
71
+ if (value.length >= 2) {
72
+ const first = value[0];
73
+ const last = value[value.length - 1];
74
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
75
+ value = value.slice(1, -1);
76
+ }
77
+ }
78
+
79
+ result[key] = value;
80
+ });
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Load environment variables from .env files
87
+ * Only sets variables not already in process.env
88
+ *
89
+ * @returns {string|null} Path to loaded .env file, or null if none found
90
+ */
91
+ export function loadEnv() {
92
+ const searchPaths = getEnvSearchPaths();
93
+
94
+ for (const dir of searchPaths) {
95
+ const envPath = path.join(dir, '.env');
96
+
97
+ if (fs.existsSync(envPath)) {
98
+ try {
99
+ const content = fs.readFileSync(envPath, 'utf-8');
100
+ const parsed = parseEnvContent(content);
101
+
102
+ // Only set vars not already in process.env
103
+ Object.entries(parsed).forEach(([key, value]) => {
104
+ if (!process.env[key]) {
105
+ process.env[key] = value;
106
+ }
107
+ });
108
+
109
+ return envPath;
110
+ } catch (err) {
111
+ console.error(`[env] Failed to read ${envPath}: ${err.message}`);
112
+ }
113
+ }
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Get environment variable with optional default
121
+ *
122
+ * @param {string} key - Environment variable name
123
+ * @param {string} [defaultValue=null] - Default value if not found
124
+ * @returns {string|null} Variable value or default
125
+ */
126
+ export function getEnv(key, defaultValue = null) {
127
+ return process.env[key] || defaultValue;
128
+ }
129
+
130
+ /**
131
+ * Require environment variable, throw if not found
132
+ *
133
+ * @param {string} key - Environment variable name
134
+ * @param {string} [hint] - Hint message for how to set the variable
135
+ * @returns {string} Variable value
136
+ * @throws {Error} If variable not set
137
+ */
138
+ export function requireEnv(key, hint = '') {
139
+ const value = process.env[key];
140
+ if (!value) {
141
+ const hintMsg = hint ? `\nHint: ${hint}` : '';
142
+ throw new Error(`Required environment variable ${key} not set.${hintMsg}`);
143
+ }
144
+ return value;
145
+ }
146
+
147
+ /**
148
+ * Get skill directory path
149
+ * @returns {string} Absolute path to skill directory
150
+ */
151
+ export function getSkillDir() {
152
+ return SKILL_DIR;
153
+ }
@@ -0,0 +1,134 @@
1
+ """
2
+ Environment variable resolution for design-clone scripts.
3
+
4
+ Search order (first found wins, os.environ takes precedence):
5
+ 1. os.environ (already set)
6
+ 2. .env in current working directory
7
+ 3. .env in skill directory (scripts/design-clone/)
8
+ 4. .env in ~/.claude/skills/
9
+ 5. .env in ~/.claude/
10
+
11
+ Usage:
12
+ from lib.env import resolve_env, load_env, get_skill_dir
13
+
14
+ # Load all .env files
15
+ load_env()
16
+
17
+ # Get specific variable with fallback
18
+ api_key = resolve_env('GEMINI_API_KEY', default=None)
19
+ """
20
+
21
+ import os
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional
24
+
25
+ # Skill directory - from src/utils/ go up 2 levels to reach design-clone/
26
+ SKILL_DIR = Path(__file__).parent.parent.parent.resolve()
27
+
28
+
29
+ def get_env_search_paths() -> List[Path]:
30
+ """Get list of directories to search for .env files."""
31
+ return [
32
+ Path.cwd(),
33
+ SKILL_DIR,
34
+ Path.home() / '.claude' / 'skills',
35
+ Path.home() / '.claude'
36
+ ]
37
+
38
+
39
+ def parse_env_file(file_path: Path) -> Dict[str, str]:
40
+ """
41
+ Parse .env file into key-value dict.
42
+ Handles: KEY=value, KEY="quoted value", comments (#), empty lines
43
+ """
44
+ result = {}
45
+
46
+ try:
47
+ with open(file_path, 'r', encoding='utf-8') as f:
48
+ for line in f:
49
+ line = line.strip()
50
+
51
+ # Skip empty lines and comments
52
+ if not line or line.startswith('#'):
53
+ continue
54
+
55
+ # Parse KEY=value
56
+ if '=' in line:
57
+ key, _, value = line.partition('=')
58
+ key = key.strip()
59
+ value = value.strip()
60
+
61
+ # Remove quotes if present
62
+ if (value.startswith('"') and value.endswith('"')) or \
63
+ (value.startswith("'") and value.endswith("'")):
64
+ value = value[1:-1]
65
+
66
+ result[key] = value
67
+ except Exception as e:
68
+ print(f"[env] Failed to read {file_path}: {e}")
69
+
70
+ return result
71
+
72
+
73
+ def load_env() -> Optional[Path]:
74
+ """
75
+ Load environment variables from .env files.
76
+ Only sets variables not already in os.environ.
77
+
78
+ Returns:
79
+ Path to loaded .env file, or None if none found.
80
+ """
81
+ for dir_path in get_env_search_paths():
82
+ env_file = dir_path / '.env'
83
+
84
+ if env_file.exists():
85
+ parsed = parse_env_file(env_file)
86
+
87
+ # Only set vars not already in environ
88
+ for key, value in parsed.items():
89
+ if key not in os.environ:
90
+ os.environ[key] = value
91
+
92
+ return env_file
93
+
94
+ return None
95
+
96
+
97
+ def resolve_env(key: str, default: Optional[str] = None) -> Optional[str]:
98
+ """
99
+ Get environment variable with optional default.
100
+
101
+ Args:
102
+ key: Environment variable name
103
+ default: Default value if not found
104
+
105
+ Returns:
106
+ Variable value or default
107
+ """
108
+ return os.environ.get(key, default)
109
+
110
+
111
+ def require_env(key: str, hint: str = '') -> str:
112
+ """
113
+ Require environment variable, raise if not found.
114
+
115
+ Args:
116
+ key: Environment variable name
117
+ hint: Hint message for how to set the variable
118
+
119
+ Returns:
120
+ Variable value
121
+
122
+ Raises:
123
+ OSError: If variable not set
124
+ """
125
+ value = os.environ.get(key)
126
+ if not value:
127
+ hint_msg = f'\nHint: {hint}' if hint else ''
128
+ raise OSError(f'Required environment variable {key} not set.{hint_msg}')
129
+ return value
130
+
131
+
132
+ def get_skill_dir() -> Path:
133
+ """Get skill directory path."""
134
+ return SKILL_DIR
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared utility functions for design-clone scripts
3
+ * Provides CLI parsing and JSON output helpers
4
+ */
5
+
6
+ /**
7
+ * Parse command line arguments into key-value object
8
+ * @param {string[]} argv - Command line arguments
9
+ * @returns {Object} Parsed arguments
10
+ * @throws {TypeError} If argv is not an array
11
+ *
12
+ * @example
13
+ * parseArgs(['--url', 'https://example.com', '--headless', '--port', '9222'])
14
+ * // Returns: { url: 'https://example.com', headless: true, port: '9222' }
15
+ */
16
+ export function parseArgs(argv) {
17
+ // Input validation
18
+ if (!Array.isArray(argv)) {
19
+ throw new TypeError('argv must be an array');
20
+ }
21
+
22
+ const args = {};
23
+
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const arg = argv[i];
26
+
27
+ // Skip non-string arguments
28
+ if (typeof arg !== 'string') continue;
29
+
30
+ if (arg.startsWith('--')) {
31
+ const key = arg.slice(2);
32
+ const nextArg = argv[i + 1];
33
+
34
+ // If next arg exists and doesn't start with --, use it as value
35
+ if (nextArg && typeof nextArg === 'string' && !nextArg.startsWith('--')) {
36
+ args[key] = nextArg;
37
+ i++; // Skip next arg
38
+ } else {
39
+ // Boolean flag
40
+ args[key] = true;
41
+ }
42
+ }
43
+ }
44
+
45
+ return args;
46
+ }
47
+
48
+ /**
49
+ * Output data as formatted JSON to stdout
50
+ * @param {Object} data - Data to output
51
+ */
52
+ export function outputJSON(data) {
53
+ console.log(JSON.stringify(data, null, 2));
54
+ }
55
+
56
+ /**
57
+ * Output error as JSON to stderr and exit
58
+ * @param {Error} error - Error object
59
+ * @throws {never} Always exits the process
60
+ */
61
+ export function outputError(error) {
62
+ const errorMessage = error instanceof Error ? error.message : String(error);
63
+ const errorStack = error instanceof Error ? error.stack : undefined;
64
+
65
+ console.error(JSON.stringify({
66
+ success: false,
67
+ error: errorMessage,
68
+ stack: process.env.DEBUG ? errorStack : undefined
69
+ }, null, 2));
70
+ process.exit(1);
71
+ }