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/index.js ADDED
@@ -0,0 +1,933 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import { CDPClient } from './CDPClient.js';
4
+ import http from 'http';
5
+ // Global state
6
+ let client = null;
7
+ let currentTargetId = null;
8
+ let consoleWatcher = false;
9
+ let networkWatcher = false;
10
+ let connectionHost = '127.0.0.1';
11
+ let connectionPort = 9222;
12
+ let daemonProcess = null;
13
+ const DAEMON_PORT = 9229;
14
+ // Daemon helper functions
15
+ async function daemonRequest(endpoint, method, body) {
16
+ return new Promise((resolve, reject) => {
17
+ const req = http.request({
18
+ hostname: '127.0.0.1',
19
+ port: DAEMON_PORT,
20
+ path: endpoint,
21
+ method,
22
+ headers: { 'Content-Type': 'application/json' },
23
+ }, (res) => {
24
+ let data = '';
25
+ res.on('data', (chunk) => (data += chunk));
26
+ res.on('end', () => {
27
+ try {
28
+ const json = JSON.parse(data);
29
+ if (json.success)
30
+ resolve(json.data);
31
+ else
32
+ reject(new Error(json.error ?? 'Unknown error'));
33
+ }
34
+ catch {
35
+ reject(new Error(data));
36
+ }
37
+ });
38
+ });
39
+ req.on('error', reject);
40
+ if (body)
41
+ req.write(JSON.stringify(body));
42
+ req.end();
43
+ });
44
+ }
45
+ async function daemonCheck() {
46
+ return new Promise((resolve) => {
47
+ const req = http.request({
48
+ hostname: '127.0.0.1',
49
+ port: DAEMON_PORT,
50
+ path: '/status',
51
+ method: 'GET',
52
+ }, (res) => {
53
+ let data = '';
54
+ res.on('data', (chunk) => (data += chunk));
55
+ res.on('end', () => {
56
+ try {
57
+ const json = JSON.parse(data);
58
+ resolve(json.success && json.data?.running === true);
59
+ }
60
+ catch {
61
+ resolve(false);
62
+ }
63
+ });
64
+ });
65
+ req.on('error', () => resolve(false));
66
+ req.end();
67
+ });
68
+ }
69
+ async function daemonStatus() {
70
+ return new Promise((resolve) => {
71
+ const req = http.request({
72
+ hostname: '127.0.0.1',
73
+ port: DAEMON_PORT,
74
+ path: '/status',
75
+ method: 'GET',
76
+ }, (res) => {
77
+ let data = '';
78
+ res.on('data', (chunk) => (data += chunk));
79
+ res.on('end', () => {
80
+ try {
81
+ const json = JSON.parse(data);
82
+ if (json.success && json.data)
83
+ resolve(json.data);
84
+ else
85
+ resolve({ running: false, connected: false });
86
+ }
87
+ catch {
88
+ resolve({ running: false, connected: false });
89
+ }
90
+ });
91
+ });
92
+ req.on('error', () => resolve({ running: false, connected: false }));
93
+ req.end();
94
+ });
95
+ }
96
+
97
+ /**
98
+ * 自动管理 daemon 连接
99
+ * 如果 daemon 未运行,自动启动
100
+ * 如果 daemon 未连接,自动连接
101
+ * 返回 daemonStatus 信息
102
+ */
103
+ async function ensureDaemon(args) {
104
+ const electronPort = Number(args.electronPort) || Number(args.port) || 9222;
105
+ const electronHost = args.host ? String(args.host) : '127.0.0.1';
106
+
107
+ // 如果是 connect 或 daemon 命令,不需要自动连接
108
+ const command = args._ && Array.isArray(args._) ? args._[0] : '';
109
+ if (command === 'connect' || command === 'daemon') {
110
+ return { started: false, running: false, connected: false };
111
+ }
112
+
113
+ // 检查 daemon 是否运行
114
+ const isRunning = await daemonCheck();
115
+ if (!isRunning) {
116
+ // 自动启动 daemon
117
+ console.log('Daemon not running, starting...');
118
+ await cmdDaemonStart({ electronPort, electronHost });
119
+ }
120
+
121
+ // 检查是否已连接
122
+ const status = await daemonStatus();
123
+ if (!status.connected) {
124
+ console.log('Not connected to Electron, connecting...');
125
+ await daemonRequest('/connect', 'POST', { host: electronHost, port: electronPort });
126
+ }
127
+
128
+ return { started: !isRunning, running: true, connected: true };
129
+ }
130
+
131
+ /**
132
+ * 便捷函数:确保已连接 Electron
133
+ * 用于需要 CDP 连接的命令
134
+ */
135
+ async function ensureConnected() {
136
+ const status = await daemonStatus();
137
+ if (!status.running) {
138
+ throw new Error('Daemon not running. Use "connect --electron-port <port>" first.');
139
+ }
140
+ if (!status.connected) {
141
+ throw new Error('Not connected to Electron. Use "connect --electron-port <port>" first.');
142
+ }
143
+ return status;
144
+ return status;
145
+ }
146
+
147
+ function parseArgs(argv) {
148
+ const command = argv[2] || 'status';
149
+ const args = {};
150
+
151
+ // Parse all arguments starting from index 3
152
+ for (let i = 3; i < argv.length; i++) {
153
+ const arg = argv[i];
154
+ if (arg.startsWith('--')) {
155
+ let key = arg.slice(2);
156
+ // Convert hyphenated keys to camelCase (e.g., electron-port -> electronPort)
157
+ key = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
158
+ const next = argv[i + 1];
159
+ if (next && !next.startsWith('--')) {
160
+ // Try to parse as number
161
+ const num = Number(next);
162
+ args[key] = isNaN(num) ? next : num;
163
+ i++;
164
+ }
165
+ else {
166
+ args[key] = true;
167
+ }
168
+ }
169
+ else if (!args._) {
170
+ // First non-flag argument is the subcommand for daemon
171
+ args._ = [arg];
172
+ }
173
+ }
174
+
175
+ // If no command was set, default to status
176
+ if (command === 'daemon' && !args._[0]) {
177
+ args._ = [''];
178
+ }
179
+
180
+ return { command, args };
181
+ }
182
+ async function runCommand(cli) {
183
+ const { command, args } = cli;
184
+
185
+ // 自动管理 daemon(connect 和 daemon 命令不需要)
186
+ if (command !== 'connect' && command !== 'daemon' && command !== 'status') {
187
+ await ensureDaemon(args);
188
+ }
189
+
190
+ switch (command) {
191
+ case 'connect':
192
+ await cmdConnect(args);
193
+ break;
194
+ case 'disconnect':
195
+ cmdDisconnect();
196
+ break;
197
+ case 'status':
198
+ await cmdStatus();
199
+ break;
200
+ case 'list-pages':
201
+ await cmdListPages(args);
202
+ break;
203
+ case 'switch-page':
204
+ await cmdSwitchPage(args);
205
+ break;
206
+ case 'console':
207
+ await cmdConsole(args);
208
+ break;
209
+ case 'network':
210
+ await cmdNetwork(args);
211
+ break;
212
+ case 'screenshot':
213
+ await cmdScreenshot(args);
214
+ break;
215
+ case 'dom':
216
+ await cmdDom(args);
217
+ break;
218
+ case 'eval':
219
+ await cmdEval(args);
220
+ break;
221
+ case 'call':
222
+ await cmdEval(args); // call is similar to eval
223
+ break;
224
+ case 'breakpoint':
225
+ await cmdBreakpoint(args);
226
+ break;
227
+ case 'step':
228
+ await cmdStep(args);
229
+ break;
230
+ case 'main-connect':
231
+ await cmdMainConnect(args);
232
+ break;
233
+ case 'main-logs':
234
+ await cmdMainLogs();
235
+ break;
236
+ case 'diagnose':
237
+ await cmdDiagnose(args);
238
+ break;
239
+ case 'page':
240
+ await cmdPage(args);
241
+ break;
242
+ case 'click':
243
+ await cmdClick(args);
244
+ break;
245
+ case 'daemon':
246
+ await cmdDaemon(args);
247
+ break;
248
+ default:
249
+ console.log(`Unknown command: ${command}`);
250
+ console.log('Use --help to see available commands');
251
+ }
252
+ }
253
+ // Command implementations
254
+ async function cmdConnect(args) {
255
+ const electronPort = Number(args.electronPort) || Number(args.port) || 9222;
256
+ const electronHost = args.host ? String(args.host) : '127.0.0.1';
257
+
258
+ // Check if daemon is running
259
+ const isRunning = await daemonCheck();
260
+ if (!isRunning) {
261
+ // Start daemon first
262
+ console.log('Starting daemon...');
263
+ await cmdDaemonStart({ electronPort, electronHost });
264
+ }
265
+
266
+ // Ask daemon to connect to Electron
267
+ try {
268
+ const result = await daemonRequest('/connect', 'POST', { host: electronHost, port: electronPort });
269
+ console.log(`Connected to ${electronHost}:${electronPort}`);
270
+ if (result.target) {
271
+ console.log(`Attached to: ${result.target.title} (${result.target.url})`);
272
+ }
273
+ } catch (err) {
274
+ console.error('Failed to connect:', err.message);
275
+ }
276
+ }
277
+ async function cmdDisconnect() {
278
+ // Check if daemon is running
279
+ const isRunning = await daemonCheck();
280
+ if (!isRunning) {
281
+ console.log('Not connected');
282
+ return;
283
+ }
284
+
285
+ // Tell daemon to disconnect
286
+ try {
287
+ await daemonRequest('/disconnect', 'POST', {});
288
+ console.log('Disconnected from Electron');
289
+ } catch (err) {
290
+ console.error('Error:', err.message);
291
+ }
292
+ }
293
+ async function cmdStatus() {
294
+ // Check daemon status
295
+ const status = await daemonStatus();
296
+ if (!status.running) {
297
+ console.log('Daemon: Not running');
298
+ console.log('Use "connect --electron-port <port>" to start');
299
+ return;
300
+ }
301
+ console.log('Daemon: Running');
302
+ console.log(`Connected to Electron: ${status.connected ? 'Yes' : 'No'}`);
303
+ if (status.electronPort) {
304
+ console.log(`Electron port: ${status.electronPort}`);
305
+ }
306
+ if (status.targetTitle) {
307
+ console.log(`Target: ${status.targetTitle}`);
308
+ }
309
+ }
310
+ async function cmdListPages(args) {
311
+ const port = Number(args.port) || connectionPort;
312
+ const host = args.host ? String(args.host) : connectionHost;
313
+ // Fetch targets via HTTP JSON endpoint (doesn't require connection)
314
+ const http = await import('http');
315
+ const targetsJson = await new Promise((resolve, reject) => {
316
+ http.get(`http://${host}:${port}/json`, (res) => {
317
+ let data = '';
318
+ res.on('data', (chunk) => (data += chunk));
319
+ res.on('end', () => resolve(data));
320
+ res.on('error', reject);
321
+ }).on('error', reject);
322
+ });
323
+ const targets = JSON.parse(targetsJson);
324
+ console.log('\nAvailable Pages:');
325
+ console.log('─'.repeat(60));
326
+ targets
327
+ .filter((t) => t.type === 'page')
328
+ .forEach((t, i) => {
329
+ const marker = t.id === currentTargetId ? '→ ' : ' ';
330
+ console.log(`${marker}[${i + 1}] ${t.title}`);
331
+ console.log(` URL: ${t.url}`);
332
+ console.log(` ID: ${t.id}`);
333
+ console.log('');
334
+ });
335
+ }
336
+ async function cmdSwitchPage(args) {
337
+ if (!client) {
338
+ console.log('Not connected.');
339
+ return;
340
+ }
341
+ const targetId = String(args.id);
342
+ // Fetch targets via HTTP to get WebSocket URL
343
+ const http = await import('http');
344
+ const targetsJson = await new Promise((resolve, reject) => {
345
+ http.get(`http://${connectionHost}:${connectionPort}/json`, (res) => {
346
+ let data = '';
347
+ res.on('data', (chunk) => (data += chunk));
348
+ res.on('end', () => resolve(data));
349
+ res.on('error', reject);
350
+ }).on('error', reject);
351
+ });
352
+ const targets = JSON.parse(targetsJson);
353
+ const target = targets.find((t) => t.id === targetId);
354
+ if (!target) {
355
+ console.log(`Target not found: ${targetId}`);
356
+ return;
357
+ }
358
+ client.disconnect();
359
+ await client.connectToTarget(target.webSocketDebuggerUrl);
360
+ currentTargetId = targetId;
361
+ console.log(`Switched to: ${target.title}`);
362
+ }
363
+ async function cmdConsole(args) {
364
+ // Try daemon mode first
365
+ const isDaemonRunning = await daemonCheck();
366
+ if (isDaemonRunning) {
367
+ try {
368
+ const messages = await daemonRequest('/console', 'GET');
369
+ const filterType = String(args.type || 'all');
370
+ console.log('\nConsole Messages:');
371
+ console.log('─'.repeat(60));
372
+ (messages || [])
373
+ .filter((m) => filterType === 'all' || m.type === filterType)
374
+ .forEach((m) => {
375
+ console.log(`[${m.type}] ${m.text}`);
376
+ });
377
+ } catch (err) {
378
+ console.error('Error:', err.message);
379
+ }
380
+ return;
381
+ }
382
+ // Direct mode
383
+ if (!client) {
384
+ console.log('Not connected.');
385
+ return;
386
+ }
387
+ if (args.watch) {
388
+ consoleWatcher = true;
389
+ await client.enableConsole();
390
+ console.log('Watching console... (Ctrl+C to stop)');
391
+ client.on('Console.messageAdded', (params) => {
392
+ const msg = params;
393
+ const time = msg.timestamp ? new Date(msg.timestamp).toISOString() : '';
394
+ console.log(`[${time}] [${msg.type}] ${msg.text}`);
395
+ });
396
+ return;
397
+ }
398
+ if (args.clear) {
399
+ console.log('Console cleared');
400
+ return;
401
+ }
402
+ const messages = await client.getConsoleMessages();
403
+ const filterType = String(args.type || 'all');
404
+ console.log('\nConsole Messages:');
405
+ console.log('─'.repeat(60));
406
+ messages
407
+ .filter((m) => filterType === 'all' || m.type === filterType)
408
+ .forEach((m) => {
409
+ console.log(`[${m.type}] ${m.text}`);
410
+ if (m.args?.length) {
411
+ console.log(' Args:', m.args);
412
+ }
413
+ });
414
+ }
415
+ async function cmdNetwork(args) {
416
+ // Try daemon mode first (for non-watch modes)
417
+ if (!args.watch && !args.pause && !args.resume && !args.request) {
418
+ const isDaemonRunning = await daemonCheck();
419
+ if (isDaemonRunning) {
420
+ try {
421
+ const result = await daemonRequest('/network', 'GET');
422
+ if (result.requests && result.requests.length > 0) {
423
+ console.log('\nNetwork Requests:');
424
+ console.log('─'.repeat(60));
425
+ result.requests.forEach((req) => {
426
+ console.log(`→ ${req.method} ${req.url}`);
427
+ });
428
+ }
429
+ else {
430
+ console.log('No network requests recorded.');
431
+ }
432
+ if (result.responses && result.responses.length > 0) {
433
+ console.log('\nNetwork Responses:');
434
+ console.log('─'.repeat(60));
435
+ result.responses.forEach((resp) => {
436
+ console.log(`← [${resp.status}] ${resp.url}`);
437
+ });
438
+ }
439
+ } catch (err) {
440
+ console.error('Error:', err.message);
441
+ }
442
+ return;
443
+ }
444
+ }
445
+ // Direct mode (or watch mode)
446
+ if (!client) {
447
+ console.log('Not connected.');
448
+ return;
449
+ }
450
+ if (args.watch) {
451
+ networkWatcher = true;
452
+ await client.enableNetwork();
453
+ console.log('Watching network... (Ctrl+C to stop)');
454
+ client.on('Network.requestWillBeSent', (params) => {
455
+ const req = params;
456
+ console.log(`→ ${req.request.method} ${req.request.url}`);
457
+ });
458
+ client.on('Network.responseReceived', (params) => {
459
+ const resp = params;
460
+ console.log(`← [${resp.response.status}] ${resp.response.url}`);
461
+ });
462
+ return;
463
+ }
464
+ if (args.pause) {
465
+ await client.disableNetwork();
466
+ console.log('Network monitoring paused');
467
+ return;
468
+ }
469
+ if (args.resume) {
470
+ await client.enableNetwork();
471
+ console.log('Network monitoring resumed');
472
+ return;
473
+ }
474
+ if (args.request) {
475
+ const body = await client.getResponseBody(String(args.request));
476
+ console.log('\nResponse Body:');
477
+ console.log('─'.repeat(60));
478
+ console.log(body);
479
+ return;
480
+ }
481
+ // Default: show recent requests
482
+ await client.enableNetwork();
483
+ console.log('Network monitoring enabled (use --watch to stream)');
484
+ }
485
+ async function cmdScreenshot(args) {
486
+ // Try daemon mode first
487
+ const isDaemonRunning = await daemonCheck();
488
+ if (isDaemonRunning) {
489
+ const path = String(args.path || '');
490
+ try {
491
+ const result = await daemonRequest('/screenshot', 'GET');
492
+ if (path) {
493
+ const fs = await import('fs/promises');
494
+ const buffer = Buffer.from(result.data, 'base64');
495
+ await fs.writeFile(path, buffer);
496
+ console.log(`Screenshot saved to: ${path}`);
497
+ } else {
498
+ console.log(`Screenshot captured (${result.data.length} bytes, base64)`);
499
+ console.log(`Preview: data:image/${result.format || 'png'};base64,${result.data.slice(0, 100)}...`);
500
+ }
501
+ } catch (err) {
502
+ console.error('Error:', err.message);
503
+ }
504
+ return;
505
+ }
506
+ // Direct mode
507
+ if (!client || !client.isConnected()) {
508
+ console.log('Not connected. Use "daemon start" or "connect" first.');
509
+ return;
510
+ }
511
+ const path = String(args.path || '');
512
+ const format = args.jpeg ? 'jpeg' : 'png';
513
+ const quality = args.jpeg ? 80 : undefined;
514
+ const result = await client.captureScreenshot(format, quality);
515
+ if (path) {
516
+ const fs = await import('fs/promises');
517
+ const buffer = Buffer.from(result.data, 'base64');
518
+ await fs.writeFile(path, buffer);
519
+ console.log(`Screenshot saved to: ${path}`);
520
+ }
521
+ else {
522
+ console.log(`Screenshot captured (${result.data.length} bytes, base64)`);
523
+ console.log(`Preview: data:image/${format};base64,${result.data.slice(0, 100)}...`);
524
+ }
525
+ }
526
+ async function cmdDom(args) {
527
+ // Try daemon mode first
528
+ const isDaemonRunning = await daemonCheck();
529
+ if (isDaemonRunning) {
530
+ const selector = String(args.selector || 'body');
531
+ const props = String(args.props || '');
532
+ try {
533
+ const result = await daemonRequest('/dom', 'POST', { selector, props });
534
+ console.log(`\nElement: ${selector}`);
535
+ console.log('─'.repeat(60));
536
+ console.log('HTML:', (result.html || '').slice(0, 500));
537
+ if (result.attrs && props) {
538
+ console.log('\nAttributes:');
539
+ result.attrs
540
+ .filter(([name]) => props.split(',').map((p) => p.trim()).includes(name))
541
+ .forEach(([name, value]) => {
542
+ console.log(` ${name}="${value}"`);
543
+ });
544
+ }
545
+ } catch (err) {
546
+ console.error('Error:', err.message);
547
+ }
548
+ return;
549
+ }
550
+ // Direct mode
551
+ if (!client) {
552
+ console.log('Not connected.');
553
+ return;
554
+ }
555
+ const selector = String(args.selector || 'body');
556
+ const props = String(args.props || '');
557
+ const doc = await client.getDocument();
558
+ const nodeId = await client.querySelector(doc.nodeId, selector);
559
+ if (!nodeId) {
560
+ console.log(`Element not found: ${selector}`);
561
+ return;
562
+ }
563
+ console.log(`\nElement: ${selector}`);
564
+ console.log('─'.repeat(60));
565
+ const html = await client.getOuterHTML(nodeId);
566
+ console.log('HTML:', html.slice(0, 500));
567
+ if (props) {
568
+ const attrs = await client.getAttributes(nodeId);
569
+ console.log('\nAttributes:');
570
+ attrs
571
+ .filter(([name]) => props.split(',').map((p) => p.trim()).includes(name))
572
+ .forEach(([name, value]) => {
573
+ console.log(` ${name}="${value}"`);
574
+ });
575
+ }
576
+ }
577
+ async function cmdEval(args) {
578
+ // Try daemon mode first
579
+ const isDaemonRunning = await daemonCheck();
580
+ if (isDaemonRunning) {
581
+ const expr = String((args._ && Array.isArray(args._) && args._[0]) || args.expression || '');
582
+ if (!expr) {
583
+ console.log('No expression provided. Usage: eval "document.title"');
584
+ return;
585
+ }
586
+ try {
587
+ const result = await daemonRequest('/eval', 'POST', { expression: expr });
588
+ if (typeof result === 'object') {
589
+ console.log(JSON.stringify(result, null, 2));
590
+ }
591
+ else {
592
+ console.log(result);
593
+ }
594
+ } catch (err) {
595
+ console.error('Error:', err.message);
596
+ }
597
+ return;
598
+ }
599
+ // Direct mode
600
+ if (!client) {
601
+ console.log('Not connected.');
602
+ return;
603
+ }
604
+ const expr = String((args._ && Array.isArray(args._) && args._[0]) || args.expression || ''); // Handle positional arg
605
+ if (!expr) {
606
+ console.log('No expression provided. Usage: eval "document.title"');
607
+ return;
608
+ }
609
+ const result = await client.evaluate(expr);
610
+ if (typeof result === 'object') {
611
+ console.log(JSON.stringify(result, null, 2));
612
+ }
613
+ else {
614
+ console.log(result);
615
+ }
616
+ }
617
+ async function cmdBreakpoint(args) {
618
+ if (!client) {
619
+ console.log('Not connected.');
620
+ return;
621
+ }
622
+ await client.enableDebugger();
623
+ if (args.list) {
624
+ const breakpoints = await client.listBreakpoints();
625
+ console.log('\nBreakpoints:');
626
+ console.log('─'.repeat(60));
627
+ breakpoints.forEach((bp, i) => {
628
+ console.log(`[${i + 1}] ${bp.location.url}:${bp.location.lineNumber}`);
629
+ });
630
+ return;
631
+ }
632
+ const url = String(args.url || '');
633
+ const line = Number(args.line) || 0;
634
+ if (!url || !line) {
635
+ console.log('Usage: breakpoint --url <file> --line <num>');
636
+ return;
637
+ }
638
+ const id = await client.setBreakpoint(url, line);
639
+ console.log(`Breakpoint set: ${id}`);
640
+ }
641
+ async function cmdStep(args) {
642
+ if (!client) {
643
+ console.log('Not connected.');
644
+ return;
645
+ }
646
+ const action = String(args.action || 'next');
647
+ switch (action) {
648
+ case 'next':
649
+ await client.stepNext();
650
+ break;
651
+ case 'in':
652
+ await client.stepInto();
653
+ break;
654
+ case 'out':
655
+ await client.stepOut();
656
+ break;
657
+ default:
658
+ await client.resume();
659
+ }
660
+ console.log(`Step: ${action}`);
661
+ }
662
+ async function cmdMainConnect(args) {
663
+ // Main process uses different debugging protocol (V8 Inspector Protocol)
664
+ const port = Number(args.port) || 9229;
665
+ const http = await import('http');
666
+ return new Promise((resolve, reject) => {
667
+ const req = http.get(`http://127.0.0.1:${port}/json`, (res) => {
668
+ let data = '';
669
+ res.on('data', (chunk) => (data += chunk));
670
+ res.on('end', () => {
671
+ try {
672
+ const targets = JSON.parse(data);
673
+ console.log('Main Process Debugger');
674
+ console.log('─'.repeat(60));
675
+ console.log(`WebSocket URL: ${targets[0]?.webSocketDebuggerUrl}`);
676
+ console.log('\nConnect using:');
677
+ console.log(` /electron-debug main-connect --port ${port}`);
678
+ }
679
+ catch {
680
+ console.log('Failed to parse debug info');
681
+ }
682
+ resolve();
683
+ });
684
+ });
685
+ req.on('error', reject);
686
+ });
687
+ }
688
+ async function cmdMainLogs() {
689
+ if (!client) {
690
+ console.log('Not connected.');
691
+ return;
692
+ }
693
+ await client.enableLog();
694
+ const entries = await client.getLogEntries();
695
+ console.log('\nBrowser Logs:');
696
+ console.log('─'.repeat(60));
697
+ entries.entries.forEach((e) => {
698
+ console.log(`[${e.level}] ${e.text}`);
699
+ });
700
+ }
701
+ async function cmdDiagnose(args) {
702
+ if (!client) {
703
+ console.log('Not connected. Run "connect" first.');
704
+ return;
705
+ }
706
+ const problem = String((args._ && Array.isArray(args._) && args._[0]) || args.problem || ''); // positional or --problem
707
+ if (!problem) {
708
+ console.log('Usage: /electron-debug diagnose "<问题描述>"');
709
+ return;
710
+ }
711
+ console.log(`\n🔍 Diagnosing: "${problem}"`);
712
+ console.log('─'.repeat(60));
713
+ const findings = [];
714
+ // Step 1: Check console for errors
715
+ try {
716
+ const messages = await client.getConsoleMessages();
717
+ const errors = messages.filter((m) => m.type === 'error');
718
+ if (errors.length > 0) {
719
+ findings.push({
720
+ type: 'error',
721
+ message: `Found ${errors.length} console error(s)`,
722
+ details: { errors: errors.map((e) => e.text) },
723
+ });
724
+ }
725
+ }
726
+ catch {
727
+ // Console might not be enabled
728
+ }
729
+ // Step 2: Check network for failed requests
730
+ try {
731
+ await client.enableNetwork();
732
+ }
733
+ catch {
734
+ // ignore
735
+ }
736
+ // Step 3: Try to get page info
737
+ try {
738
+ await client.enableConsole();
739
+ const title = await client.evaluate('document.title');
740
+ findings.push({
741
+ type: 'info',
742
+ message: `Page title: "${title}"`,
743
+ });
744
+ }
745
+ catch {
746
+ findings.push({
747
+ type: 'warning',
748
+ message: 'Could not evaluate page title',
749
+ });
750
+ }
751
+ // Step 4: Check for common issues based on problem text
752
+ const problemLower = problem.toLowerCase();
753
+ if (problemLower.includes('点击') || problemLower.includes('click')) {
754
+ findings.push({
755
+ type: 'info',
756
+ message: 'Tip: Check if click handlers are properly attached',
757
+ details: { suggestion: 'Use Runtime.evaluate to check element.click() directly' },
758
+ });
759
+ }
760
+ if (problemLower.includes('加载') || problemLower.includes('load')) {
761
+ findings.push({
762
+ type: 'info',
763
+ message: 'Tip: Check Network panel for slow/failed requests',
764
+ details: { suggestion: 'Use --watch to monitor network activity' },
765
+ });
766
+ }
767
+ // Output findings
768
+ console.log('\n📋 Findings:');
769
+ findings.forEach((f, i) => {
770
+ const icon = f.type === 'error' ? '❌' : f.type === 'warning' ? '⚠️' : 'ℹ️';
771
+ console.log(` ${icon} ${f.message}`);
772
+ if (f.details) {
773
+ console.log(` → ${JSON.stringify(f.details)}`);
774
+ }
775
+ });
776
+ // Suggestions
777
+ console.log('\n💡 Next Steps:');
778
+ console.log(' 1. Check /electron-debug console --watch for real-time errors');
779
+ console.log(' 2. Run /electron-debug network --watch to monitor requests');
780
+ console.log(' 3. Try /electron-debug eval "<js expression>" to test behavior');
781
+ }
782
+ async function cmdPage(args) {
783
+ if (!client) {
784
+ console.log('Not connected.');
785
+ return;
786
+ }
787
+ if (args.info) {
788
+ try {
789
+ const doc = await client.getDocument();
790
+ console.log('\n📄 Page Info:');
791
+ console.log('─'.repeat(60));
792
+ console.log(`Root node ID: ${doc.nodeId}`);
793
+ console.log(`Document URL: ${doc.documentURL || 'N/A'}`);
794
+ }
795
+ catch {
796
+ console.log('Could not get page info');
797
+ }
798
+ }
799
+ }
800
+ async function cmdClick(args) {
801
+ // Try daemon mode first
802
+ const isDaemonRunning = await daemonCheck();
803
+ if (isDaemonRunning) {
804
+ const selector = String((args._ && Array.isArray(args._) && args._[0]) || args.selector || '');
805
+ if (!selector) {
806
+ console.log('Usage: click "#selector" or click --selector "#selector"');
807
+ return;
808
+ }
809
+ try {
810
+ const result = await daemonRequest('/click', 'POST', { selector });
811
+ console.log(`Clicked: ${result.selector} (nodeId: ${result.nodeId})`);
812
+ }
813
+ catch (err) {
814
+ console.error('Error:', err.message);
815
+ }
816
+ return;
817
+ }
818
+ // Direct mode
819
+ if (!client || !client.isConnected()) {
820
+ console.log('Not connected. Use "daemon start" or "connect" first.');
821
+ return;
822
+ }
823
+ const selector = String((args._ && Array.isArray(args._) && args._[0]) || args.selector || '');
824
+ if (!selector) {
825
+ console.log('Usage: click "#selector" or click --selector "#selector"');
826
+ return;
827
+ }
828
+ const doc = await client.getDocument();
829
+ const nodeId = await client.querySelector(doc.nodeId, selector);
830
+ if (!nodeId) {
831
+ console.log(`Element not found: ${selector}`);
832
+ return;
833
+ }
834
+ await client.evaluate(`(function() {
835
+ const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
836
+ if (el) { el.click(); return true; }
837
+ return false;
838
+ })()`);
839
+ console.log(`Clicked: ${selector} (nodeId: ${nodeId})`);
840
+ }
841
+ async function cmdDaemon(args) {
842
+ const subCommand = (args._ && Array.isArray(args._) && args._[0]) || '';
843
+ switch (subCommand) {
844
+ case 'start':
845
+ await cmdDaemonStart(args);
846
+ break;
847
+ case 'stop':
848
+ await cmdDaemonStop();
849
+ break;
850
+ case 'status':
851
+ await cmdDaemonStatus();
852
+ break;
853
+ default:
854
+ console.log('Daemon commands:');
855
+ console.log(' daemon start --electron-port <port> Start daemon and connect to Electron');
856
+ console.log(' daemon stop Stop the daemon');
857
+ console.log(' daemon status Check daemon status');
858
+ }
859
+ }
860
+ async function cmdDaemonStart(args) {
861
+ const electronPort = Number(args.electronPort) || 9222;
862
+ // Check if already running
863
+ const isRunning = await daemonCheck();
864
+ if (isRunning) {
865
+ console.log('Daemon is already running. Use "daemon stop" first.');
866
+ return;
867
+ }
868
+ console.log(`Starting daemon with electron-port ${electronPort}...`);
869
+ // Spawn daemon process
870
+ daemonProcess = spawn('node', ['dist/daemon.js', '--port', String(DAEMON_PORT), '--electron-port', String(electronPort)], {
871
+ detached: true,
872
+ stdio: 'ignore',
873
+ });
874
+ daemonProcess.unref();
875
+ // Wait for daemon to start
876
+ let attempts = 0;
877
+ const maxAttempts = 20;
878
+ while (attempts < maxAttempts) {
879
+ await new Promise((resolve) => setTimeout(resolve, 100));
880
+ const running = await daemonCheck();
881
+ if (running) {
882
+ console.log('Daemon started successfully.');
883
+ // Auto-connect to Electron
884
+ try {
885
+ await daemonRequest('/connect', 'POST', {});
886
+ console.log('Connected to Electron.');
887
+ }
888
+ catch (err) {
889
+ console.log('Daemon started but could not connect to Electron:', err.message);
890
+ }
891
+ return;
892
+ }
893
+ attempts++;
894
+ }
895
+ console.error('Failed to start daemon: timeout waiting for it to become ready');
896
+ }
897
+ async function cmdDaemonStop() {
898
+ const isRunning = await daemonCheck();
899
+ if (!isRunning) {
900
+ console.log('Daemon is not running.');
901
+ return;
902
+ }
903
+ console.log('Stopping daemon...');
904
+ try {
905
+ await daemonRequest('/', 'DELETE', {});
906
+ // Wait for daemon to stop
907
+ await new Promise((resolve) => setTimeout(resolve, 500));
908
+ console.log('Daemon stopped.');
909
+ }
910
+ catch (err) {
911
+ console.error('Error stopping daemon:', err.message);
912
+ }
913
+ }
914
+ async function cmdDaemonStatus() {
915
+ const status = await daemonStatus();
916
+ if (status.running) {
917
+ console.log('Daemon: Running');
918
+ console.log(` Connected to Electron: ${status.connected ? 'Yes' : 'No'}`);
919
+ if (status.electronPort) {
920
+ console.log(` Electron port: ${status.electronPort}`);
921
+ }
922
+ }
923
+ else {
924
+ console.log('Daemon: Not running');
925
+ console.log('Use "daemon start --electron-port <port>" to start.');
926
+ }
927
+ }
928
+ // Main
929
+ const argv = process.argv;
930
+ runCommand(parseArgs(argv)).catch((err) => {
931
+ console.error('Error:', err.message);
932
+ process.exit(1);
933
+ });