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.
- drissionpage_cli/__init__.py +1372 -0
- drissionpage_cli/skills/drissionpage-cli/SKILL.md +249 -0
- drissionpage_cli/skills/drissionpage-cli/references/dual-mode.md +91 -0
- drissionpage_cli/skills/drissionpage-cli/references/element-locators.md +116 -0
- drissionpage_cli/skills/drissionpage-cli/references/network-listening.md +121 -0
- drissionpage_cli/skills/drissionpage-cli/references/running-code.md +182 -0
- drissionpage_cli/skills/drissionpage-cli/references/screenshots-pdf.md +93 -0
- drissionpage_cli/skills/drissionpage-cli/references/session-management.md +136 -0
- drissionpage_cli/skills/drissionpage-cli/references/storage-state.md +144 -0
- drissionpage_cli-0.1.0.dist-info/METADATA +314 -0
- drissionpage_cli-0.1.0.dist-info/RECORD +15 -0
- drissionpage_cli-0.1.0.dist-info/WHEEL +5 -0
- drissionpage_cli-0.1.0.dist-info/entry_points.txt +2 -0
- drissionpage_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- drissionpage_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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()
|