patchright-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.
- patchright_cli/__init__.py +3 -0
- patchright_cli/__main__.py +6 -0
- patchright_cli/cli.py +348 -0
- patchright_cli/daemon.py +909 -0
- patchright_cli/snapshot.py +242 -0
- patchright_cli-0.1.0.dist-info/METADATA +311 -0
- patchright_cli-0.1.0.dist-info/RECORD +10 -0
- patchright_cli-0.1.0.dist-info/WHEEL +4 -0
- patchright_cli-0.1.0.dist-info/entry_points.txt +2 -0
- patchright_cli-0.1.0.dist-info/licenses/LICENSE +191 -0
patchright_cli/cli.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Thin CLI client for patchright-cli.
|
|
2
|
+
|
|
3
|
+
Parses arguments, connects to the daemon socket, sends a JSON command,
|
|
4
|
+
receives the result, and prints it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import socket
|
|
12
|
+
import struct
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from patchright_cli import __version__
|
|
18
|
+
from patchright_cli.daemon import DEFAULT_PORT, ensure_daemon_running
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _send_command(command: str, args: list, options: dict, port: int = DEFAULT_PORT) -> dict:
|
|
22
|
+
"""Connect to daemon, send command, receive response."""
|
|
23
|
+
msg = {
|
|
24
|
+
"command": command,
|
|
25
|
+
"args": args,
|
|
26
|
+
"options": options,
|
|
27
|
+
"cwd": os.getcwd(),
|
|
28
|
+
}
|
|
29
|
+
data = json.dumps(msg).encode("utf-8")
|
|
30
|
+
|
|
31
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
32
|
+
sock.settimeout(120) # generous timeout for slow operations
|
|
33
|
+
try:
|
|
34
|
+
sock.connect(("127.0.0.1", port))
|
|
35
|
+
sock.sendall(struct.pack("!I", len(data)) + data)
|
|
36
|
+
|
|
37
|
+
# Read length-prefixed response
|
|
38
|
+
header = _recv_exact(sock, 4)
|
|
39
|
+
length = struct.unpack("!I", header)[0]
|
|
40
|
+
resp_data = _recv_exact(sock, length)
|
|
41
|
+
return json.loads(resp_data.decode("utf-8"))
|
|
42
|
+
finally:
|
|
43
|
+
sock.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
|
47
|
+
"""Receive exactly n bytes from socket."""
|
|
48
|
+
buf = bytearray()
|
|
49
|
+
while len(buf) < n:
|
|
50
|
+
chunk = sock.recv(n - len(buf))
|
|
51
|
+
if not chunk:
|
|
52
|
+
raise ConnectionError("Connection closed by daemon")
|
|
53
|
+
buf.extend(chunk)
|
|
54
|
+
return bytes(buf)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# CLI definition
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
COMMANDS_HELP = {
|
|
62
|
+
# Core
|
|
63
|
+
"open": "open [url] Open browser (starts daemon if needed)",
|
|
64
|
+
"goto": "goto <url> Navigate to URL",
|
|
65
|
+
"click": "click <ref> Click element by ref",
|
|
66
|
+
"dblclick": "dblclick <ref> Double-click element by ref",
|
|
67
|
+
"fill": "fill <ref> <value> Fill text into element",
|
|
68
|
+
"type": "type <text> Type text via keyboard",
|
|
69
|
+
"hover": "hover <ref> Hover over element",
|
|
70
|
+
"select": "select <ref> <value> Select dropdown option",
|
|
71
|
+
"check": "check <ref> Check checkbox/radio",
|
|
72
|
+
"uncheck": "uncheck <ref> Uncheck checkbox/radio",
|
|
73
|
+
"snapshot": "snapshot Take accessibility snapshot",
|
|
74
|
+
"eval": "eval <expr> Evaluate JavaScript",
|
|
75
|
+
"screenshot": "screenshot Save screenshot",
|
|
76
|
+
"drag": "drag <from> <to> Drag element to target",
|
|
77
|
+
"close": "close Close browser session",
|
|
78
|
+
# Navigation
|
|
79
|
+
"go-back": "go-back Go back",
|
|
80
|
+
"go-forward": "go-forward Go forward",
|
|
81
|
+
"reload": "reload Reload page",
|
|
82
|
+
# Keyboard
|
|
83
|
+
"press": "press <key> Press key",
|
|
84
|
+
"keydown": "keydown <key> Key down",
|
|
85
|
+
"keyup": "keyup <key> Key up",
|
|
86
|
+
# Mouse
|
|
87
|
+
"mousemove": "mousemove <x> <y> Move mouse",
|
|
88
|
+
"mousedown": "mousedown [button] Mouse button down",
|
|
89
|
+
"mouseup": "mouseup [button] Mouse button up",
|
|
90
|
+
"mousewheel": "mousewheel <dx> <dy> Scroll mouse wheel",
|
|
91
|
+
# Tabs
|
|
92
|
+
"tab-list": "tab-list List tabs",
|
|
93
|
+
"tab-new": "tab-new [url] Open new tab",
|
|
94
|
+
"tab-close": "tab-close [index] Close tab",
|
|
95
|
+
"tab-select": "tab-select <index> Switch to tab",
|
|
96
|
+
# Storage
|
|
97
|
+
"cookie-list": "cookie-list List cookies",
|
|
98
|
+
"cookie-get": "cookie-get <name> Get cookie",
|
|
99
|
+
"cookie-set": "cookie-set <n> <v> Set cookie",
|
|
100
|
+
"cookie-delete": "cookie-delete <name> Delete cookie",
|
|
101
|
+
"cookie-clear": "cookie-clear Clear all cookies",
|
|
102
|
+
"localstorage-list": "localstorage-list List localStorage",
|
|
103
|
+
"localstorage-get": "localstorage-get <k> Get localStorage item",
|
|
104
|
+
"localstorage-set": "localstorage-set <k> <v> Set localStorage item",
|
|
105
|
+
"localstorage-delete": "localstorage-delete <k> Delete localStorage item",
|
|
106
|
+
"localstorage-clear": "localstorage-clear Clear localStorage",
|
|
107
|
+
# Dialog
|
|
108
|
+
"dialog-accept": "dialog-accept [text] Accept next dialog",
|
|
109
|
+
"dialog-dismiss": "dialog-dismiss Dismiss next dialog",
|
|
110
|
+
# Upload / Resize
|
|
111
|
+
"upload": "upload <file> [ref] Upload file to input",
|
|
112
|
+
"resize": "resize <w> <h> Resize viewport",
|
|
113
|
+
# State
|
|
114
|
+
"state-save": "state-save [file] Save cookies+storage to JSON",
|
|
115
|
+
"state-load": "state-load <file> Load saved state",
|
|
116
|
+
# Session storage
|
|
117
|
+
"sessionstorage-list": "sessionstorage-list List sessionStorage",
|
|
118
|
+
"sessionstorage-get": "sessionstorage-get <k> Get sessionStorage item",
|
|
119
|
+
"sessionstorage-set": "sessionstorage-set <k> <v> Set sessionStorage item",
|
|
120
|
+
"sessionstorage-delete": "sessionstorage-delete <k> Delete sessionStorage item",
|
|
121
|
+
"sessionstorage-clear": "sessionstorage-clear Clear sessionStorage",
|
|
122
|
+
# Route
|
|
123
|
+
"route": "route <pattern> [--status=N] [--body=S] Mock requests",
|
|
124
|
+
"route-list": "route-list List active routes",
|
|
125
|
+
"unroute": "unroute [pattern] Remove route(s)",
|
|
126
|
+
# Run code
|
|
127
|
+
"run-code": "run-code <code> Run raw JS in page context",
|
|
128
|
+
# Tracing
|
|
129
|
+
"tracing-start": "tracing-start Start Playwright tracing",
|
|
130
|
+
"tracing-stop": "tracing-stop Stop tracing and save",
|
|
131
|
+
# Video
|
|
132
|
+
"video-start": "video-start Start video recording",
|
|
133
|
+
"video-stop": "video-stop [file] Stop recording and save",
|
|
134
|
+
# PDF
|
|
135
|
+
"pdf": "pdf [--filename=F] Save page as PDF",
|
|
136
|
+
# DevTools
|
|
137
|
+
"console": "console [level] Show console messages",
|
|
138
|
+
"network": "network Show network requests",
|
|
139
|
+
# Session
|
|
140
|
+
"list": "list List sessions",
|
|
141
|
+
"close-all": "close-all Close all sessions",
|
|
142
|
+
"kill-all": "kill-all Kill all sessions",
|
|
143
|
+
"delete-data": "delete-data Delete persistent profile",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ALL_COMMANDS = list(COMMANDS_HELP.keys())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _print_help():
|
|
150
|
+
click.echo("patchright-cli — Undetected browser automation CLI\n")
|
|
151
|
+
click.echo("Usage: patchright-cli [OPTIONS] <command> [args...]\n")
|
|
152
|
+
click.echo("Options:")
|
|
153
|
+
click.echo(" --headless Run headless (default: headed)")
|
|
154
|
+
click.echo(" --persistent Use persistent profile")
|
|
155
|
+
click.echo(" --profile=<path> Custom profile directory")
|
|
156
|
+
click.echo(" -s=<name> Named session (default: 'default')")
|
|
157
|
+
click.echo(" --port=<n> Daemon port (default: 9321)")
|
|
158
|
+
click.echo(" --version Show version")
|
|
159
|
+
click.echo(" --help Show this help\n")
|
|
160
|
+
click.echo("Commands:")
|
|
161
|
+
# Group by category
|
|
162
|
+
categories = [
|
|
163
|
+
(
|
|
164
|
+
"Core",
|
|
165
|
+
[
|
|
166
|
+
"open",
|
|
167
|
+
"goto",
|
|
168
|
+
"click",
|
|
169
|
+
"dblclick",
|
|
170
|
+
"fill",
|
|
171
|
+
"type",
|
|
172
|
+
"hover",
|
|
173
|
+
"select",
|
|
174
|
+
"check",
|
|
175
|
+
"uncheck",
|
|
176
|
+
"snapshot",
|
|
177
|
+
"eval",
|
|
178
|
+
"screenshot",
|
|
179
|
+
"drag",
|
|
180
|
+
"close",
|
|
181
|
+
],
|
|
182
|
+
),
|
|
183
|
+
("Navigation", ["go-back", "go-forward", "reload"]),
|
|
184
|
+
("Keyboard", ["press", "keydown", "keyup"]),
|
|
185
|
+
("Mouse", ["mousemove", "mousedown", "mouseup", "mousewheel"]),
|
|
186
|
+
("Tabs", ["tab-list", "tab-new", "tab-close", "tab-select"]),
|
|
187
|
+
("Dialog", ["dialog-accept", "dialog-dismiss"]),
|
|
188
|
+
("Upload/Resize", ["upload", "resize"]),
|
|
189
|
+
("State", ["state-save", "state-load"]),
|
|
190
|
+
(
|
|
191
|
+
"Storage",
|
|
192
|
+
[
|
|
193
|
+
"cookie-list",
|
|
194
|
+
"cookie-get",
|
|
195
|
+
"cookie-set",
|
|
196
|
+
"cookie-delete",
|
|
197
|
+
"cookie-clear",
|
|
198
|
+
"localstorage-list",
|
|
199
|
+
"localstorage-get",
|
|
200
|
+
"localstorage-set",
|
|
201
|
+
"localstorage-delete",
|
|
202
|
+
"localstorage-clear",
|
|
203
|
+
"sessionstorage-list",
|
|
204
|
+
"sessionstorage-get",
|
|
205
|
+
"sessionstorage-set",
|
|
206
|
+
"sessionstorage-delete",
|
|
207
|
+
"sessionstorage-clear",
|
|
208
|
+
],
|
|
209
|
+
),
|
|
210
|
+
("Route", ["route", "route-list", "unroute"]),
|
|
211
|
+
("Code", ["run-code"]),
|
|
212
|
+
("Tracing", ["tracing-start", "tracing-stop"]),
|
|
213
|
+
("Video", ["video-start", "video-stop"]),
|
|
214
|
+
("PDF", ["pdf"]),
|
|
215
|
+
("DevTools", ["console", "network"]),
|
|
216
|
+
("Session", ["list", "close-all", "kill-all", "delete-data"]),
|
|
217
|
+
]
|
|
218
|
+
for cat_name, cmds in categories:
|
|
219
|
+
click.echo(f"\n {cat_name}:")
|
|
220
|
+
for c in cmds:
|
|
221
|
+
click.echo(f" {COMMANDS_HELP[c]}")
|
|
222
|
+
click.echo()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main():
|
|
226
|
+
"""Entry point for the CLI."""
|
|
227
|
+
argv = sys.argv[1:]
|
|
228
|
+
|
|
229
|
+
# Parse global options manually (before the command)
|
|
230
|
+
headless = False
|
|
231
|
+
persistent = False
|
|
232
|
+
profile = None
|
|
233
|
+
session_name = "default"
|
|
234
|
+
port = DEFAULT_PORT
|
|
235
|
+
|
|
236
|
+
# Extract options
|
|
237
|
+
remaining = []
|
|
238
|
+
i = 0
|
|
239
|
+
while i < len(argv):
|
|
240
|
+
arg = argv[i]
|
|
241
|
+
if arg == "--headless":
|
|
242
|
+
headless = True
|
|
243
|
+
elif arg == "--persistent":
|
|
244
|
+
persistent = True
|
|
245
|
+
elif arg.startswith("--profile="):
|
|
246
|
+
profile = arg.split("=", 1)[1]
|
|
247
|
+
elif arg.startswith("--profile") and i + 1 < len(argv):
|
|
248
|
+
i += 1
|
|
249
|
+
profile = argv[i]
|
|
250
|
+
elif arg.startswith("-s="):
|
|
251
|
+
session_name = arg.split("=", 1)[1]
|
|
252
|
+
elif arg == "-s" and i + 1 < len(argv):
|
|
253
|
+
i += 1
|
|
254
|
+
session_name = argv[i]
|
|
255
|
+
elif arg.startswith("--port="):
|
|
256
|
+
port = int(arg.split("=", 1)[1])
|
|
257
|
+
elif arg == "--port" and i + 1 < len(argv):
|
|
258
|
+
i += 1
|
|
259
|
+
port = int(argv[i])
|
|
260
|
+
elif arg in ("--version", "-v"):
|
|
261
|
+
click.echo(f"patchright-cli {__version__}")
|
|
262
|
+
sys.exit(0)
|
|
263
|
+
elif arg in ("--help", "-h"):
|
|
264
|
+
_print_help()
|
|
265
|
+
sys.exit(0)
|
|
266
|
+
else:
|
|
267
|
+
remaining.append(arg)
|
|
268
|
+
i += 1
|
|
269
|
+
|
|
270
|
+
if not remaining:
|
|
271
|
+
_print_help()
|
|
272
|
+
sys.exit(0)
|
|
273
|
+
|
|
274
|
+
command = remaining[0]
|
|
275
|
+
args = remaining[1:]
|
|
276
|
+
|
|
277
|
+
if command not in ALL_COMMANDS:
|
|
278
|
+
click.echo(f"Unknown command: {command}\n", err=True)
|
|
279
|
+
_print_help()
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
|
|
282
|
+
# Separate --key=value and --flag from positional args
|
|
283
|
+
positional_args = []
|
|
284
|
+
extra_opts = {}
|
|
285
|
+
for a in args:
|
|
286
|
+
if a.startswith("--") and "=" in a:
|
|
287
|
+
k, v = a[2:].split("=", 1)
|
|
288
|
+
extra_opts[k] = v
|
|
289
|
+
elif a.startswith("--"):
|
|
290
|
+
extra_opts[a[2:]] = True
|
|
291
|
+
else:
|
|
292
|
+
positional_args.append(a)
|
|
293
|
+
args = positional_args
|
|
294
|
+
|
|
295
|
+
# Build options dict
|
|
296
|
+
options = {"session": session_name, **extra_opts}
|
|
297
|
+
if headless:
|
|
298
|
+
options["headless"] = True
|
|
299
|
+
if persistent:
|
|
300
|
+
options["persistent"] = True
|
|
301
|
+
if profile:
|
|
302
|
+
options["profile"] = profile
|
|
303
|
+
|
|
304
|
+
# Ensure daemon is running (auto-start for 'open', require for others)
|
|
305
|
+
try:
|
|
306
|
+
if command == "open":
|
|
307
|
+
ensure_daemon_running(port, headless)
|
|
308
|
+
else:
|
|
309
|
+
# Try connecting first; if fails, tell user to open
|
|
310
|
+
import socket as _socket
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
314
|
+
sock.settimeout(1)
|
|
315
|
+
sock.connect(("127.0.0.1", port))
|
|
316
|
+
sock.close()
|
|
317
|
+
except (ConnectionRefusedError, OSError, TimeoutError):
|
|
318
|
+
click.echo(
|
|
319
|
+
f"Daemon is not running on port {port}. Run 'patchright-cli open' first.",
|
|
320
|
+
err=True,
|
|
321
|
+
)
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
except RuntimeError as e:
|
|
324
|
+
click.echo(str(e), err=True)
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
327
|
+
# Send command to daemon
|
|
328
|
+
try:
|
|
329
|
+
response = _send_command(command, args, options, port)
|
|
330
|
+
except ConnectionError as e:
|
|
331
|
+
click.echo(f"Connection error: {e}", err=True)
|
|
332
|
+
sys.exit(1)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
click.echo(f"Error: {e}", err=True)
|
|
335
|
+
sys.exit(1)
|
|
336
|
+
|
|
337
|
+
# Print output
|
|
338
|
+
output = response.get("output", "")
|
|
339
|
+
if output:
|
|
340
|
+
click.echo(output)
|
|
341
|
+
|
|
342
|
+
success = response.get("success", True)
|
|
343
|
+
if not success:
|
|
344
|
+
sys.exit(1)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
main()
|