electron-debug-skill 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/LICENSE +21 -0
- package/README.md +392 -0
- package/bin/cli.js +2 -0
- package/bin/daemon.js +2 -0
- package/dist/CDPClient.js +258 -0
- package/dist/daemon.js +407 -0
- package/dist/index.js +933 -0
- package/dist/types.js +2 -0
- package/package.json +34 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { CDPClient } from './CDPClient.js';
|
|
3
|
+
export class ElectronDebugDaemon {
|
|
4
|
+
server;
|
|
5
|
+
client = null;
|
|
6
|
+
options;
|
|
7
|
+
consoleMessages = [];
|
|
8
|
+
networkRequests = [];
|
|
9
|
+
networkResponses = [];
|
|
10
|
+
currentTarget = null;
|
|
11
|
+
running = false;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
port: options.port ?? 9229,
|
|
15
|
+
electronPort: options.electronPort ?? 9222,
|
|
16
|
+
electronHost: options.electronHost ?? '127.0.0.1',
|
|
17
|
+
};
|
|
18
|
+
this.server = http.createServer((req, res) => {
|
|
19
|
+
this.handleRequest(req, res).catch((err) => {
|
|
20
|
+
console.error('Request handler error:', err);
|
|
21
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async start() {
|
|
26
|
+
if (this.running) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
this.server.listen(this.options.port, () => {
|
|
31
|
+
this.running = true;
|
|
32
|
+
console.log(`Daemon listening on http://localhost:${this.options.port}`);
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
stop() {
|
|
38
|
+
if (this.client) {
|
|
39
|
+
this.client.disconnect();
|
|
40
|
+
this.client = null;
|
|
41
|
+
}
|
|
42
|
+
this.server.close();
|
|
43
|
+
this.running = false;
|
|
44
|
+
}
|
|
45
|
+
isRunning() {
|
|
46
|
+
return this.running;
|
|
47
|
+
}
|
|
48
|
+
async handleRequest(req, res) {
|
|
49
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
50
|
+
const pathname = url.pathname;
|
|
51
|
+
const method = req.method?.toUpperCase() || 'GET';
|
|
52
|
+
// Set CORS headers
|
|
53
|
+
res.setHeader('Content-Type', 'application/json');
|
|
54
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
55
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
56
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
57
|
+
if (method === 'OPTIONS') {
|
|
58
|
+
res.writeHead(204);
|
|
59
|
+
res.end();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Parse request body for POST requests
|
|
63
|
+
let body = {};
|
|
64
|
+
if (method === 'POST') {
|
|
65
|
+
const rawBody = await this.readBody(req);
|
|
66
|
+
try {
|
|
67
|
+
body = JSON.parse(rawBody);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Ignore parse errors
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Route requests
|
|
74
|
+
try {
|
|
75
|
+
if (method === 'GET' && pathname === '/status') {
|
|
76
|
+
await this.handleStatus(res);
|
|
77
|
+
}
|
|
78
|
+
else if (method === 'GET' && pathname === '/targets') {
|
|
79
|
+
await this.handleTargets(res);
|
|
80
|
+
}
|
|
81
|
+
else if (method === 'GET' && pathname === '/console') {
|
|
82
|
+
await this.handleConsole(res);
|
|
83
|
+
}
|
|
84
|
+
else if (method === 'GET' && pathname === '/screenshot') {
|
|
85
|
+
await this.handleScreenshot(res);
|
|
86
|
+
}
|
|
87
|
+
else if (method === 'POST' && pathname === '/connect') {
|
|
88
|
+
await this.handleConnect(res);
|
|
89
|
+
}
|
|
90
|
+
else if (method === 'POST' && pathname === '/switch-target') {
|
|
91
|
+
await this.handleSwitchTarget(res, body);
|
|
92
|
+
}
|
|
93
|
+
else if (method === 'POST' && pathname === '/eval') {
|
|
94
|
+
await this.handleEval(res, body);
|
|
95
|
+
}
|
|
96
|
+
else if (method === 'POST' && pathname === '/screenshot') {
|
|
97
|
+
await this.handleScreenshot(res);
|
|
98
|
+
}
|
|
99
|
+
else if (method === 'POST' && pathname === '/click') {
|
|
100
|
+
await this.handleClick(res, body);
|
|
101
|
+
}
|
|
102
|
+
else if (method === 'POST' && pathname === '/disconnect') {
|
|
103
|
+
await this.handleDisconnect(res);
|
|
104
|
+
}
|
|
105
|
+
else if (method === 'DELETE' && pathname === '/') {
|
|
106
|
+
await this.handleShutdown(res);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.sendJson(res, { success: false, error: `Unknown route: ${method} ${pathname}` });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
sendJson(res, data) {
|
|
117
|
+
res.end(JSON.stringify(data));
|
|
118
|
+
}
|
|
119
|
+
readBody(req) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
req.on('data', (chunk) => chunks.push(chunk.toString()));
|
|
123
|
+
req.on('end', () => resolve(chunks.join('')));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// GET /status
|
|
127
|
+
async handleStatus(res) {
|
|
128
|
+
const status = {
|
|
129
|
+
running: this.running,
|
|
130
|
+
connected: this.client?.isConnected() ?? false,
|
|
131
|
+
electronPort: this.options.electronPort,
|
|
132
|
+
electronHost: this.options.electronHost,
|
|
133
|
+
};
|
|
134
|
+
if (this.currentTarget) {
|
|
135
|
+
status.targetId = this.currentTarget.id;
|
|
136
|
+
status.targetTitle = this.currentTarget.title;
|
|
137
|
+
}
|
|
138
|
+
this.sendJson(res, { success: true, data: status });
|
|
139
|
+
}
|
|
140
|
+
// GET /targets
|
|
141
|
+
async handleTargets(res) {
|
|
142
|
+
if (!this.client || !this.client.isConnected()) {
|
|
143
|
+
this.sendJson(res, { success: false, error: 'Not connected to Electron' });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const targets = await this.client.getTargets();
|
|
148
|
+
this.sendJson(res, { success: true, data: targets });
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// POST /connect
|
|
155
|
+
async handleConnect(res) {
|
|
156
|
+
try {
|
|
157
|
+
// Get Electron targets via HTTP /json endpoint
|
|
158
|
+
const httpRes = await fetch(`http://${this.options.electronHost}:${this.options.electronPort}/json`);
|
|
159
|
+
if (!httpRes.ok) {
|
|
160
|
+
throw new Error(`Failed to get targets: ${httpRes.status} ${httpRes.statusText}`);
|
|
161
|
+
}
|
|
162
|
+
const targets = await httpRes.json();
|
|
163
|
+
// Find first page target
|
|
164
|
+
const pageTarget = targets.find((t) => t.type === 'page');
|
|
165
|
+
if (!pageTarget) {
|
|
166
|
+
throw new Error('No page target found');
|
|
167
|
+
}
|
|
168
|
+
// Disconnect existing client
|
|
169
|
+
if (this.client) {
|
|
170
|
+
this.client.disconnect();
|
|
171
|
+
}
|
|
172
|
+
// Create new client and connect to target
|
|
173
|
+
this.client = new CDPClient({
|
|
174
|
+
host: this.options.electronHost,
|
|
175
|
+
port: this.options.electronPort,
|
|
176
|
+
});
|
|
177
|
+
await this.client.connectToTarget(pageTarget.webSocketDebuggerUrl);
|
|
178
|
+
// Store current target info
|
|
179
|
+
this.currentTarget = {
|
|
180
|
+
id: pageTarget.id,
|
|
181
|
+
type: pageTarget.type,
|
|
182
|
+
title: pageTarget.title,
|
|
183
|
+
url: pageTarget.url,
|
|
184
|
+
attached: true,
|
|
185
|
+
};
|
|
186
|
+
// Enable console and network events
|
|
187
|
+
await this.client.enableConsole();
|
|
188
|
+
await this.client.enableRuntimeConsole();
|
|
189
|
+
await this.client.enableNetwork();
|
|
190
|
+
// Set up event listeners for console and network
|
|
191
|
+
this.client.on('Console.messageAdded', (params) => {
|
|
192
|
+
const p = params;
|
|
193
|
+
this.consoleMessages.push({
|
|
194
|
+
type: (p.message?.level ?? 'log'),
|
|
195
|
+
text: p.message?.text ?? '',
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
this.client.on('Runtime.consoleAPICalled', (params) => {
|
|
200
|
+
const p = params;
|
|
201
|
+
const text = p.args
|
|
202
|
+
?.map((a) => a.value ?? a.description ?? String(a))
|
|
203
|
+
.join(' ') ?? '';
|
|
204
|
+
this.consoleMessages.push({
|
|
205
|
+
type: (p.type ?? 'log'),
|
|
206
|
+
text,
|
|
207
|
+
timestamp: p.timestamp ?? Date.now(),
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
this.client.on('Network.requestWillBeSent', (params) => {
|
|
211
|
+
const p = params;
|
|
212
|
+
this.networkRequests.push({
|
|
213
|
+
requestId: p.requestId,
|
|
214
|
+
url: p.request.url,
|
|
215
|
+
method: p.request.method,
|
|
216
|
+
headers: p.request.headers,
|
|
217
|
+
documentURL: p.documentURL,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
this.client.on('Network.responseReceived', (params) => {
|
|
221
|
+
const p = params;
|
|
222
|
+
this.networkResponses.push({
|
|
223
|
+
requestId: p.requestId,
|
|
224
|
+
url: p.response.url,
|
|
225
|
+
status: p.response.status,
|
|
226
|
+
statusText: p.response.statusText,
|
|
227
|
+
headers: p.response.headers,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
this.sendJson(res, {
|
|
231
|
+
success: true,
|
|
232
|
+
data: { target: this.currentTarget, wsUrl: pageTarget.webSocketDebuggerUrl },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// POST /switch-target
|
|
240
|
+
async handleSwitchTarget(res, body) {
|
|
241
|
+
const targetId = body.targetId;
|
|
242
|
+
if (!targetId) {
|
|
243
|
+
this.sendJson(res, { success: false, error: 'Missing targetId' });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!this.client || !this.client.isConnected()) {
|
|
247
|
+
this.sendJson(res, { success: false, error: 'Not connected to Electron' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
// Get fresh targets
|
|
252
|
+
const httpRes = await fetch(`http://${this.options.electronHost}:${this.options.electronPort}/json`);
|
|
253
|
+
const targets = await httpRes.json();
|
|
254
|
+
const target = targets.find((t) => t.id === targetId);
|
|
255
|
+
if (!target) {
|
|
256
|
+
throw new Error(`Target not found: ${targetId}`);
|
|
257
|
+
}
|
|
258
|
+
// Disconnect from current target
|
|
259
|
+
this.client.disconnect();
|
|
260
|
+
// Connect to new target
|
|
261
|
+
await this.client.connectToTarget(target.webSocketDebuggerUrl);
|
|
262
|
+
// Update current target
|
|
263
|
+
this.currentTarget = {
|
|
264
|
+
id: target.id,
|
|
265
|
+
type: target.type,
|
|
266
|
+
title: target.title,
|
|
267
|
+
url: target.url,
|
|
268
|
+
attached: true,
|
|
269
|
+
};
|
|
270
|
+
// Re-enable events
|
|
271
|
+
await this.client.enableConsole();
|
|
272
|
+
await this.client.enableRuntimeConsole();
|
|
273
|
+
await this.client.enableNetwork();
|
|
274
|
+
this.sendJson(res, { success: true, data: this.currentTarget });
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// POST /eval
|
|
281
|
+
async handleEval(res, body) {
|
|
282
|
+
const expression = body.expression;
|
|
283
|
+
if (!expression) {
|
|
284
|
+
this.sendJson(res, { success: false, error: 'Missing expression' });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!this.client || !this.client.isConnected()) {
|
|
288
|
+
this.sendJson(res, { success: false, error: 'Not connected to Electron' });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const result = await this.client.evaluate(expression);
|
|
293
|
+
this.sendJson(res, { success: true, data: result });
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// GET/POST /screenshot
|
|
300
|
+
async handleScreenshot(res) {
|
|
301
|
+
if (!this.client || !this.client.isConnected()) {
|
|
302
|
+
this.sendJson(res, { success: false, error: 'Not connected to Electron' });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const screenshot = await this.client.captureScreenshot('png');
|
|
307
|
+
// Always return JSON for CLI compatibility
|
|
308
|
+
this.sendJson(res, { success: true, data: screenshot, format: 'png' });
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// POST /click
|
|
315
|
+
async handleClick(res, body) {
|
|
316
|
+
const selector = body.selector;
|
|
317
|
+
if (!selector) {
|
|
318
|
+
this.sendJson(res, { success: false, error: 'Missing selector' });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!this.client || !this.client.isConnected()) {
|
|
322
|
+
this.sendJson(res, { success: false, error: 'Not connected to Electron' });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
// Get document root
|
|
327
|
+
const root = await this.client.getDocument();
|
|
328
|
+
const nodeId = await this.client.querySelector(root.nodeId, selector);
|
|
329
|
+
if (!nodeId) {
|
|
330
|
+
this.sendJson(res, { success: false, error: `Element not found: ${selector}` });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Simulate click using JavaScript
|
|
334
|
+
await this.client.evaluate(`(function() {
|
|
335
|
+
const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
336
|
+
if (el) {
|
|
337
|
+
el.click();
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
})()`);
|
|
342
|
+
this.sendJson(res, { success: true, data: { selector, nodeId } });
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// GET /console
|
|
349
|
+
async handleConsole(res) {
|
|
350
|
+
if (!this.client || !this.client.isConnected()) {
|
|
351
|
+
this.sendJson(res, { success: false, error: 'Not connected to Electron' });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const messages = await this.client.getConsoleMessages();
|
|
356
|
+
this.sendJson(res, { success: true, data: messages });
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
this.sendJson(res, { success: false, error: String(err) });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// POST /disconnect
|
|
363
|
+
async handleDisconnect(res) {
|
|
364
|
+
if (this.client) {
|
|
365
|
+
this.client.disconnect();
|
|
366
|
+
this.client = null;
|
|
367
|
+
}
|
|
368
|
+
this.currentTarget = null;
|
|
369
|
+
this.consoleMessages = [];
|
|
370
|
+
this.networkRequests = [];
|
|
371
|
+
this.networkResponses = [];
|
|
372
|
+
this.sendJson(res, { success: true, data: { disconnected: true } });
|
|
373
|
+
}
|
|
374
|
+
// DELETE / (shutdown)
|
|
375
|
+
async handleShutdown(res) {
|
|
376
|
+
this.sendJson(res, { success: true, data: { shutdown: true } });
|
|
377
|
+
// Give time for response to be sent
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
this.stop();
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}, 100);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Allow running as standalone script
|
|
385
|
+
// Parse command-line arguments
|
|
386
|
+
const args = process.argv.slice(2);
|
|
387
|
+
const options = {};
|
|
388
|
+
for (let i = 0; i < args.length; i++) {
|
|
389
|
+
if (args[i] === '--port' && i + 1 < args.length) {
|
|
390
|
+
options.port = parseInt(args[i + 1], 10);
|
|
391
|
+
i++;
|
|
392
|
+
}
|
|
393
|
+
else if (args[i] === '--electron-port' && i + 1 < args.length) {
|
|
394
|
+
options.electronPort = parseInt(args[i + 1], 10);
|
|
395
|
+
i++;
|
|
396
|
+
}
|
|
397
|
+
else if (args[i] === '--electron-host' && i + 1 < args.length) {
|
|
398
|
+
options.electronHost = args[i + 1];
|
|
399
|
+
i++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const daemon = new ElectronDebugDaemon(options);
|
|
403
|
+
daemon.start().catch(console.error);
|
|
404
|
+
process.on('SIGINT', () => {
|
|
405
|
+
daemon.stop();
|
|
406
|
+
process.exit(0);
|
|
407
|
+
});
|