cluso-inspector 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/README.md +122 -0
- package/assets/cluso-icon.png +0 -0
- package/bin/cluso-inspector.js +122 -0
- package/cluso-inspector.js +644 -0
- package/main.js +153 -0
- package/package.json +48 -0
- package/preload.js +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# cluso-inspector
|
|
2
|
+
|
|
3
|
+
Visual element selector for extracting HTML and screenshots from any website.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Use directly with npx (no installation required)
|
|
9
|
+
npx cluso-inspector https://github.com
|
|
10
|
+
|
|
11
|
+
# Or install globally
|
|
12
|
+
npm install -g cluso-inspector
|
|
13
|
+
cluso-inspector https://github.com
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cluso-inspector <url> [language]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Arguments
|
|
23
|
+
|
|
24
|
+
- **url** (required) - The URL to open and extract from
|
|
25
|
+
- **language** (optional) - Target language: `typescript` or `javascript` (default: `typescript`)
|
|
26
|
+
|
|
27
|
+
### Examples
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Extract from GitHub
|
|
31
|
+
npx cluso-inspector https://github.com
|
|
32
|
+
|
|
33
|
+
# Specify JavaScript output
|
|
34
|
+
npx cluso-inspector https://github.com javascript
|
|
35
|
+
|
|
36
|
+
# Extract from any site
|
|
37
|
+
npx cluso-inspector https://example.com typescript
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How It Works
|
|
41
|
+
|
|
42
|
+
1. **Launch**: Opens the URL in an Electron browser window
|
|
43
|
+
2. **Select**: Hover over elements (blue outline), click to select (purple outline)
|
|
44
|
+
3. **Extract**: Click "Select" button to capture HTML + screenshot
|
|
45
|
+
4. **Output**: Returns JSON to stdout with all extracted data
|
|
46
|
+
|
|
47
|
+
## Output Format
|
|
48
|
+
|
|
49
|
+
The tool outputs JSON to stdout:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"success": true,
|
|
54
|
+
"url": "https://github.com",
|
|
55
|
+
"timestamp": "2026-01-11T10:00:00.000Z",
|
|
56
|
+
"language": "typescript",
|
|
57
|
+
"extractions": [
|
|
58
|
+
{
|
|
59
|
+
"selector": "div.hero > div.container",
|
|
60
|
+
"isReact": false,
|
|
61
|
+
"component": { ... },
|
|
62
|
+
"html": "<div class=\"hero\">...</div>",
|
|
63
|
+
"screenshot": "data:image/png;base64,...",
|
|
64
|
+
"dimensions": {
|
|
65
|
+
"width": 1200,
|
|
66
|
+
"height": 600,
|
|
67
|
+
"top": 100,
|
|
68
|
+
"left": 120
|
|
69
|
+
},
|
|
70
|
+
"meta": {
|
|
71
|
+
"tagName": "div",
|
|
72
|
+
"id": "hero-section",
|
|
73
|
+
"classes": ["hero", "container"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Use Cases
|
|
81
|
+
|
|
82
|
+
- **Component extraction**: Extract React/HTML components from live sites
|
|
83
|
+
- **Design replication**: Capture visual elements with pixel-perfect screenshots
|
|
84
|
+
- **Automated testing**: Extract DOM structure for test validation
|
|
85
|
+
- **Documentation**: Capture UI elements with code + screenshot
|
|
86
|
+
|
|
87
|
+
## Piping to Files
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Save JSON to file
|
|
91
|
+
npx cluso-inspector https://github.com > extraction.json
|
|
92
|
+
|
|
93
|
+
# Extract screenshot separately
|
|
94
|
+
npx cluso-inspector https://github.com | jq -r '.extractions[0].screenshot' > screenshot.png
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Integration
|
|
98
|
+
|
|
99
|
+
Use with other tools via stdout:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
const { execSync } = require('child_process');
|
|
103
|
+
const data = JSON.parse(
|
|
104
|
+
execSync('npx cluso-inspector https://github.com').toString()
|
|
105
|
+
);
|
|
106
|
+
console.log(data.extractions[0].selector);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Keyboard Shortcuts
|
|
110
|
+
|
|
111
|
+
- **Click**: Select element
|
|
112
|
+
- **ESC**: Cancel and close
|
|
113
|
+
- **Clear**: Clear current selection
|
|
114
|
+
|
|
115
|
+
## Requirements
|
|
116
|
+
|
|
117
|
+
- Node.js 14+
|
|
118
|
+
- Electron 28+ (auto-installed)
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cluso-inspector CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* cluso-inspector <url> [language]
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* cluso-inspector https://github.com
|
|
11
|
+
* cluso-inspector https://github.com typescript
|
|
12
|
+
* npx cluso-inspector https://example.com javascript
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { spawn } = require('child_process');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
// Parse CLI arguments
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
|
|
22
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
23
|
+
console.log(`
|
|
24
|
+
cluso-inspector - Visual element selector for web components
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
cluso-inspector <url> [language]
|
|
28
|
+
|
|
29
|
+
Arguments:
|
|
30
|
+
url - The URL to open and extract from (required)
|
|
31
|
+
language - Target language: typescript or javascript (default: typescript)
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
cluso-inspector https://github.com
|
|
35
|
+
cluso-inspector https://github.com typescript
|
|
36
|
+
npx cluso-inspector https://example.com javascript
|
|
37
|
+
|
|
38
|
+
Output:
|
|
39
|
+
Outputs JSON to stdout with: { screenshot, html, selector, dimensions, ... }
|
|
40
|
+
`);
|
|
41
|
+
process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = args[0];
|
|
45
|
+
const language = args[1] || 'typescript';
|
|
46
|
+
|
|
47
|
+
// Validate URL
|
|
48
|
+
try {
|
|
49
|
+
new URL(url);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Error: Invalid URL:', url);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate language
|
|
56
|
+
if (!['typescript', 'javascript'].includes(language)) {
|
|
57
|
+
console.error('Error: Language must be "typescript" or "javascript"');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create temp file for output (Electron will write here, we'll read and output to stdout)
|
|
62
|
+
const tmpFile = `/tmp/cluso-inspector-${process.pid}.json`;
|
|
63
|
+
|
|
64
|
+
// Path to Electron app
|
|
65
|
+
const electronPath = require('electron');
|
|
66
|
+
const mainPath = path.join(__dirname, '..', 'main.js');
|
|
67
|
+
|
|
68
|
+
// Launch Electron
|
|
69
|
+
const electron = spawn(electronPath, [mainPath, url, tmpFile, language], {
|
|
70
|
+
stdio: ['inherit', 'pipe', 'pipe']
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Capture electron stdout/stderr (for debugging)
|
|
74
|
+
electron.stdout.on('data', (data) => {
|
|
75
|
+
// Log to stderr so it doesn't interfere with JSON output
|
|
76
|
+
process.stderr.write(`[cluso-inspector] ${data}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
electron.stderr.on('data', (data) => {
|
|
80
|
+
process.stderr.write(`[cluso-inspector] ${data}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// When Electron exits
|
|
84
|
+
electron.on('close', (code) => {
|
|
85
|
+
if (code === 0) {
|
|
86
|
+
// Read the temp file and output to stdout
|
|
87
|
+
if (fs.existsSync(tmpFile)) {
|
|
88
|
+
const data = fs.readFileSync(tmpFile, 'utf8');
|
|
89
|
+
console.log(data); // Output JSON to stdout
|
|
90
|
+
|
|
91
|
+
// Clean up temp file
|
|
92
|
+
try {
|
|
93
|
+
fs.unlinkSync(tmpFile);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// Ignore cleanup errors
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
process.exit(0);
|
|
99
|
+
} else {
|
|
100
|
+
console.error('Error: Extraction data not found');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
} else if (code === 1) {
|
|
104
|
+
// User cancelled
|
|
105
|
+
console.error('Extraction cancelled');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
} else {
|
|
108
|
+
console.error('Error: Electron process failed with code', code);
|
|
109
|
+
process.exit(code || 1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Handle signals
|
|
114
|
+
process.on('SIGINT', () => {
|
|
115
|
+
electron.kill('SIGINT');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
process.on('SIGTERM', () => {
|
|
120
|
+
electron.kill('SIGTERM');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloneReact Element Inspector
|
|
3
|
+
* Adapted from ai-cluso shared-inspector package
|
|
4
|
+
* Provides visual overlay for element selection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Import styles (will be read from inspector-styles.ts)
|
|
8
|
+
const INSPECTOR_STYLES = `
|
|
9
|
+
#cluso-hover-overlay {
|
|
10
|
+
position: absolute;
|
|
11
|
+
pointer-events: none;
|
|
12
|
+
border: 2px dashed #3b82f6;
|
|
13
|
+
border-radius: 8px;
|
|
14
|
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
|
|
15
|
+
z-index: 999998;
|
|
16
|
+
opacity: 0;
|
|
17
|
+
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
18
|
+
}
|
|
19
|
+
#cluso-hover-overlay.visible {
|
|
20
|
+
opacity: 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#cluso-screenshot-overlay {
|
|
24
|
+
position: absolute;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
border: 2px solid #3b82f6;
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
|
|
29
|
+
z-index: 999998;
|
|
30
|
+
opacity: 0;
|
|
31
|
+
transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
32
|
+
}
|
|
33
|
+
#cluso-screenshot-overlay.visible {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#cluso-selection-overlay {
|
|
38
|
+
position: absolute;
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
border: 3px solid #8b5cf6;
|
|
41
|
+
border-radius: 8px;
|
|
42
|
+
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.3), 0 4px 12px rgba(139, 92, 246, 0.4);
|
|
43
|
+
z-index: 999999;
|
|
44
|
+
opacity: 0;
|
|
45
|
+
transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
46
|
+
}
|
|
47
|
+
#cluso-selection-overlay.visible {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.cluso-overlay-label {
|
|
52
|
+
position: absolute;
|
|
53
|
+
top: -24px;
|
|
54
|
+
left: 0;
|
|
55
|
+
background: rgba(0, 0, 0, 0.85);
|
|
56
|
+
color: white;
|
|
57
|
+
padding: 4px 8px;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
font-size: 11px;
|
|
60
|
+
font-weight: 600;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#cluso-screenshot-overlay .cluso-overlay-label {
|
|
66
|
+
background: rgba(59, 130, 246, 0.9);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#cluso-selection-overlay .cluso-overlay-label {
|
|
70
|
+
background: rgba(139, 92, 246, 0.9);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#clonereact-toolbar {
|
|
74
|
+
position: fixed;
|
|
75
|
+
top: 20px;
|
|
76
|
+
left: 50%;
|
|
77
|
+
transform: translateX(-50%);
|
|
78
|
+
background: rgba(0, 0, 0, 0.95);
|
|
79
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
80
|
+
border-radius: 12px;
|
|
81
|
+
padding: 12px 16px;
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 12px;
|
|
85
|
+
backdrop-filter: blur(10px);
|
|
86
|
+
z-index: 1000000;
|
|
87
|
+
box-shadow: 0 10px 40px rgba(139, 92, 246, 0.2);
|
|
88
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#clonereact-toolbar button {
|
|
92
|
+
padding: 8px 16px;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
95
|
+
background: rgba(255, 255, 255, 0.05);
|
|
96
|
+
color: white;
|
|
97
|
+
font-size: 14px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition: all 0.2s;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#clonereact-toolbar button:hover {
|
|
104
|
+
background: rgba(139, 92, 246, 0.2);
|
|
105
|
+
border-color: rgba(139, 92, 246, 0.5);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#clonereact-toolbar button.primary {
|
|
109
|
+
background: rgb(139, 92, 246);
|
|
110
|
+
border-color: rgb(139, 92, 246);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#clonereact-toolbar button.primary:hover {
|
|
114
|
+
background: rgb(124, 58, 237);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#clonereact-toolbar .info {
|
|
118
|
+
color: rgba(255, 255, 255, 0.7);
|
|
119
|
+
font-size: 13px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#cluso-branding {
|
|
123
|
+
position: fixed;
|
|
124
|
+
top: 8px;
|
|
125
|
+
left: 80px;
|
|
126
|
+
z-index: 1000001;
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 8px;
|
|
130
|
+
padding: 4px 12px 4px 8px;
|
|
131
|
+
background: rgba(0, 0, 0, 0.5);
|
|
132
|
+
backdrop-filter: blur(10px);
|
|
133
|
+
border-radius: 8px;
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
-webkit-app-region: no-drag;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#cluso-branding-logo {
|
|
139
|
+
width: 20px;
|
|
140
|
+
height: 20px;
|
|
141
|
+
border-radius: 4px;
|
|
142
|
+
object-fit: contain;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#cluso-branding-text {
|
|
146
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
color: white;
|
|
150
|
+
letter-spacing: -0.02em;
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
(function() {
|
|
155
|
+
'use strict';
|
|
156
|
+
|
|
157
|
+
const SCREENSHOT_PADDING_X = 0.2; // 20% horizontal padding for screenshot
|
|
158
|
+
const SCREENSHOT_PADDING_Y = 1.0; // 100% vertical padding for screenshot
|
|
159
|
+
|
|
160
|
+
let hoverOverlay = null;
|
|
161
|
+
let screenshotOverlay = null;
|
|
162
|
+
let selectionOverlay = null;
|
|
163
|
+
let selectedElement = null;
|
|
164
|
+
let currentHoveredElement = null;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Initialize inspector
|
|
168
|
+
*/
|
|
169
|
+
function init() {
|
|
170
|
+
console.log('[CloneReact] Initializing inspector...');
|
|
171
|
+
|
|
172
|
+
// Inject styles
|
|
173
|
+
const styleEl = document.createElement('style');
|
|
174
|
+
styleEl.textContent = INSPECTOR_STYLES;
|
|
175
|
+
document.head.appendChild(styleEl);
|
|
176
|
+
|
|
177
|
+
// Create overlays
|
|
178
|
+
hoverOverlay = document.createElement('div');
|
|
179
|
+
hoverOverlay.id = 'cluso-hover-overlay';
|
|
180
|
+
hoverOverlay.setAttribute('data-clonereact-ui', '1');
|
|
181
|
+
document.body.appendChild(hoverOverlay);
|
|
182
|
+
|
|
183
|
+
screenshotOverlay = document.createElement('div');
|
|
184
|
+
screenshotOverlay.id = 'cluso-screenshot-overlay';
|
|
185
|
+
screenshotOverlay.setAttribute('data-clonereact-ui', '1');
|
|
186
|
+
screenshotOverlay.innerHTML = '<div class="cluso-overlay-label">Image</div>';
|
|
187
|
+
document.body.appendChild(screenshotOverlay);
|
|
188
|
+
|
|
189
|
+
selectionOverlay = document.createElement('div');
|
|
190
|
+
selectionOverlay.id = 'cluso-selection-overlay';
|
|
191
|
+
selectionOverlay.setAttribute('data-clonereact-ui', '1');
|
|
192
|
+
selectionOverlay.innerHTML = '<div class="cluso-overlay-label">Element</div>';
|
|
193
|
+
document.body.appendChild(selectionOverlay);
|
|
194
|
+
|
|
195
|
+
// Create branding
|
|
196
|
+
createBranding();
|
|
197
|
+
|
|
198
|
+
// Create toolbar
|
|
199
|
+
createToolbar();
|
|
200
|
+
|
|
201
|
+
// Event listeners
|
|
202
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
203
|
+
document.addEventListener('click', handleClick, true);
|
|
204
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
205
|
+
|
|
206
|
+
console.log('[CloneReact] Inspector ready');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create branding in top-left
|
|
211
|
+
*/
|
|
212
|
+
function createBranding() {
|
|
213
|
+
const branding = document.createElement('div');
|
|
214
|
+
branding.id = 'cluso-branding';
|
|
215
|
+
branding.setAttribute('data-clonereact-ui', '1');
|
|
216
|
+
|
|
217
|
+
const iconUrl = window.__CLONEREACT_ICON_URL__ || '';
|
|
218
|
+
|
|
219
|
+
if (iconUrl) {
|
|
220
|
+
branding.innerHTML = `
|
|
221
|
+
<img id="cluso-branding-logo" src="${iconUrl}" alt="cluso" />
|
|
222
|
+
<div id="cluso-branding-text">cluso</div>
|
|
223
|
+
`;
|
|
224
|
+
} else {
|
|
225
|
+
// Fallback to "C" if no icon
|
|
226
|
+
branding.innerHTML = `
|
|
227
|
+
<div id="cluso-branding-logo" style="font-weight:700;font-size:16px;display:flex;align-items:center;justify-content:center;">C</div>
|
|
228
|
+
<div id="cluso-branding-text">cluso</div>
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
document.body.appendChild(branding);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Handle scroll - reposition overlays
|
|
237
|
+
*/
|
|
238
|
+
function handleScroll() {
|
|
239
|
+
if (selectedElement) {
|
|
240
|
+
positionScreenshotOverlay(selectedElement);
|
|
241
|
+
positionOverlay(selectionOverlay, selectedElement);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create toolbar
|
|
247
|
+
*/
|
|
248
|
+
function createToolbar() {
|
|
249
|
+
const toolbar = document.createElement('div');
|
|
250
|
+
toolbar.id = 'clonereact-toolbar';
|
|
251
|
+
toolbar.setAttribute('data-clonereact-ui', '1');
|
|
252
|
+
|
|
253
|
+
toolbar.innerHTML = `
|
|
254
|
+
<div class="info"><span id="selection-count">Hover to highlight, click to select</span></div>
|
|
255
|
+
<button id="btn-clear">Clear</button>
|
|
256
|
+
<button id="btn-extract" class="primary">Select</button>
|
|
257
|
+
<button id="btn-cancel">Cancel</button>
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
document.body.appendChild(toolbar);
|
|
261
|
+
|
|
262
|
+
// Button handlers
|
|
263
|
+
document.getElementById('btn-clear').addEventListener('click', clearSelection);
|
|
264
|
+
document.getElementById('btn-extract').addEventListener('click', extractComponent);
|
|
265
|
+
document.getElementById('btn-cancel').addEventListener('click', cancel);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Position overlay over element
|
|
270
|
+
*/
|
|
271
|
+
function positionOverlay(overlay, element) {
|
|
272
|
+
const rect = element.getBoundingClientRect();
|
|
273
|
+
overlay.style.top = `${rect.top + window.scrollY}px`;
|
|
274
|
+
overlay.style.left = `${rect.left + window.scrollX}px`;
|
|
275
|
+
overlay.style.width = `${rect.width}px`;
|
|
276
|
+
overlay.style.height = `${rect.height}px`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle mouse move
|
|
281
|
+
*/
|
|
282
|
+
function handleMouseMove(event) {
|
|
283
|
+
const target = event.target;
|
|
284
|
+
|
|
285
|
+
// Ignore our UI
|
|
286
|
+
if (target.closest('[data-clonereact-ui]')) {
|
|
287
|
+
hoverOverlay.classList.remove('visible');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
currentHoveredElement = target;
|
|
292
|
+
positionOverlay(hoverOverlay, target);
|
|
293
|
+
hoverOverlay.classList.add('visible');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Handle click
|
|
298
|
+
*/
|
|
299
|
+
function handleClick(event) {
|
|
300
|
+
const target = event.target;
|
|
301
|
+
|
|
302
|
+
// Ignore our UI
|
|
303
|
+
if (target.closest('[data-clonereact-ui]')) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
event.stopPropagation();
|
|
309
|
+
|
|
310
|
+
// Clear previous selection
|
|
311
|
+
if (screenshotOverlay) {
|
|
312
|
+
screenshotOverlay.classList.remove('visible');
|
|
313
|
+
}
|
|
314
|
+
if (selectionOverlay) {
|
|
315
|
+
selectionOverlay.classList.remove('visible');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Mark new selection
|
|
319
|
+
selectedElement = target;
|
|
320
|
+
|
|
321
|
+
// Position screenshot overlay (20% larger, blue)
|
|
322
|
+
positionScreenshotOverlay(target);
|
|
323
|
+
screenshotOverlay.classList.add('visible');
|
|
324
|
+
|
|
325
|
+
// Position element overlay (exact bounds, purple)
|
|
326
|
+
positionOverlay(selectionOverlay, target);
|
|
327
|
+
selectionOverlay.classList.add('visible');
|
|
328
|
+
|
|
329
|
+
// Hide hover
|
|
330
|
+
hoverOverlay.classList.remove('visible');
|
|
331
|
+
|
|
332
|
+
// Update count
|
|
333
|
+
updateSelectionCount();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Position screenshot overlay with padding (50% width, 60% height)
|
|
338
|
+
*/
|
|
339
|
+
function positionScreenshotOverlay(element) {
|
|
340
|
+
const rect = element.getBoundingClientRect();
|
|
341
|
+
const paddingX = rect.width * SCREENSHOT_PADDING_X / 2;
|
|
342
|
+
const paddingY = rect.height * SCREENSHOT_PADDING_Y / 2;
|
|
343
|
+
|
|
344
|
+
screenshotOverlay.style.top = `${rect.top + window.scrollY - paddingY}px`;
|
|
345
|
+
screenshotOverlay.style.left = `${rect.left + window.scrollX - paddingX}px`;
|
|
346
|
+
screenshotOverlay.style.width = `${rect.width + (paddingX * 2)}px`;
|
|
347
|
+
screenshotOverlay.style.height = `${rect.height + (paddingY * 2)}px`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Update selection count
|
|
352
|
+
*/
|
|
353
|
+
function updateSelectionCount() {
|
|
354
|
+
const countEl = document.getElementById('selection-count');
|
|
355
|
+
if (selectedElement) {
|
|
356
|
+
const tagName = selectedElement.tagName.toLowerCase();
|
|
357
|
+
const className = selectedElement.className ? '.' + selectedElement.className.split(' ')[0] : '';
|
|
358
|
+
countEl.textContent = `Selected: ${tagName}${className}`;
|
|
359
|
+
} else {
|
|
360
|
+
countEl.textContent = 'Hover to highlight, click to select';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Clear selection
|
|
366
|
+
*/
|
|
367
|
+
function clearSelection() {
|
|
368
|
+
selectedElement = null;
|
|
369
|
+
if (screenshotOverlay) {
|
|
370
|
+
screenshotOverlay.classList.remove('visible');
|
|
371
|
+
}
|
|
372
|
+
if (selectionOverlay) {
|
|
373
|
+
selectionOverlay.classList.remove('visible');
|
|
374
|
+
}
|
|
375
|
+
updateSelectionCount();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Extract component
|
|
380
|
+
*/
|
|
381
|
+
async function extractComponent() {
|
|
382
|
+
if (!selectedElement) {
|
|
383
|
+
alert('Please select an element first');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log('[CloneReact] Extracting component...');
|
|
388
|
+
|
|
389
|
+
// Use the react-fiber extraction if available
|
|
390
|
+
const extraction = window.__extractReactContext ?
|
|
391
|
+
window.__extractReactContext(selectedElement) :
|
|
392
|
+
await extractBasicElement(selectedElement);
|
|
393
|
+
|
|
394
|
+
// Send to main process (will show generating phase and preview)
|
|
395
|
+
window.clonereact.sendExtraction({
|
|
396
|
+
success: true,
|
|
397
|
+
url: window.location.href,
|
|
398
|
+
timestamp: new Date().toISOString(),
|
|
399
|
+
extractions: [extraction]
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Capture element screenshot using Electron's native API with padding (50% width, 60% height)
|
|
405
|
+
*/
|
|
406
|
+
async function captureScreenshot(element) {
|
|
407
|
+
try {
|
|
408
|
+
const rect = element.getBoundingClientRect();
|
|
409
|
+
const selector = getSelector(element);
|
|
410
|
+
|
|
411
|
+
// Calculate padding (50% width, 60% height)
|
|
412
|
+
const paddingX = rect.width * SCREENSHOT_PADDING_X / 2;
|
|
413
|
+
const paddingY = rect.height * SCREENSHOT_PADDING_Y / 2;
|
|
414
|
+
|
|
415
|
+
// Get bounds relative to page (not viewport) with padding
|
|
416
|
+
const bounds = {
|
|
417
|
+
x: Math.max(0, Math.round(rect.left + window.scrollX - paddingX)),
|
|
418
|
+
y: Math.max(0, Math.round(rect.top + window.scrollY - paddingY)),
|
|
419
|
+
width: Math.round(rect.width + (paddingX * 2)),
|
|
420
|
+
height: Math.round(rect.height + (paddingY * 2))
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
console.log('[CloneReact Renderer] Requesting screenshot with 20% width + 100% height padding for:', selector, bounds);
|
|
424
|
+
|
|
425
|
+
// Use Electron's native screenshot via IPC with bounds
|
|
426
|
+
const screenshot = await window.clonereact.captureElement(selector, bounds);
|
|
427
|
+
|
|
428
|
+
console.log('[CloneReact Renderer] IPC returned, screenshot:', screenshot ? screenshot.substring(0, 50) : 'NULL');
|
|
429
|
+
|
|
430
|
+
if (screenshot && screenshot.length > 1000) {
|
|
431
|
+
console.log('[CloneReact Renderer] Screenshot received:', screenshot.length, 'chars');
|
|
432
|
+
return screenshot;
|
|
433
|
+
} else {
|
|
434
|
+
console.error('[CloneReact Renderer] Screenshot too small or null:', screenshot ? screenshot.length : 0);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error('[CloneReact Renderer] Screenshot request failed:', error);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Extract DOM tree structure recursively
|
|
445
|
+
*/
|
|
446
|
+
function extractDOMTree(element, depth = 0, maxDepth = 3) {
|
|
447
|
+
if (!element || depth > maxDepth) return null;
|
|
448
|
+
|
|
449
|
+
// Skip script and style tags
|
|
450
|
+
if (element.tagName === 'SCRIPT' || element.tagName === 'STYLE') return null;
|
|
451
|
+
|
|
452
|
+
const node = {
|
|
453
|
+
type: 'element',
|
|
454
|
+
tagName: element.tagName.toLowerCase(),
|
|
455
|
+
name: element.tagName.toLowerCase(),
|
|
456
|
+
props: {},
|
|
457
|
+
children: []
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Extract attributes as props
|
|
461
|
+
Array.from(element.attributes || []).forEach(attr => {
|
|
462
|
+
if (attr.name === 'class') {
|
|
463
|
+
node.props.className = attr.value;
|
|
464
|
+
} else if (attr.name !== 'style') {
|
|
465
|
+
node.props[attr.name] = attr.value;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Extract state classes (hover, active, disabled, etc.)
|
|
470
|
+
const stateClasses = extractStateClasses(element);
|
|
471
|
+
if (stateClasses) {
|
|
472
|
+
node.states = stateClasses;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Extract children
|
|
476
|
+
Array.from(element.childNodes || []).forEach(child => {
|
|
477
|
+
if (child.nodeType === 3) { // Text node
|
|
478
|
+
const text = child.textContent.trim();
|
|
479
|
+
if (text.length > 0 && text.length < 200) {
|
|
480
|
+
node.children.push({ type: 'text', value: text });
|
|
481
|
+
}
|
|
482
|
+
} else if (child.nodeType === 1) { // Element node
|
|
483
|
+
const childNode = extractDOMTree(child, depth + 1, maxDepth);
|
|
484
|
+
if (childNode) {
|
|
485
|
+
node.children.push(childNode);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return node;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Basic element extraction (fallback if React Fiber not available)
|
|
495
|
+
*/
|
|
496
|
+
async function extractBasicElement(element) {
|
|
497
|
+
const rect = element.getBoundingClientRect();
|
|
498
|
+
|
|
499
|
+
console.log('[CloneReact Renderer] Starting extraction for:', element.tagName);
|
|
500
|
+
|
|
501
|
+
// Capture screenshot FIRST (returns Promise)
|
|
502
|
+
console.log('[CloneReact Renderer] About to call captureScreenshot...');
|
|
503
|
+
const screenshot = await captureScreenshot(element);
|
|
504
|
+
console.log('[CloneReact Renderer] captureScreenshot returned, type:', typeof screenshot);
|
|
505
|
+
console.log('[CloneReact Renderer] Screenshot value:', screenshot ? screenshot.substring(0, 100) : 'NULL OR UNDEFINED');
|
|
506
|
+
console.log('[CloneReact Renderer] Screenshot length:', screenshot ? screenshot.length : 0);
|
|
507
|
+
|
|
508
|
+
// Extract DOM tree
|
|
509
|
+
const domTree = extractDOMTree(element, 0, 5);
|
|
510
|
+
|
|
511
|
+
const extraction = {
|
|
512
|
+
selector: getSelector(element),
|
|
513
|
+
isReact: false,
|
|
514
|
+
reactVersion: null,
|
|
515
|
+
component: domTree,
|
|
516
|
+
html: element.outerHTML.substring(0, 10000),
|
|
517
|
+
styles: getComputedStylesObj(element),
|
|
518
|
+
screenshot: screenshot, // Assign the awaited screenshot
|
|
519
|
+
dimensions: {
|
|
520
|
+
width: rect.width,
|
|
521
|
+
height: rect.height,
|
|
522
|
+
top: rect.top,
|
|
523
|
+
left: rect.left
|
|
524
|
+
},
|
|
525
|
+
meta: {
|
|
526
|
+
tagName: element.tagName.toLowerCase(),
|
|
527
|
+
id: element.id,
|
|
528
|
+
classes: Array.from(element.classList)
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
console.log('[CloneReact Renderer] Built extraction object');
|
|
533
|
+
console.log('[CloneReact Renderer] extraction.screenshot exists?', !!extraction.screenshot);
|
|
534
|
+
console.log('[CloneReact Renderer] extraction.screenshot length:', extraction.screenshot ? extraction.screenshot.length : 0);
|
|
535
|
+
|
|
536
|
+
return extraction;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get CSS selector for element
|
|
541
|
+
*/
|
|
542
|
+
function getSelector(element) {
|
|
543
|
+
if (element.id) return '#' + element.id;
|
|
544
|
+
|
|
545
|
+
const path = [];
|
|
546
|
+
let current = element;
|
|
547
|
+
|
|
548
|
+
while (current && current !== document.body) {
|
|
549
|
+
let selector = current.tagName.toLowerCase();
|
|
550
|
+
|
|
551
|
+
if (current.className) {
|
|
552
|
+
const classes = current.className.split(' ').filter(c => c.trim());
|
|
553
|
+
if (classes.length > 0) {
|
|
554
|
+
selector += '.' + classes[0];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
path.unshift(selector);
|
|
559
|
+
current = current.parentElement;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return path.join(' > ');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Get computed styles as object
|
|
567
|
+
*/
|
|
568
|
+
function getComputedStylesObj(element) {
|
|
569
|
+
const computed = window.getComputedStyle(element);
|
|
570
|
+
const styles = {};
|
|
571
|
+
|
|
572
|
+
const props = [
|
|
573
|
+
'display', 'position', 'width', 'height', 'margin', 'padding',
|
|
574
|
+
'background', 'backgroundColor', 'border', 'borderRadius',
|
|
575
|
+
'color', 'fontSize', 'fontFamily', 'fontWeight',
|
|
576
|
+
'flex', 'flexDirection', 'alignItems', 'justifyContent', 'gap'
|
|
577
|
+
];
|
|
578
|
+
|
|
579
|
+
props.forEach(prop => {
|
|
580
|
+
const value = computed.getPropertyValue(prop);
|
|
581
|
+
if (value && value !== 'none' && value !== 'normal' && value !== '0px') {
|
|
582
|
+
styles[prop] = value;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return styles;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Extract state-based classes (hover, active, disabled, focus, etc.)
|
|
591
|
+
*/
|
|
592
|
+
function extractStateClasses(element) {
|
|
593
|
+
const className = element.className || '';
|
|
594
|
+
if (typeof className !== 'string') return {};
|
|
595
|
+
|
|
596
|
+
const classes = className.split(' ').filter(c => c.trim());
|
|
597
|
+
const states = {
|
|
598
|
+
hover: [],
|
|
599
|
+
active: [],
|
|
600
|
+
focus: [],
|
|
601
|
+
disabled: [],
|
|
602
|
+
checked: [],
|
|
603
|
+
selected: []
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const statePattern = /^(hover|active|focus|disabled|checked|selected):(.+)$/;
|
|
607
|
+
|
|
608
|
+
classes.forEach(cls => {
|
|
609
|
+
const match = cls.match(statePattern);
|
|
610
|
+
if (match) {
|
|
611
|
+
const [, state, style] = match;
|
|
612
|
+
if (states[state]) {
|
|
613
|
+
states[state].push(style);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Filter out empty state arrays
|
|
619
|
+
Object.keys(states).forEach(key => {
|
|
620
|
+
if (states[key].length === 0) {
|
|
621
|
+
delete states[key];
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
return Object.keys(states).length > 0 ? states : null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Cancel extraction
|
|
630
|
+
*/
|
|
631
|
+
function cancel() {
|
|
632
|
+
window.clonereact.cancel();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Export API
|
|
636
|
+
window.__CLONEREACT_INSPECTOR__ = {
|
|
637
|
+
init,
|
|
638
|
+
clearSelection,
|
|
639
|
+
extractComponent,
|
|
640
|
+
cancel
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
console.log('[CloneReact] Inspector module loaded');
|
|
644
|
+
})();
|
package/main.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const { app, BrowserWindow, ipcMain } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
let mainWindow = null;
|
|
6
|
+
|
|
7
|
+
function createWindow(url) {
|
|
8
|
+
mainWindow = new BrowserWindow({
|
|
9
|
+
width: 1440,
|
|
10
|
+
height: 900,
|
|
11
|
+
titleBarStyle: 'hiddenInset', // Hide title bar, keep traffic lights
|
|
12
|
+
webPreferences: {
|
|
13
|
+
nodeIntegration: false,
|
|
14
|
+
contextIsolation: true,
|
|
15
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
16
|
+
webSecurity: true,
|
|
17
|
+
},
|
|
18
|
+
backgroundColor: '#1a1a1a',
|
|
19
|
+
show: false,
|
|
20
|
+
title: 'Cluso Inspector'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
mainWindow.loadURL(url);
|
|
24
|
+
|
|
25
|
+
mainWindow.once('ready-to-show', () => {
|
|
26
|
+
mainWindow.show();
|
|
27
|
+
|
|
28
|
+
mainWindow.webContents.on('did-finish-load', () => {
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
injectInspector();
|
|
31
|
+
}, 1000);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
mainWindow.on('closed', () => {
|
|
36
|
+
mainWindow = null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function injectInspector() {
|
|
41
|
+
if (!mainWindow) return;
|
|
42
|
+
|
|
43
|
+
const inspectorScript = fs.readFileSync(
|
|
44
|
+
path.join(__dirname, 'cluso-inspector.js'),
|
|
45
|
+
'utf8'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Load cluso icon as base64 data URL
|
|
49
|
+
const iconPath = path.join(__dirname, 'assets', 'cluso-icon.png');
|
|
50
|
+
let iconDataUrl = '';
|
|
51
|
+
try {
|
|
52
|
+
const iconBuffer = fs.readFileSync(iconPath);
|
|
53
|
+
iconDataUrl = `data:image/png;base64,${iconBuffer.toString('base64')}`;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn('[CloneReact] Could not load cluso icon:', error.message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mainWindow.webContents.executeJavaScript(inspectorScript)
|
|
59
|
+
.then(() => {
|
|
60
|
+
return mainWindow.webContents.executeJavaScript(`
|
|
61
|
+
window.__CLONEREACT_ICON_URL__ = ${JSON.stringify(iconDataUrl)};
|
|
62
|
+
window.__CLONEREACT_INSPECTOR__.init();
|
|
63
|
+
`);
|
|
64
|
+
})
|
|
65
|
+
.then(() => {
|
|
66
|
+
console.log('[CloneReact] Inspector ready');
|
|
67
|
+
})
|
|
68
|
+
.catch((error) => {
|
|
69
|
+
console.error('[CloneReact] Injection failed:', error);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// IPC: Screenshot capture
|
|
74
|
+
ipcMain.handle('capture-element', async (event, selector, bounds) => {
|
|
75
|
+
if (!mainWindow) return null;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Capture only the element's bounding rectangle
|
|
79
|
+
const rect = bounds ? {
|
|
80
|
+
x: bounds.x,
|
|
81
|
+
y: bounds.y,
|
|
82
|
+
width: bounds.width,
|
|
83
|
+
height: bounds.height
|
|
84
|
+
} : undefined;
|
|
85
|
+
|
|
86
|
+
const image = await mainWindow.webContents.capturePage(rect);
|
|
87
|
+
const base64 = image.toPNG().toString('base64');
|
|
88
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
89
|
+
|
|
90
|
+
console.log('[CloneReact] Screenshot captured:', bounds ? `${bounds.width}x${bounds.height}` : 'full page', dataUrl.length, 'chars');
|
|
91
|
+
return dataUrl;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('[CloneReact] Screenshot failed:', error);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// IPC: Extraction complete
|
|
99
|
+
ipcMain.handle('extraction-complete', async (event, data) => {
|
|
100
|
+
const outputPath = process.argv[3] || '/tmp/clonereact-output-' + process.pid + '.json';
|
|
101
|
+
const language = process.argv[4] || 'typescript';
|
|
102
|
+
|
|
103
|
+
// Add language to output
|
|
104
|
+
const outputData = {
|
|
105
|
+
...data,
|
|
106
|
+
language
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
|
|
111
|
+
console.log('[CloneReact] Extraction saved:', outputPath);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('[CloneReact] Failed to save:', error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Close and exit
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
if (mainWindow) mainWindow.close();
|
|
119
|
+
app.quit();
|
|
120
|
+
}, 500);
|
|
121
|
+
|
|
122
|
+
return { success: true };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// IPC: Cancel
|
|
126
|
+
ipcMain.handle('extraction-cancelled', async () => {
|
|
127
|
+
if (mainWindow) mainWindow.close();
|
|
128
|
+
app.quit();
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// App lifecycle
|
|
133
|
+
app.whenReady().then(() => {
|
|
134
|
+
const targetURL = process.argv[2];
|
|
135
|
+
|
|
136
|
+
if (!targetURL) {
|
|
137
|
+
console.error('Usage: electron main.js <url> <output-json-path>');
|
|
138
|
+
app.quit();
|
|
139
|
+
process.exit(1);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
createWindow(targetURL);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
app.on('window-all-closed', () => {
|
|
147
|
+
app.quit();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
process.on('uncaughtException', (error) => {
|
|
151
|
+
console.error('[CloneReact] Error:', error);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cluso-inspector",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Visual element selector for extracting HTML and screenshots from websites",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cluso-inspector": "./bin/cluso-inspector.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"assets/",
|
|
12
|
+
"main.js",
|
|
13
|
+
"preload.js",
|
|
14
|
+
"cluso-inspector.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "electron .",
|
|
19
|
+
"prepublishOnly": "npm pack --dry-run"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"electron",
|
|
23
|
+
"inspector",
|
|
24
|
+
"component",
|
|
25
|
+
"extraction",
|
|
26
|
+
"html",
|
|
27
|
+
"screenshot",
|
|
28
|
+
"react",
|
|
29
|
+
"clonereact",
|
|
30
|
+
"web-scraping"
|
|
31
|
+
],
|
|
32
|
+
"author": "Jason Kneen",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"homepage": "https://github.com/jasonkneen/clone-react-skill#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/jasonkneen/clone-react-skill/issues"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/jasonkneen/clone-react-skill.git"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"electron": "^28.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/preload.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
// Expose safe IPC methods to renderer
|
|
4
|
+
contextBridge.exposeInMainWorld('clonereact', {
|
|
5
|
+
// Send extraction data back to main process
|
|
6
|
+
sendExtraction: (data) => ipcRenderer.invoke('extraction-complete', data),
|
|
7
|
+
|
|
8
|
+
// Cancel extraction
|
|
9
|
+
cancel: () => ipcRenderer.invoke('extraction-cancelled'),
|
|
10
|
+
|
|
11
|
+
// Request screenshot of element via main process
|
|
12
|
+
captureElement: (selector, bounds) => ipcRenderer.invoke('capture-element', selector, bounds),
|
|
13
|
+
|
|
14
|
+
// Preview phase actions
|
|
15
|
+
exportFiles: () => ipcRenderer.invoke('export-files'),
|
|
16
|
+
openFolder: () => ipcRenderer.invoke('open-folder'),
|
|
17
|
+
startOver: () => ipcRenderer.invoke('start-over'),
|
|
18
|
+
close: () => ipcRenderer.invoke('close-app'),
|
|
19
|
+
|
|
20
|
+
// Listen for generation complete event from main
|
|
21
|
+
onGenerationComplete: (callback) => {
|
|
22
|
+
ipcRenderer.on('generation-complete', (event, data) => callback(data));
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Get initial config
|
|
26
|
+
getConfig: () => ({
|
|
27
|
+
version: '1.0.0',
|
|
28
|
+
maxDepth: 5,
|
|
29
|
+
}),
|
|
30
|
+
});
|