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/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
+ });