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.
- package/.env.example +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/SKILL.md +239 -0
- package/bin/cli.js +45 -0
- package/bin/commands/help.js +29 -0
- package/bin/commands/init.js +126 -0
- package/bin/commands/verify.js +99 -0
- package/bin/utils/copy.js +65 -0
- package/bin/utils/validate.js +122 -0
- package/docs/basic-clone.md +63 -0
- package/docs/cli-reference.md +94 -0
- package/docs/design-clone-architecture.md +247 -0
- package/docs/pixel-perfect.md +86 -0
- package/docs/troubleshooting.md +97 -0
- package/package.json +57 -0
- package/requirements.txt +5 -0
- package/src/ai/analyze-structure.py +305 -0
- package/src/ai/extract-design-tokens.py +439 -0
- package/src/ai/prompts/__init__.py +2 -0
- package/src/ai/prompts/design_tokens.py +183 -0
- package/src/ai/prompts/structure_analysis.py +273 -0
- package/src/core/cookie-handler.js +76 -0
- package/src/core/css-extractor.js +107 -0
- package/src/core/dimension-extractor.js +366 -0
- package/src/core/dimension-output.js +208 -0
- package/src/core/extract-assets.js +468 -0
- package/src/core/filter-css.js +499 -0
- package/src/core/html-extractor.js +102 -0
- package/src/core/lazy-loader.js +188 -0
- package/src/core/page-readiness.js +161 -0
- package/src/core/screenshot.js +380 -0
- package/src/post-process/enhance-assets.js +157 -0
- package/src/post-process/fetch-images.js +398 -0
- package/src/post-process/inject-icons.js +311 -0
- package/src/utils/__init__.py +16 -0
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/browser.js +103 -0
- package/src/utils/env.js +153 -0
- package/src/utils/env.py +134 -0
- package/src/utils/helpers.js +71 -0
- package/src/utils/puppeteer.js +281 -0
- package/src/verification/verify-layout.js +424 -0
- package/src/verification/verify-menu.js +422 -0
- package/templates/base.css +705 -0
- 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']
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|
package/src/utils/env.js
ADDED
|
@@ -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
|
+
}
|
package/src/utils/env.py
ADDED
|
@@ -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
|
+
}
|