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.
@@ -0,0 +1,3 @@
1
+ """patchright-cli — Undetected browser automation CLI using Patchright."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow running as `python -m patchright_cli`."""
2
+
3
+ from patchright_cli.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
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()