drissionpage-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,1372 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DrissionPage CLI - Command-line interface for browser automation with DrissionPage.
4
+
5
+ Mirrors the architecture of playwright-cli but uses DrissionPage as the backend.
6
+ Designed for token-efficient browser automation by coding agents.
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import os
12
+ import signal
13
+ import sys
14
+ import time
15
+ import traceback
16
+ from pathlib import Path
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ # Session storage directory
21
+ CLI_DIR = Path(os.environ.get("DRISSIONPAGE_CLI_DIR", ".drissionpage-cli"))
22
+ SESSIONS_FILE = CLI_DIR / "sessions.json"
23
+
24
+
25
+ def ensure_cli_dir():
26
+ CLI_DIR.mkdir(parents=True, exist_ok=True)
27
+
28
+
29
+ def _load_sessions():
30
+ """Load session registry from disk."""
31
+ if SESSIONS_FILE.exists():
32
+ try:
33
+ return json.loads(SESSIONS_FILE.read_text())
34
+ except (json.JSONDecodeError, OSError):
35
+ pass
36
+ return {}
37
+
38
+
39
+ def _save_sessions(sessions):
40
+ """Persist session registry to disk."""
41
+ ensure_cli_dir()
42
+ SESSIONS_FILE.write_text(json.dumps(sessions, indent=2))
43
+
44
+
45
+ def _get_session_name(args):
46
+ """Resolve the session name from args or env."""
47
+ return getattr(args, "session", None) or os.environ.get(
48
+ "DRISSIONPAGE_CLI_SESSION", "default"
49
+ )
50
+
51
+
52
+ def _kill_session(sessions, session_name):
53
+ """Try to kill a stale session's browser process."""
54
+ info = sessions.get(session_name)
55
+ if not info:
56
+ return
57
+ pid = info.get("pid")
58
+ if pid:
59
+ try:
60
+ os.kill(pid, signal.SIGTERM)
61
+ time.sleep(0.5)
62
+ except (ProcessLookupError, PermissionError):
63
+ pass
64
+ del sessions[session_name]
65
+ _save_sessions(sessions)
66
+
67
+
68
+ def _get_page(session_name, create=False, options=None):
69
+ """Get or create a ChromiumPage for the given session.
70
+
71
+ DrissionPage manages browser instances via CDP. We track ports per session
72
+ so that multiple named sessions can coexist.
73
+ """
74
+ from DrissionPage import ChromiumPage, ChromiumOptions
75
+
76
+ sessions = _load_sessions()
77
+ info = sessions.get(session_name)
78
+
79
+ if info:
80
+ # Try to reconnect to existing session
81
+ co = ChromiumOptions()
82
+ co.set_address(info["address"])
83
+ try:
84
+ page = ChromiumPage(addr_or_opts=co)
85
+ if not create:
86
+ return page
87
+ # create=True but session is alive — close old one first
88
+ try:
89
+ page.quit()
90
+ except Exception:
91
+ pass
92
+ _kill_session(sessions, session_name)
93
+ sessions = _load_sessions()
94
+ except Exception:
95
+ # Browser is dead — clean up stale entry
96
+ _kill_session(sessions, session_name)
97
+ sessions = _load_sessions()
98
+ if not create:
99
+ raise RuntimeError(
100
+ f"Session '{session_name}' is no longer running. "
101
+ f"Use 'open' to start a new one."
102
+ )
103
+
104
+ if not create:
105
+ raise RuntimeError(
106
+ f"No active session '{session_name}'. Use 'open' to start one."
107
+ )
108
+
109
+ # Create new session
110
+ co = ChromiumOptions()
111
+ co.auto_port() # pick a free port to avoid conflicts with stale browsers
112
+
113
+ if options:
114
+ if options.get("headless") is not None:
115
+ co.headless(options["headless"])
116
+ if options.get("browser_path"):
117
+ co.set_browser_path(options["browser_path"])
118
+ if options.get("user_data_path"):
119
+ co.set_user_data_path(options["user_data_path"])
120
+ if options.get("proxy"):
121
+ co.set_proxy(options["proxy"])
122
+ if options.get("port"):
123
+ co.set_local_port(options["port"])
124
+ if options.get("user_agent"):
125
+ co.set_user_agent(options["user_agent"])
126
+ if options.get("args"):
127
+ for arg in options["args"]:
128
+ co.set_argument(arg)
129
+
130
+ page = ChromiumPage(addr_or_opts=co)
131
+
132
+ # Record session info
133
+ sessions[session_name] = {
134
+ "address": page.address,
135
+ "pid": page.process_id,
136
+ "started": time.time(),
137
+ }
138
+ _save_sessions(sessions)
139
+ return page
140
+
141
+
142
+ def _format_snapshot(page, element=None):
143
+ """Produce a text snapshot of the current page state, similar to playwright-cli's snapshot."""
144
+ lines = []
145
+ lines.append("### Page")
146
+ lines.append(f"- Page URL: {page.url}")
147
+ lines.append(f"- Page Title: {page.title}")
148
+ lines.append("")
149
+
150
+ if element:
151
+ lines.append("### Element Snapshot")
152
+ lines.append(f"- Tag: {element.tag}")
153
+ lines.append(f"- Text: {element.text[:200]}")
154
+ if element.attrs:
155
+ for k, v in list(element.attrs.items())[:10]:
156
+ lines.append(f" @{k}={v}")
157
+ else:
158
+ lines.append("### Snapshot")
159
+ # Save snapshot to file
160
+ timestamp = time.strftime("%Y-%m-%dT%H-%M-%S")
161
+ snap_dir = CLI_DIR / "snapshots"
162
+ snap_dir.mkdir(parents=True, exist_ok=True)
163
+ snap_file = snap_dir / f"page-{timestamp}.html"
164
+ try:
165
+ snap_file.write_text(page.html)
166
+ lines.append(f"[Snapshot]({snap_file})")
167
+ except Exception:
168
+ lines.append("[Snapshot could not be saved]")
169
+
170
+ return "\n".join(lines)
171
+
172
+
173
+ def _find_element(page, ref):
174
+ """Find an element by reference. Supports CSS, XPath, DrissionPage locator syntax."""
175
+ if not ref:
176
+ return None
177
+ # DrissionPage native locator syntax
178
+ return page.ele(ref)
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Command handlers
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ def cmd_open(args):
187
+ """Open a browser, optionally navigate to a URL."""
188
+ session = _get_session_name(args)
189
+ options = {
190
+ "headless": not getattr(args, "headed", False),
191
+ "user_data_path": getattr(args, "profile", None),
192
+ "port": getattr(args, "port", None),
193
+ }
194
+ page = _get_page(session, create=True, options=options)
195
+
196
+ url = getattr(args, "url", None)
197
+ if url:
198
+ page.get(url)
199
+
200
+ print(_format_snapshot(page))
201
+
202
+
203
+ def cmd_goto(args):
204
+ """Navigate to a URL."""
205
+ session = _get_session_name(args)
206
+ page = _get_page(session)
207
+ page.get(args.url)
208
+ print(_format_snapshot(page))
209
+
210
+
211
+ def cmd_click(args):
212
+ """Click an element."""
213
+ session = _get_session_name(args)
214
+ page = _get_page(session)
215
+ ele = _find_element(page, args.ref)
216
+ if not ele:
217
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
218
+ sys.exit(1)
219
+ ele.click()
220
+ print(_format_snapshot(page))
221
+
222
+
223
+ def cmd_dblclick(args):
224
+ """Double-click an element."""
225
+ session = _get_session_name(args)
226
+ page = _get_page(session)
227
+ ele = _find_element(page, args.ref)
228
+ if not ele:
229
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
230
+ sys.exit(1)
231
+ ele.click(times=2)
232
+ print(_format_snapshot(page))
233
+
234
+
235
+ def cmd_right_click(args):
236
+ """Right-click an element."""
237
+ session = _get_session_name(args)
238
+ page = _get_page(session)
239
+ ele = _find_element(page, args.ref)
240
+ if not ele:
241
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
242
+ sys.exit(1)
243
+ ele.click(button="right")
244
+ print(_format_snapshot(page))
245
+
246
+
247
+ def cmd_type(args):
248
+ """Type text into the focused or specified element."""
249
+ session = _get_session_name(args)
250
+ page = _get_page(session)
251
+ if hasattr(args, "ref") and args.ref:
252
+ ele = _find_element(page, args.ref)
253
+ if not ele:
254
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
255
+ sys.exit(1)
256
+ ele.input(args.text)
257
+ else:
258
+ # Type into active element
259
+ page.actions.type(args.text)
260
+ print(_format_snapshot(page))
261
+
262
+
263
+ def cmd_fill(args):
264
+ """Clear and fill text into an element."""
265
+ session = _get_session_name(args)
266
+ page = _get_page(session)
267
+ ele = _find_element(page, args.ref)
268
+ if not ele:
269
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
270
+ sys.exit(1)
271
+ ele.clear()
272
+ ele.input(args.text)
273
+ if getattr(args, "submit", False):
274
+ ele.input("\n")
275
+ print(_format_snapshot(page))
276
+
277
+
278
+ def cmd_hover(args):
279
+ """Hover over an element."""
280
+ session = _get_session_name(args)
281
+ page = _get_page(session)
282
+ ele = _find_element(page, args.ref)
283
+ if not ele:
284
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
285
+ sys.exit(1)
286
+ ele.hover()
287
+ print(_format_snapshot(page))
288
+
289
+
290
+ def cmd_drag(args):
291
+ """Drag one element to another."""
292
+ session = _get_session_name(args)
293
+ page = _get_page(session)
294
+ src = _find_element(page, args.start_ref)
295
+ dst = _find_element(page, args.end_ref)
296
+ if not src or not dst:
297
+ print("Error: source or destination element not found", file=sys.stderr)
298
+ sys.exit(1)
299
+ src.drag_to(dst)
300
+ print(_format_snapshot(page))
301
+
302
+
303
+ def cmd_select(args):
304
+ """Select an option in a dropdown."""
305
+ session = _get_session_name(args)
306
+ page = _get_page(session)
307
+ ele = _find_element(page, args.ref)
308
+ if not ele:
309
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
310
+ sys.exit(1)
311
+ ele.select.by_text(args.value)
312
+ print(_format_snapshot(page))
313
+
314
+
315
+ def cmd_check(args):
316
+ """Check a checkbox."""
317
+ session = _get_session_name(args)
318
+ page = _get_page(session)
319
+ ele = _find_element(page, args.ref)
320
+ if not ele:
321
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
322
+ sys.exit(1)
323
+ if not ele.states.is_checked:
324
+ ele.click()
325
+ print(_format_snapshot(page))
326
+
327
+
328
+ def cmd_uncheck(args):
329
+ """Uncheck a checkbox."""
330
+ session = _get_session_name(args)
331
+ page = _get_page(session)
332
+ ele = _find_element(page, args.ref)
333
+ if not ele:
334
+ print(f"Error: element not found: {args.ref}", file=sys.stderr)
335
+ sys.exit(1)
336
+ if ele.states.is_checked:
337
+ ele.click()
338
+ print(_format_snapshot(page))
339
+
340
+
341
+ def cmd_upload(args):
342
+ """Upload a file."""
343
+ session = _get_session_name(args)
344
+ page = _get_page(session)
345
+ ele = _find_element(page, args.ref)
346
+ if not ele:
347
+ print(f"Error: file input element not found: {args.ref}", file=sys.stderr)
348
+ sys.exit(1)
349
+ ele.input(args.file)
350
+ print(_format_snapshot(page))
351
+
352
+
353
+ def cmd_snapshot(args):
354
+ """Take a page or element snapshot."""
355
+ session = _get_session_name(args)
356
+ page = _get_page(session)
357
+ ref = getattr(args, "ref", None)
358
+ if ref:
359
+ ele = _find_element(page, ref)
360
+ if not ele:
361
+ print(f"Error: element not found: {ref}", file=sys.stderr)
362
+ sys.exit(1)
363
+ print(_format_snapshot(page, element=ele))
364
+ else:
365
+ filename = getattr(args, "filename", None)
366
+ if filename:
367
+ Path(filename).write_text(page.html)
368
+ print(f"Snapshot saved to {filename}")
369
+ else:
370
+ print(_format_snapshot(page))
371
+
372
+
373
+ def cmd_eval(args):
374
+ """Evaluate JavaScript on the page or an element."""
375
+ session = _get_session_name(args)
376
+ page = _get_page(session)
377
+ ref = getattr(args, "ref", None)
378
+ if ref:
379
+ ele = _find_element(page, ref)
380
+ if not ele:
381
+ print(f"Error: element not found: {ref}", file=sys.stderr)
382
+ sys.exit(1)
383
+ result = ele.run_js(args.expression)
384
+ else:
385
+ result = page.run_js(args.expression)
386
+ if result is not None:
387
+ if isinstance(result, (dict, list)):
388
+ print(json.dumps(result, indent=2, ensure_ascii=False))
389
+ else:
390
+ print(result)
391
+
392
+
393
+ def cmd_run_code(args):
394
+ """Run a DrissionPage Python code snippet."""
395
+ session = _get_session_name(args)
396
+ page = _get_page(session)
397
+
398
+ if getattr(args, "filename", None):
399
+ code = Path(args.filename).read_text()
400
+ else:
401
+ code = args.code
402
+
403
+ # Execute code with page in scope
404
+ local_vars = {"page": page, "result": None}
405
+ exec(code, {"__builtins__": __builtins__}, local_vars)
406
+ if local_vars.get("result") is not None:
407
+ result = local_vars["result"]
408
+ if isinstance(result, (dict, list)):
409
+ print(json.dumps(result, indent=2, ensure_ascii=False))
410
+ else:
411
+ print(result)
412
+ else:
413
+ print(_format_snapshot(page))
414
+
415
+
416
+ def cmd_screenshot(args):
417
+ """Take a screenshot."""
418
+ session = _get_session_name(args)
419
+ page = _get_page(session)
420
+
421
+ filename = getattr(args, "filename", None)
422
+ if not filename:
423
+ timestamp = time.strftime("%Y-%m-%dT%H-%M-%S")
424
+ ensure_cli_dir()
425
+ filename = str(CLI_DIR / f"screenshot-{timestamp}.png")
426
+
427
+ ref = getattr(args, "ref", None)
428
+ if ref:
429
+ ele = _find_element(page, ref)
430
+ if not ele:
431
+ print(f"Error: element not found: {ref}", file=sys.stderr)
432
+ sys.exit(1)
433
+ ele.get_screenshot(path=filename)
434
+ else:
435
+ page.get_screenshot(path=filename)
436
+
437
+ print(f"Screenshot saved to {filename}")
438
+
439
+
440
+ def cmd_pdf(args):
441
+ """Save page as PDF."""
442
+ session = _get_session_name(args)
443
+ page = _get_page(session)
444
+
445
+ filename = getattr(args, "filename", None)
446
+ if not filename:
447
+ timestamp = time.strftime("%Y-%m-%dT%H-%M-%S")
448
+ ensure_cli_dir()
449
+ filename = str(CLI_DIR / f"page-{timestamp}.pdf")
450
+
451
+ page.save(path=filename, as_pdf=True)
452
+ print(f"PDF saved to {filename}")
453
+
454
+
455
+ # --- Navigation ---
456
+
457
+
458
+ def cmd_go_back(args):
459
+ """Go back."""
460
+ session = _get_session_name(args)
461
+ page = _get_page(session)
462
+ page.back()
463
+ print(_format_snapshot(page))
464
+
465
+
466
+ def cmd_go_forward(args):
467
+ """Go forward."""
468
+ session = _get_session_name(args)
469
+ page = _get_page(session)
470
+ page.forward()
471
+ print(_format_snapshot(page))
472
+
473
+
474
+ def cmd_reload(args):
475
+ """Reload the page."""
476
+ session = _get_session_name(args)
477
+ page = _get_page(session)
478
+ page.refresh()
479
+ print(_format_snapshot(page))
480
+
481
+
482
+ # --- Keyboard ---
483
+
484
+
485
+ def cmd_press(args):
486
+ """Press a key."""
487
+ session = _get_session_name(args)
488
+ page = _get_page(session)
489
+ from DrissionPage.common import Keys
490
+
491
+ key_map = {
492
+ "enter": Keys.ENTER,
493
+ "tab": Keys.TAB,
494
+ "escape": Keys.ESCAPE,
495
+ "backspace": Keys.BACKSPACE,
496
+ "delete": Keys.DELETE,
497
+ "arrowup": Keys.UP,
498
+ "arrowdown": Keys.DOWN,
499
+ "arrowleft": Keys.LEFT,
500
+ "arrowright": Keys.RIGHT,
501
+ "home": Keys.HOME,
502
+ "end": Keys.END,
503
+ "pageup": Keys.PAGE_UP,
504
+ "pagedown": Keys.PAGE_DOWN,
505
+ "space": Keys.SPACE,
506
+ "f1": Keys.F1,
507
+ "f2": Keys.F2,
508
+ "f3": Keys.F3,
509
+ "f4": Keys.F4,
510
+ "f5": Keys.F5,
511
+ "f6": Keys.F6,
512
+ "f7": Keys.F7,
513
+ "f8": Keys.F8,
514
+ "f9": Keys.F9,
515
+ "f10": Keys.F10,
516
+ "f11": Keys.F11,
517
+ "f12": Keys.F12,
518
+ }
519
+ key_name = args.key.lower()
520
+ key = key_map.get(key_name, args.key)
521
+ page.actions.key_down(key).key_up(key)
522
+ print(_format_snapshot(page))
523
+
524
+
525
+ # --- Mouse ---
526
+
527
+
528
+ def cmd_mousemove(args):
529
+ """Move mouse to coordinates."""
530
+ session = _get_session_name(args)
531
+ page = _get_page(session)
532
+ page.actions.move_to(args.x, args.y)
533
+ print("Mouse moved to", args.x, args.y)
534
+
535
+
536
+ def cmd_mousedown(args):
537
+ """Press mouse button down."""
538
+ session = _get_session_name(args)
539
+ page = _get_page(session)
540
+ button = getattr(args, "button", "left") or "left"
541
+ page.actions.hold(button)
542
+ print(f"Mouse {button} button down")
543
+
544
+
545
+ def cmd_mouseup(args):
546
+ """Release mouse button."""
547
+ session = _get_session_name(args)
548
+ page = _get_page(session)
549
+ button = getattr(args, "button", "left") or "left"
550
+ page.actions.release(button)
551
+ print(f"Mouse {button} button up")
552
+
553
+
554
+ def cmd_scroll(args):
555
+ """Scroll the page."""
556
+ session = _get_session_name(args)
557
+ page = _get_page(session)
558
+ page.scroll.down(args.dy)
559
+ print(f"Scrolled by ({args.dx}, {args.dy})")
560
+
561
+
562
+ # --- Tabs ---
563
+
564
+
565
+ def cmd_tab_list(args):
566
+ """List all tabs."""
567
+ session = _get_session_name(args)
568
+ page = _get_page(session)
569
+ tab_ids = page.tab_ids
570
+ print(f"Tabs ({len(tab_ids)}):")
571
+ for i, tid in enumerate(tab_ids):
572
+ tab = page.get_tab(tid)
573
+ marker = " *" if tid == page.tab_id else ""
574
+ print(f" [{i}] {tab.title} - {tab.url}{marker}")
575
+
576
+
577
+ def cmd_tab_new(args):
578
+ """Open a new tab."""
579
+ session = _get_session_name(args)
580
+ page = _get_page(session)
581
+ url = getattr(args, "url", None) or ""
582
+ tab = page.new_tab(url=url if url else None)
583
+ print(f"New tab opened: {tab.url if hasattr(tab, 'url') else url}")
584
+
585
+
586
+ def cmd_tab_close(args):
587
+ """Close a tab."""
588
+ session = _get_session_name(args)
589
+ page = _get_page(session)
590
+ index = getattr(args, "index", None)
591
+ if index is not None:
592
+ tab_ids = page.tab_ids
593
+ if 0 <= index < len(tab_ids):
594
+ page.close_tabs(tab_ids[index])
595
+ else:
596
+ print(f"Error: tab index {index} out of range", file=sys.stderr)
597
+ sys.exit(1)
598
+ else:
599
+ page.close_tabs(page.tab_id)
600
+ print("Tab closed")
601
+
602
+
603
+ def cmd_tab_select(args):
604
+ """Select a tab by index."""
605
+ session = _get_session_name(args)
606
+ page = _get_page(session)
607
+ tab_ids = page.tab_ids
608
+ if 0 <= args.index < len(tab_ids):
609
+ tab = page.get_tab(tab_ids[args.index])
610
+ print(_format_snapshot(tab))
611
+ else:
612
+ print(f"Error: tab index {args.index} out of range", file=sys.stderr)
613
+ sys.exit(1)
614
+
615
+
616
+ # --- Resize ---
617
+
618
+
619
+ def cmd_resize(args):
620
+ """Resize the browser window."""
621
+ session = _get_session_name(args)
622
+ page = _get_page(session)
623
+ page.set.window.size(args.width, args.height)
624
+ print(f"Window resized to {args.width}x{args.height}")
625
+
626
+
627
+ # --- Dialog ---
628
+
629
+
630
+ def cmd_dialog_accept(args):
631
+ """Accept a dialog."""
632
+ session = _get_session_name(args)
633
+ page = _get_page(session)
634
+ text = getattr(args, "text", None)
635
+ page.handle_alert(accept=True, send=text)
636
+ print("Dialog accepted")
637
+
638
+
639
+ def cmd_dialog_dismiss(args):
640
+ """Dismiss a dialog."""
641
+ session = _get_session_name(args)
642
+ page = _get_page(session)
643
+ page.handle_alert(accept=False)
644
+ print("Dialog dismissed")
645
+
646
+
647
+ # --- Cookies ---
648
+
649
+
650
+ def cmd_cookie_list(args):
651
+ """List cookies."""
652
+ session = _get_session_name(args)
653
+ page = _get_page(session)
654
+ cookies = page.cookies(as_dict=False, all_info=True)
655
+ domain = getattr(args, "domain", None)
656
+ if domain:
657
+ cookies = [c for c in cookies if domain in c.get("domain", "")]
658
+ for c in cookies:
659
+ print(json.dumps(c, indent=2, ensure_ascii=False))
660
+
661
+
662
+ def cmd_cookie_get(args):
663
+ """Get a cookie by name."""
664
+ session = _get_session_name(args)
665
+ page = _get_page(session)
666
+ cookies = page.cookies(as_dict=False, all_info=True)
667
+ for c in cookies:
668
+ if c.get("name") == args.name:
669
+ print(json.dumps(c, indent=2, ensure_ascii=False))
670
+ return
671
+ print(f"Cookie '{args.name}' not found")
672
+
673
+
674
+ def cmd_cookie_set(args):
675
+ """Set a cookie."""
676
+ session = _get_session_name(args)
677
+ page = _get_page(session)
678
+ cookie = {"name": args.name, "value": args.value}
679
+ if getattr(args, "domain", None):
680
+ cookie["domain"] = args.domain
681
+ if getattr(args, "path", None):
682
+ cookie["path"] = args.path
683
+ if getattr(args, "secure", False):
684
+ cookie["secure"] = True
685
+ if getattr(args, "httpOnly", False):
686
+ cookie["httpOnly"] = True
687
+ page.set.cookies(cookie)
688
+ print(f"Cookie '{args.name}' set")
689
+
690
+
691
+ def cmd_cookie_delete(args):
692
+ """Delete a cookie by name."""
693
+ session = _get_session_name(args)
694
+ page = _get_page(session)
695
+ page.set.cookies.remove(args.name)
696
+ print(f"Cookie '{args.name}' deleted")
697
+
698
+
699
+ def cmd_cookie_clear(args):
700
+ """Clear all cookies."""
701
+ session = _get_session_name(args)
702
+ page = _get_page(session)
703
+ page.set.cookies.clear()
704
+ print("All cookies cleared")
705
+
706
+
707
+ # --- LocalStorage ---
708
+
709
+
710
+ def cmd_localstorage_list(args):
711
+ """List localStorage entries."""
712
+ session = _get_session_name(args)
713
+ page = _get_page(session)
714
+ result = page.run_js(
715
+ "return JSON.stringify(Object.entries(localStorage))"
716
+ )
717
+ entries = json.loads(result) if result else []
718
+ for key, value in entries:
719
+ print(f" {key}: {value}")
720
+
721
+
722
+ def cmd_localstorage_get(args):
723
+ """Get a localStorage value."""
724
+ session = _get_session_name(args)
725
+ page = _get_page(session)
726
+ result = page.run_js(f"return localStorage.getItem('{args.key}')")
727
+ if result is not None:
728
+ print(result)
729
+ else:
730
+ print(f"Key '{args.key}' not found in localStorage")
731
+
732
+
733
+ def cmd_localstorage_set(args):
734
+ """Set a localStorage value."""
735
+ session = _get_session_name(args)
736
+ page = _get_page(session)
737
+ value_escaped = args.value.replace("'", "\\'")
738
+ page.run_js(f"localStorage.setItem('{args.key}', '{value_escaped}')")
739
+ print(f"localStorage['{args.key}'] = '{args.value}'")
740
+
741
+
742
+ def cmd_localstorage_delete(args):
743
+ """Delete a localStorage entry."""
744
+ session = _get_session_name(args)
745
+ page = _get_page(session)
746
+ page.run_js(f"localStorage.removeItem('{args.key}')")
747
+ print(f"localStorage['{args.key}'] deleted")
748
+
749
+
750
+ def cmd_localstorage_clear(args):
751
+ """Clear all localStorage."""
752
+ session = _get_session_name(args)
753
+ page = _get_page(session)
754
+ page.run_js("localStorage.clear()")
755
+ print("localStorage cleared")
756
+
757
+
758
+ # --- SessionStorage ---
759
+
760
+
761
+ def cmd_sessionstorage_list(args):
762
+ """List sessionStorage entries."""
763
+ session = _get_session_name(args)
764
+ page = _get_page(session)
765
+ result = page.run_js(
766
+ "return JSON.stringify(Object.entries(sessionStorage))"
767
+ )
768
+ entries = json.loads(result) if result else []
769
+ for key, value in entries:
770
+ print(f" {key}: {value}")
771
+
772
+
773
+ def cmd_sessionstorage_get(args):
774
+ """Get a sessionStorage value."""
775
+ session = _get_session_name(args)
776
+ page = _get_page(session)
777
+ result = page.run_js(f"return sessionStorage.getItem('{args.key}')")
778
+ if result is not None:
779
+ print(result)
780
+ else:
781
+ print(f"Key '{args.key}' not found in sessionStorage")
782
+
783
+
784
+ def cmd_sessionstorage_set(args):
785
+ """Set a sessionStorage value."""
786
+ session = _get_session_name(args)
787
+ page = _get_page(session)
788
+ value_escaped = args.value.replace("'", "\\'")
789
+ page.run_js(f"sessionStorage.setItem('{args.key}', '{value_escaped}')")
790
+ print(f"sessionStorage['{args.key}'] = '{args.value}'")
791
+
792
+
793
+ def cmd_sessionstorage_delete(args):
794
+ """Delete a sessionStorage entry."""
795
+ session = _get_session_name(args)
796
+ page = _get_page(session)
797
+ page.run_js(f"sessionStorage.removeItem('{args.key}')")
798
+ print(f"sessionStorage['{args.key}'] deleted")
799
+
800
+
801
+ def cmd_sessionstorage_clear(args):
802
+ """Clear all sessionStorage."""
803
+ session = _get_session_name(args)
804
+ page = _get_page(session)
805
+ page.run_js("sessionStorage.clear()")
806
+ print("sessionStorage cleared")
807
+
808
+
809
+ # --- Network ---
810
+
811
+
812
+ def cmd_console(args):
813
+ """List console messages."""
814
+ session = _get_session_name(args)
815
+ page = _get_page(session)
816
+ result = page.run_js("""
817
+ return JSON.stringify(
818
+ (window.__dp_console_logs || []).map(e => ({
819
+ type: e.type, text: e.text
820
+ }))
821
+ )
822
+ """)
823
+ if result:
824
+ logs = json.loads(result)
825
+ min_level = getattr(args, "level", None)
826
+ levels = ["error", "warning", "info", "debug"]
827
+ if min_level and min_level in levels:
828
+ allowed = set(levels[: levels.index(min_level) + 1])
829
+ logs = [l for l in logs if l.get("type", "info") in allowed]
830
+ for log in logs:
831
+ print(f"[{log.get('type', 'info')}] {log.get('text', '')}")
832
+ else:
833
+ print("No console messages captured. Use run-code to inject console capture first.")
834
+
835
+
836
+ def cmd_network(args):
837
+ """Show network requests (requires listener to be started)."""
838
+ session = _get_session_name(args)
839
+ page = _get_page(session)
840
+ print("Network monitoring requires starting a listener first.")
841
+ print("Use: drissionpage-cli run-code \"page.listen.start()\"")
842
+ print("Then perform actions and use: drissionpage-cli run-code \"...")
843
+
844
+
845
+ # --- Session management ---
846
+
847
+
848
+ def cmd_list(args):
849
+ """List all sessions."""
850
+ sessions = _load_sessions()
851
+ if not sessions:
852
+ print("No active sessions")
853
+ return
854
+ print(f"Sessions ({len(sessions)}):")
855
+ for name, info in sessions.items():
856
+ started = time.strftime(
857
+ "%Y-%m-%d %H:%M:%S", time.localtime(info.get("started", 0))
858
+ )
859
+ print(f" {name}: address={info.get('address')} pid={info.get('pid')} started={started}")
860
+
861
+
862
+ def cmd_close(args):
863
+ """Close the browser for a session."""
864
+ session = _get_session_name(args)
865
+ sessions = _load_sessions()
866
+
867
+ if session not in sessions:
868
+ print(f"Session '{session}' not found")
869
+ return
870
+
871
+ try:
872
+ page = _get_page(session)
873
+ page.quit()
874
+ except Exception:
875
+ pass
876
+
877
+ del sessions[session]
878
+ _save_sessions(sessions)
879
+ print(f"Session '{session}' closed")
880
+
881
+
882
+ def cmd_close_all(args):
883
+ """Close all browser sessions."""
884
+ sessions = _load_sessions()
885
+ for name in list(sessions.keys()):
886
+ try:
887
+ from DrissionPage import ChromiumPage, ChromiumOptions
888
+
889
+ co = ChromiumOptions()
890
+ co.set_address(sessions[name]["address"])
891
+ page = ChromiumPage(addr_or_opts=co)
892
+ page.quit()
893
+ except Exception:
894
+ pass
895
+ _save_sessions({})
896
+ print("All sessions closed")
897
+
898
+
899
+ def cmd_kill_all(args):
900
+ """Kill all browser processes."""
901
+ import subprocess
902
+
903
+ sessions = _load_sessions()
904
+ for name, info in sessions.items():
905
+ pid = info.get("pid")
906
+ if pid:
907
+ try:
908
+ os.kill(pid, signal.SIGKILL)
909
+ except (ProcessLookupError, PermissionError):
910
+ pass
911
+ _save_sessions({})
912
+ # Also kill any orphaned chrome/chromium
913
+ try:
914
+ subprocess.run(
915
+ ["pkill", "-f", "chrome.*--remote-debugging-port"],
916
+ capture_output=True,
917
+ )
918
+ except Exception:
919
+ pass
920
+ print("All browser processes killed")
921
+
922
+
923
+ def cmd_delete_data(args):
924
+ """Delete user data for a session."""
925
+ session = _get_session_name(args)
926
+ sessions = _load_sessions()
927
+ info = sessions.get(session, {})
928
+
929
+ # Try to close the browser first
930
+ try:
931
+ page = _get_page(session)
932
+ user_data = page.user_data_path
933
+ page.quit()
934
+ except Exception:
935
+ user_data = None
936
+
937
+ if session in sessions:
938
+ del sessions[session]
939
+ _save_sessions(sessions)
940
+
941
+ if user_data and Path(user_data).exists():
942
+ import shutil
943
+
944
+ shutil.rmtree(user_data, ignore_errors=True)
945
+ print(f"Deleted user data for '{session}' at {user_data}")
946
+ else:
947
+ print(f"Deleted session '{session}' (no user data directory found)")
948
+
949
+
950
+ # --- State save/load ---
951
+
952
+
953
+ def cmd_state_save(args):
954
+ """Save browser state (cookies + localStorage) to JSON."""
955
+ session = _get_session_name(args)
956
+ page = _get_page(session)
957
+
958
+ filename = getattr(args, "filename", None)
959
+ if not filename:
960
+ timestamp = time.strftime("%Y-%m-%dT%H-%M-%S")
961
+ ensure_cli_dir()
962
+ filename = str(CLI_DIR / f"state-{timestamp}.json")
963
+
964
+ cookies = page.cookies(as_dict=False, all_info=True)
965
+ local_storage = page.run_js(
966
+ "return JSON.stringify(Object.entries(localStorage))"
967
+ )
968
+ session_storage = page.run_js(
969
+ "return JSON.stringify(Object.entries(sessionStorage))"
970
+ )
971
+
972
+ state = {
973
+ "url": page.url,
974
+ "cookies": cookies if isinstance(cookies, list) else [],
975
+ "localStorage": json.loads(local_storage) if local_storage else [],
976
+ "sessionStorage": json.loads(session_storage) if session_storage else [],
977
+ }
978
+
979
+ Path(filename).write_text(json.dumps(state, indent=2, ensure_ascii=False))
980
+ print(f"State saved to {filename}")
981
+
982
+
983
+ def cmd_state_load(args):
984
+ """Load browser state from JSON."""
985
+ session = _get_session_name(args)
986
+ page = _get_page(session)
987
+
988
+ state = json.loads(Path(args.filename).read_text())
989
+
990
+ # Restore cookies
991
+ for cookie in state.get("cookies", []):
992
+ try:
993
+ page.set.cookies(cookie)
994
+ except Exception:
995
+ pass
996
+
997
+ # Restore localStorage
998
+ for key, value in state.get("localStorage", []):
999
+ value_escaped = value.replace("\\", "\\\\").replace("'", "\\'")
1000
+ page.run_js(f"localStorage.setItem('{key}', '{value_escaped}')")
1001
+
1002
+ # Restore sessionStorage
1003
+ for key, value in state.get("sessionStorage", []):
1004
+ value_escaped = value.replace("\\", "\\\\").replace("'", "\\'")
1005
+ page.run_js(f"sessionStorage.setItem('{key}', '{value_escaped}')")
1006
+
1007
+ print(f"State loaded from {args.filename}")
1008
+
1009
+ # Navigate to saved URL if present
1010
+ url = state.get("url")
1011
+ if url and url != page.url:
1012
+ page.get(url)
1013
+ print(_format_snapshot(page))
1014
+
1015
+
1016
+ # --- Install skills ---
1017
+
1018
+
1019
+ def cmd_install(args):
1020
+ """Install skills for Claude Code or other agents."""
1021
+ if not getattr(args, "skills", False):
1022
+ print("Use --skills to install Claude Code skills")
1023
+ return
1024
+
1025
+ # Install to .claude/skills/drissionpage-cli
1026
+ target = Path(".claude") / "skills" / "drissionpage-cli"
1027
+ source = Path(__file__).parent / "skills" / "drissionpage-cli"
1028
+
1029
+ if not source.exists():
1030
+ print(f"Error: skills not found at {source}", file=sys.stderr)
1031
+ sys.exit(1)
1032
+
1033
+ import shutil
1034
+
1035
+ target.parent.mkdir(parents=True, exist_ok=True)
1036
+ if target.exists():
1037
+ shutil.rmtree(target)
1038
+ shutil.copytree(source, target)
1039
+ print(f"Skills installed to {target}")
1040
+
1041
+
1042
+ # --- Version ---
1043
+
1044
+
1045
+ def cmd_version(args):
1046
+ """Print version."""
1047
+ print(f"drissionpage-cli v{__version__}")
1048
+
1049
+
1050
+ # ---------------------------------------------------------------------------
1051
+ # Argument parser
1052
+ # ---------------------------------------------------------------------------
1053
+
1054
+
1055
+ def build_parser():
1056
+ parser = argparse.ArgumentParser(
1057
+ prog="drissionpage-cli",
1058
+ description="DrissionPage CLI - Browser automation from the command line",
1059
+ )
1060
+ parser.add_argument("--version", action="store_true", help="Print version")
1061
+ parser.add_argument(
1062
+ "-s", "--session", default=None, help="Named browser session"
1063
+ )
1064
+
1065
+ subparsers = parser.add_subparsers(dest="command")
1066
+
1067
+ # open
1068
+ p = subparsers.add_parser("open", help="Open browser, optionally navigate to URL")
1069
+ p.add_argument("url", nargs="?", help="URL to navigate to")
1070
+ p.add_argument("--headed", action="store_true", help="Run in headed mode")
1071
+ p.add_argument("--profile", help="User data directory path")
1072
+ p.add_argument("--port", type=int, help="CDP debugging port")
1073
+ p.set_defaults(func=cmd_open)
1074
+
1075
+ # goto
1076
+ p = subparsers.add_parser("goto", help="Navigate to a URL")
1077
+ p.add_argument("url", help="URL to navigate to")
1078
+ p.set_defaults(func=cmd_goto)
1079
+
1080
+ # click
1081
+ p = subparsers.add_parser("click", help="Click an element")
1082
+ p.add_argument("ref", help="Element locator")
1083
+ p.set_defaults(func=cmd_click)
1084
+
1085
+ # dblclick
1086
+ p = subparsers.add_parser("dblclick", help="Double-click an element")
1087
+ p.add_argument("ref", help="Element locator")
1088
+ p.set_defaults(func=cmd_dblclick)
1089
+
1090
+ # right-click
1091
+ p = subparsers.add_parser("right-click", help="Right-click an element")
1092
+ p.add_argument("ref", help="Element locator")
1093
+ p.set_defaults(func=cmd_right_click)
1094
+
1095
+ # type
1096
+ p = subparsers.add_parser("type", help="Type text")
1097
+ p.add_argument("text", help="Text to type")
1098
+ p.add_argument("ref", nargs="?", help="Element locator (optional)")
1099
+ p.set_defaults(func=cmd_type)
1100
+
1101
+ # fill
1102
+ p = subparsers.add_parser("fill", help="Clear and fill text into element")
1103
+ p.add_argument("ref", help="Element locator")
1104
+ p.add_argument("text", help="Text to fill")
1105
+ p.add_argument("--submit", action="store_true", help="Press Enter after filling")
1106
+ p.set_defaults(func=cmd_fill)
1107
+
1108
+ # hover
1109
+ p = subparsers.add_parser("hover", help="Hover over element")
1110
+ p.add_argument("ref", help="Element locator")
1111
+ p.set_defaults(func=cmd_hover)
1112
+
1113
+ # drag
1114
+ p = subparsers.add_parser("drag", help="Drag element to another")
1115
+ p.add_argument("start_ref", help="Source element locator")
1116
+ p.add_argument("end_ref", help="Target element locator")
1117
+ p.set_defaults(func=cmd_drag)
1118
+
1119
+ # select
1120
+ p = subparsers.add_parser("select", help="Select dropdown option")
1121
+ p.add_argument("ref", help="Select element locator")
1122
+ p.add_argument("value", help="Option text to select")
1123
+ p.set_defaults(func=cmd_select)
1124
+
1125
+ # check / uncheck
1126
+ p = subparsers.add_parser("check", help="Check a checkbox")
1127
+ p.add_argument("ref", help="Checkbox element locator")
1128
+ p.set_defaults(func=cmd_check)
1129
+
1130
+ p = subparsers.add_parser("uncheck", help="Uncheck a checkbox")
1131
+ p.add_argument("ref", help="Checkbox element locator")
1132
+ p.set_defaults(func=cmd_uncheck)
1133
+
1134
+ # upload
1135
+ p = subparsers.add_parser("upload", help="Upload a file")
1136
+ p.add_argument("ref", help="File input element locator")
1137
+ p.add_argument("file", help="File path to upload")
1138
+ p.set_defaults(func=cmd_upload)
1139
+
1140
+ # snapshot
1141
+ p = subparsers.add_parser("snapshot", help="Take page snapshot")
1142
+ p.add_argument("ref", nargs="?", help="Element locator (optional)")
1143
+ p.add_argument("--filename", help="Save snapshot to file")
1144
+ p.set_defaults(func=cmd_snapshot)
1145
+
1146
+ # eval
1147
+ p = subparsers.add_parser("eval", help="Evaluate JavaScript")
1148
+ p.add_argument("expression", help="JavaScript expression")
1149
+ p.add_argument("ref", nargs="?", help="Element locator (optional)")
1150
+ p.set_defaults(func=cmd_eval)
1151
+
1152
+ # run-code
1153
+ p = subparsers.add_parser("run-code", help="Run DrissionPage Python code")
1154
+ p.add_argument("code", nargs="?", help="Python code to run")
1155
+ p.add_argument("--filename", help="Python file to run")
1156
+ p.set_defaults(func=cmd_run_code)
1157
+
1158
+ # screenshot
1159
+ p = subparsers.add_parser("screenshot", help="Take screenshot")
1160
+ p.add_argument("ref", nargs="?", help="Element locator (optional)")
1161
+ p.add_argument("--filename", help="Output filename")
1162
+ p.set_defaults(func=cmd_screenshot)
1163
+
1164
+ # pdf
1165
+ p = subparsers.add_parser("pdf", help="Save page as PDF")
1166
+ p.add_argument("--filename", help="Output filename")
1167
+ p.set_defaults(func=cmd_pdf)
1168
+
1169
+ # Navigation
1170
+ subparsers.add_parser("go-back", help="Go back").set_defaults(func=cmd_go_back)
1171
+ subparsers.add_parser("go-forward", help="Go forward").set_defaults(
1172
+ func=cmd_go_forward
1173
+ )
1174
+ subparsers.add_parser("reload", help="Reload page").set_defaults(func=cmd_reload)
1175
+
1176
+ # Keyboard
1177
+ p = subparsers.add_parser("press", help="Press a key")
1178
+ p.add_argument("key", help="Key name (Enter, ArrowDown, etc.)")
1179
+ p.set_defaults(func=cmd_press)
1180
+
1181
+ # Mouse
1182
+ p = subparsers.add_parser("mousemove", help="Move mouse")
1183
+ p.add_argument("x", type=int, help="X coordinate")
1184
+ p.add_argument("y", type=int, help="Y coordinate")
1185
+ p.set_defaults(func=cmd_mousemove)
1186
+
1187
+ p = subparsers.add_parser("mousedown", help="Press mouse button")
1188
+ p.add_argument("button", nargs="?", default="left", help="Button (left/right)")
1189
+ p.set_defaults(func=cmd_mousedown)
1190
+
1191
+ p = subparsers.add_parser("mouseup", help="Release mouse button")
1192
+ p.add_argument("button", nargs="?", default="left", help="Button (left/right)")
1193
+ p.set_defaults(func=cmd_mouseup)
1194
+
1195
+ p = subparsers.add_parser("scroll", help="Scroll the page")
1196
+ p.add_argument("dx", type=int, help="Horizontal scroll")
1197
+ p.add_argument("dy", type=int, help="Vertical scroll")
1198
+ p.set_defaults(func=cmd_scroll)
1199
+
1200
+ # Resize
1201
+ p = subparsers.add_parser("resize", help="Resize window")
1202
+ p.add_argument("width", type=int, help="Width in pixels")
1203
+ p.add_argument("height", type=int, help="Height in pixels")
1204
+ p.set_defaults(func=cmd_resize)
1205
+
1206
+ # Dialog
1207
+ p = subparsers.add_parser("dialog-accept", help="Accept dialog")
1208
+ p.add_argument("text", nargs="?", help="Prompt text (optional)")
1209
+ p.set_defaults(func=cmd_dialog_accept)
1210
+
1211
+ subparsers.add_parser("dialog-dismiss", help="Dismiss dialog").set_defaults(
1212
+ func=cmd_dialog_dismiss
1213
+ )
1214
+
1215
+ # Tabs
1216
+ subparsers.add_parser("tab-list", help="List tabs").set_defaults(
1217
+ func=cmd_tab_list
1218
+ )
1219
+
1220
+ p = subparsers.add_parser("tab-new", help="Open new tab")
1221
+ p.add_argument("url", nargs="?", help="URL to open")
1222
+ p.set_defaults(func=cmd_tab_new)
1223
+
1224
+ p = subparsers.add_parser("tab-close", help="Close a tab")
1225
+ p.add_argument("index", nargs="?", type=int, help="Tab index")
1226
+ p.set_defaults(func=cmd_tab_close)
1227
+
1228
+ p = subparsers.add_parser("tab-select", help="Select a tab")
1229
+ p.add_argument("index", type=int, help="Tab index")
1230
+ p.set_defaults(func=cmd_tab_select)
1231
+
1232
+ # Cookies
1233
+ p = subparsers.add_parser("cookie-list", help="List cookies")
1234
+ p.add_argument("--domain", help="Filter by domain")
1235
+ p.set_defaults(func=cmd_cookie_list)
1236
+
1237
+ p = subparsers.add_parser("cookie-get", help="Get a cookie")
1238
+ p.add_argument("name", help="Cookie name")
1239
+ p.set_defaults(func=cmd_cookie_get)
1240
+
1241
+ p = subparsers.add_parser("cookie-set", help="Set a cookie")
1242
+ p.add_argument("name", help="Cookie name")
1243
+ p.add_argument("value", help="Cookie value")
1244
+ p.add_argument("--domain", help="Cookie domain")
1245
+ p.add_argument("--path", help="Cookie path")
1246
+ p.add_argument("--secure", action="store_true", help="Secure flag")
1247
+ p.add_argument("--httpOnly", action="store_true", help="HttpOnly flag")
1248
+ p.set_defaults(func=cmd_cookie_set)
1249
+
1250
+ p = subparsers.add_parser("cookie-delete", help="Delete a cookie")
1251
+ p.add_argument("name", help="Cookie name")
1252
+ p.set_defaults(func=cmd_cookie_delete)
1253
+
1254
+ subparsers.add_parser("cookie-clear", help="Clear all cookies").set_defaults(
1255
+ func=cmd_cookie_clear
1256
+ )
1257
+
1258
+ # LocalStorage
1259
+ subparsers.add_parser("localstorage-list", help="List localStorage").set_defaults(
1260
+ func=cmd_localstorage_list
1261
+ )
1262
+
1263
+ p = subparsers.add_parser("localstorage-get", help="Get localStorage value")
1264
+ p.add_argument("key", help="Key name")
1265
+ p.set_defaults(func=cmd_localstorage_get)
1266
+
1267
+ p = subparsers.add_parser("localstorage-set", help="Set localStorage value")
1268
+ p.add_argument("key", help="Key name")
1269
+ p.add_argument("value", help="Value")
1270
+ p.set_defaults(func=cmd_localstorage_set)
1271
+
1272
+ p = subparsers.add_parser("localstorage-delete", help="Delete localStorage entry")
1273
+ p.add_argument("key", help="Key name")
1274
+ p.set_defaults(func=cmd_localstorage_delete)
1275
+
1276
+ subparsers.add_parser(
1277
+ "localstorage-clear", help="Clear localStorage"
1278
+ ).set_defaults(func=cmd_localstorage_clear)
1279
+
1280
+ # SessionStorage
1281
+ subparsers.add_parser(
1282
+ "sessionstorage-list", help="List sessionStorage"
1283
+ ).set_defaults(func=cmd_sessionstorage_list)
1284
+
1285
+ p = subparsers.add_parser("sessionstorage-get", help="Get sessionStorage value")
1286
+ p.add_argument("key", help="Key name")
1287
+ p.set_defaults(func=cmd_sessionstorage_get)
1288
+
1289
+ p = subparsers.add_parser("sessionstorage-set", help="Set sessionStorage value")
1290
+ p.add_argument("key", help="Key name")
1291
+ p.add_argument("value", help="Value")
1292
+ p.set_defaults(func=cmd_sessionstorage_set)
1293
+
1294
+ p = subparsers.add_parser(
1295
+ "sessionstorage-delete", help="Delete sessionStorage entry"
1296
+ )
1297
+ p.add_argument("key", help="Key name")
1298
+ p.set_defaults(func=cmd_sessionstorage_delete)
1299
+
1300
+ subparsers.add_parser(
1301
+ "sessionstorage-clear", help="Clear sessionStorage"
1302
+ ).set_defaults(func=cmd_sessionstorage_clear)
1303
+
1304
+ # Console / Network
1305
+ p = subparsers.add_parser("console", help="Show console messages")
1306
+ p.add_argument("level", nargs="?", help="Minimum level (error/warning/info/debug)")
1307
+ p.set_defaults(func=cmd_console)
1308
+
1309
+ subparsers.add_parser("network", help="Show network requests").set_defaults(
1310
+ func=cmd_network
1311
+ )
1312
+
1313
+ # Session management
1314
+ subparsers.add_parser("list", help="List all sessions").set_defaults(
1315
+ func=cmd_list
1316
+ )
1317
+ subparsers.add_parser("close", help="Close browser session").set_defaults(
1318
+ func=cmd_close
1319
+ )
1320
+ subparsers.add_parser("close-all", help="Close all sessions").set_defaults(
1321
+ func=cmd_close_all
1322
+ )
1323
+ subparsers.add_parser(
1324
+ "kill-all", help="Kill all browser processes"
1325
+ ).set_defaults(func=cmd_kill_all)
1326
+ subparsers.add_parser(
1327
+ "delete-data", help="Delete user data for session"
1328
+ ).set_defaults(func=cmd_delete_data)
1329
+
1330
+ # State
1331
+ p = subparsers.add_parser("state-save", help="Save browser state")
1332
+ p.add_argument("filename", nargs="?", help="Output filename")
1333
+ p.set_defaults(func=cmd_state_save)
1334
+
1335
+ p = subparsers.add_parser("state-load", help="Load browser state")
1336
+ p.add_argument("filename", help="State file to load")
1337
+ p.set_defaults(func=cmd_state_load)
1338
+
1339
+ # Install
1340
+ p = subparsers.add_parser("install", help="Install skills or dependencies")
1341
+ p.add_argument("--skills", action="store_true", help="Install Claude Code skills")
1342
+ p.set_defaults(func=cmd_install)
1343
+
1344
+ return parser
1345
+
1346
+
1347
+ def main():
1348
+ parser = build_parser()
1349
+ args = parser.parse_args()
1350
+
1351
+ if args.version:
1352
+ cmd_version(args)
1353
+ return
1354
+
1355
+ if not args.command:
1356
+ parser.print_help()
1357
+ return
1358
+
1359
+ try:
1360
+ args.func(args)
1361
+ except KeyboardInterrupt:
1362
+ print("\nInterrupted")
1363
+ sys.exit(130)
1364
+ except Exception as e:
1365
+ print(f"Error: {e}", file=sys.stderr)
1366
+ if os.environ.get("DRISSIONPAGE_CLI_DEBUG"):
1367
+ traceback.print_exc()
1368
+ sys.exit(1)
1369
+
1370
+
1371
+ if __name__ == "__main__":
1372
+ main()