patchright-cli 0.4.0__tar.gz → 0.5.0__tar.gz
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-0.5.0/.claude-plugin/marketplace.json +20 -0
- patchright_cli-0.5.0/.github/workflows/ci.yml +34 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.gitignore +1 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/CLAUDE.md +14 -3
- patchright_cli-0.5.0/Makefile +18 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/PKG-INFO +36 -3
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/README.md +30 -2
- patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-cdp-attach.md +297 -0
- patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-cli-enhancements.md +529 -0
- patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-codegen.md +234 -0
- patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-context-config-and-permissions.md +411 -0
- patchright_cli-0.5.0/docs/superpowers/plans/2026-04-14-show-dashboard.md +337 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/pyproject.toml +12 -1
- patchright_cli-0.5.0/skills/patchright-cli/SKILL.md +348 -0
- patchright_cli-0.5.0/skills/patchright-cli/references/request-mocking.md +103 -0
- patchright_cli-0.5.0/skills/patchright-cli/references/running-code.md +129 -0
- patchright_cli-0.5.0/skills/patchright-cli/references/session-management.md +106 -0
- patchright_cli-0.5.0/skills/patchright-cli/references/snapshot-refs.md +86 -0
- patchright_cli-0.5.0/skills/patchright-cli/references/storage-state.md +113 -0
- patchright_cli-0.5.0/skills/patchright-cli/references/tracing.md +53 -0
- patchright_cli-0.5.0/skills/patchright-cli/references/video-recording.md +85 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/__init__.py +1 -1
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/cli.py +219 -10
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/daemon.py +292 -36
- patchright_cli-0.5.0/src/patchright_cli/dashboard.py +135 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/ref_registry.py +33 -1
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/snapshot.py +4 -2
- patchright_cli-0.5.0/tests/test_cli_config.py +203 -0
- patchright_cli-0.5.0/tests/test_daemon_handlers.py +306 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/test_ref_registry.py +62 -0
- patchright_cli-0.5.0/uv.lock +1154 -0
- patchright_cli-0.4.0/.claude-plugin/marketplace.json +0 -21
- patchright_cli-0.4.0/skills/patchright-cli/SKILL.md +0 -492
- patchright_cli-0.4.0/tests/test_daemon_handlers.py +0 -124
- patchright_cli-0.4.0/uv.lock +0 -360
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.claude-plugin/skills/browser-automation.json +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.github/workflows/publish.yml +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/.pre-commit-config.yaml +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/CONTRIBUTING.md +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/LICENSE +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/docs/installation.md +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/patchright-cli +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/patchright-cli.cmd +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/src/patchright_cli/__main__.py +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/__init__.py +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/fixture.html +0 -0
- {patchright_cli-0.4.0 → patchright_cli-0.5.0}/tests/test_e2e.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "patchright-cli",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Han Zhu"
|
|
5
|
+
},
|
|
6
|
+
"metadata": {
|
|
7
|
+
"description": "Anti-detect browser automation CLI & Skills for AI agents, powered by Patchright",
|
|
8
|
+
"version": "0.5.0"
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "patchright-cli",
|
|
13
|
+
"description": "Anti-detect browser automation skill using Patchright (undetected Playwright fork)",
|
|
14
|
+
"source": "./",
|
|
15
|
+
"skills": [
|
|
16
|
+
"./skills/patchright-cli"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v6
|
|
15
|
+
with:
|
|
16
|
+
version: ">=0.7,<0.8"
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: uv venv && uv pip install -e ".[dev]"
|
|
19
|
+
- run: uv run pre-commit run --all-files
|
|
20
|
+
|
|
21
|
+
test:
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
os: [ubuntu-latest, windows-latest]
|
|
25
|
+
python-version: ["3.10", "3.12"]
|
|
26
|
+
runs-on: ${{ matrix.os }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: astral-sh/setup-uv@v6
|
|
30
|
+
with:
|
|
31
|
+
version: ">=0.7,<0.8"
|
|
32
|
+
python-version: ${{ matrix.python-version }}
|
|
33
|
+
- run: uv venv && uv pip install -e ".[dev]"
|
|
34
|
+
- run: uv run pytest tests/ -v --ignore=tests/test_e2e.py
|
|
@@ -26,7 +26,18 @@ python -m patchright_cli <command> [args...]
|
|
|
26
26
|
patchright-cli <command> [args...]
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
## Testing
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Unit tests (no browser required)
|
|
33
|
+
pytest tests/ -v --ignore=tests/test_e2e.py
|
|
34
|
+
|
|
35
|
+
# E2E tests (requires Chrome)
|
|
36
|
+
pytest tests/test_e2e.py -v
|
|
37
|
+
|
|
38
|
+
# Lint
|
|
39
|
+
pre-commit run --all-files
|
|
40
|
+
```
|
|
30
41
|
|
|
31
42
|
## Architecture
|
|
32
43
|
|
|
@@ -38,9 +49,9 @@ CLI (cli.py) --TCP:9321--> Daemon (daemon.py) --Patchright--> Chrome
|
|
|
38
49
|
|
|
39
50
|
- **CLI (`src/patchright_cli/cli.py`)**: Stateless thin client. Parses argv manually (not click subcommands), builds a JSON `{command, args, options, cwd}` message, sends it over a length-prefixed TCP socket to the daemon, prints the response, and exits. All commands are defined in the `COMMANDS_HELP` dict and `ALL_COMMANDS` list.
|
|
40
51
|
|
|
41
|
-
- **Daemon (`src/patchright_cli/daemon.py`)**: Long-running async process managing browser sessions. Auto-spawned on `open` via `ensure_daemon_running()` which launches a subprocess. Uses `DaemonState` → multiple `Session` objects, each owning a Patchright persistent browser context. Command dispatch
|
|
52
|
+
- **Daemon (`src/patchright_cli/daemon.py`)**: Long-running async process managing browser sessions. Auto-spawned on `open` via `ensure_daemon_running()` which launches a subprocess. Uses `DaemonState` → multiple `Session` objects, each owning a Patchright persistent browser context. Command dispatch uses `@register()` decorated handlers in `COMMAND_HANDLERS` dict, dispatched by `handle_command()`. Console capture uses CDP sessions (not Playwright's `page.on('console')`) because Patchright suppresses it.
|
|
42
53
|
|
|
43
|
-
- **Snapshot (`src/patchright_cli/snapshot.py`)**:
|
|
54
|
+
- **Snapshot (`src/patchright_cli/snapshot.py`)**: Uses Playwright's native `aria_snapshot()` to get the accessibility tree. `RefRegistry` annotates each node with sequential refs (`e1`, `e2`, ...). Snapshots are saved as YAML to `.patchright-cli/` in the caller's cwd.
|
|
44
55
|
|
|
45
56
|
## Key Design Decisions
|
|
46
57
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.PHONY: install-dev lint test-unit test-e2e test build
|
|
2
|
+
|
|
3
|
+
install-dev:
|
|
4
|
+
uv venv && uv pip install -e ".[dev]" && pre-commit install
|
|
5
|
+
|
|
6
|
+
lint:
|
|
7
|
+
pre-commit run --all-files
|
|
8
|
+
|
|
9
|
+
test-unit:
|
|
10
|
+
pytest tests/ -v --ignore=tests/test_e2e.py
|
|
11
|
+
|
|
12
|
+
test-e2e:
|
|
13
|
+
pytest tests/test_e2e.py -v
|
|
14
|
+
|
|
15
|
+
test: lint test-unit
|
|
16
|
+
|
|
17
|
+
build:
|
|
18
|
+
uv build
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Anti-detect browser automation CLI using Patchright (undetected Playwright fork)
|
|
5
5
|
Project-URL: Homepage, https://github.com/AhaiMk01/patchright-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/AhaiMk01/patchright-cli
|
|
@@ -21,9 +21,14 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
22
22
|
Classifier: Topic :: Software Development :: Testing
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: aiohttp>=3.9
|
|
24
25
|
Requires-Dist: click
|
|
25
26
|
Requires-Dist: patchright
|
|
26
27
|
Requires-Dist: pyyaml
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
32
|
Description-Content-Type: text/markdown
|
|
28
33
|
|
|
29
34
|
# patchright-cli
|
|
@@ -173,6 +178,8 @@ patchright-cli check <ref> # Check checkbox
|
|
|
173
178
|
patchright-cli uncheck <ref> # Uncheck checkbox
|
|
174
179
|
patchright-cli drag <from> <to> # Drag and drop
|
|
175
180
|
patchright-cli snapshot # Accessibility snapshot
|
|
181
|
+
patchright-cli snapshot -i # Interactive elements only (recommended)
|
|
182
|
+
patchright-cli snapshot --depth=3 # Limit depth
|
|
176
183
|
patchright-cli snapshot <ref> # Snapshot element subtree
|
|
177
184
|
patchright-cli snapshot --filename=f # Save to custom path
|
|
178
185
|
patchright-cli eval <expr> # Run JavaScript
|
|
@@ -297,6 +304,17 @@ patchright-cli console warning # Filter by level
|
|
|
297
304
|
patchright-cli console --clear # Clear after printing
|
|
298
305
|
```
|
|
299
306
|
|
|
307
|
+
### Output
|
|
308
|
+
```bash
|
|
309
|
+
patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
|
|
310
|
+
patchright-cli --raw snapshot > p.yml # Pipe raw snapshot to file
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Setup
|
|
314
|
+
```bash
|
|
315
|
+
patchright-cli install --skills # Auto-install skills for detected AI agents
|
|
316
|
+
```
|
|
317
|
+
|
|
300
318
|
### Sessions
|
|
301
319
|
```bash
|
|
302
320
|
patchright-cli -s=mysession open https://example.com --persistent
|
|
@@ -371,6 +389,13 @@ Works with any AI coding agent that supports SKILL.md skills:
|
|
|
371
389
|
| **Windsurf** | Copy SKILL.md to `.windsurf/skills/patchright-cli/` in your project |
|
|
372
390
|
| **Aider** | Copy SKILL.md to `.aider/skills/patchright-cli/` in your project |
|
|
373
391
|
|
|
392
|
+
Or auto-install for all detected agents:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
pip install patchright-cli
|
|
396
|
+
patchright-cli install --skills
|
|
397
|
+
```
|
|
398
|
+
|
|
374
399
|
Or just tell your agent:
|
|
375
400
|
|
|
376
401
|
> Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md
|
|
@@ -397,9 +422,7 @@ patchright-cli aims for full command parity with [Microsoft's playwright-cli](ht
|
|
|
397
422
|
| Feature | Reason |
|
|
398
423
|
|---------|--------|
|
|
399
424
|
| `--browser=firefox/webkit/msedge` | Patchright only supports Chromium/Chrome. Anti-detect patches are Chrome-specific. |
|
|
400
|
-
| `--config=<file>` | No config file system. Options are passed as CLI flags. |
|
|
401
425
|
| `--extension` | Browser extension connection not supported. Patchright uses CDP directly. |
|
|
402
|
-
| `show` / `devtools-start` | DevTools are available natively in headed mode (default). |
|
|
403
426
|
| `install` / `install-browser` | Use `python -m patchright install chromium` instead. |
|
|
404
427
|
|
|
405
428
|
All other commands and options are fully supported.
|
|
@@ -415,6 +438,16 @@ All other commands and options are fully supported.
|
|
|
415
438
|
|
|
416
439
|
---
|
|
417
440
|
|
|
441
|
+
## Credits
|
|
442
|
+
|
|
443
|
+
This project builds on the work of:
|
|
444
|
+
|
|
445
|
+
- **[Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright)** by [@Kaliiiiiiiiii-Vinyzu](https://github.com/Kaliiiiiiiiii-Vinyzu) — the undetected Playwright fork that makes this possible. All anti-detect capabilities come from Patchright.
|
|
446
|
+
- **[playwright-cli](https://github.com/microsoft/playwright-cli)** by Microsoft — the original CLI that patchright-cli's command interface is modeled after.
|
|
447
|
+
- **[camoufox-cli](https://github.com/Bin-Huang/camoufox-cli)** by [@Bin-Huang](https://github.com/Bin-Huang) — inspiration for the CLI-first approach to anti-detect browser automation and the SKILL.md agent integration pattern.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
418
451
|
## License
|
|
419
452
|
|
|
420
453
|
Apache 2.0 — same as [playwright-cli](https://github.com/microsoft/playwright-cli)
|
|
@@ -145,6 +145,8 @@ patchright-cli check <ref> # Check checkbox
|
|
|
145
145
|
patchright-cli uncheck <ref> # Uncheck checkbox
|
|
146
146
|
patchright-cli drag <from> <to> # Drag and drop
|
|
147
147
|
patchright-cli snapshot # Accessibility snapshot
|
|
148
|
+
patchright-cli snapshot -i # Interactive elements only (recommended)
|
|
149
|
+
patchright-cli snapshot --depth=3 # Limit depth
|
|
148
150
|
patchright-cli snapshot <ref> # Snapshot element subtree
|
|
149
151
|
patchright-cli snapshot --filename=f # Save to custom path
|
|
150
152
|
patchright-cli eval <expr> # Run JavaScript
|
|
@@ -269,6 +271,17 @@ patchright-cli console warning # Filter by level
|
|
|
269
271
|
patchright-cli console --clear # Clear after printing
|
|
270
272
|
```
|
|
271
273
|
|
|
274
|
+
### Output
|
|
275
|
+
```bash
|
|
276
|
+
patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
|
|
277
|
+
patchright-cli --raw snapshot > p.yml # Pipe raw snapshot to file
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Setup
|
|
281
|
+
```bash
|
|
282
|
+
patchright-cli install --skills # Auto-install skills for detected AI agents
|
|
283
|
+
```
|
|
284
|
+
|
|
272
285
|
### Sessions
|
|
273
286
|
```bash
|
|
274
287
|
patchright-cli -s=mysession open https://example.com --persistent
|
|
@@ -343,6 +356,13 @@ Works with any AI coding agent that supports SKILL.md skills:
|
|
|
343
356
|
| **Windsurf** | Copy SKILL.md to `.windsurf/skills/patchright-cli/` in your project |
|
|
344
357
|
| **Aider** | Copy SKILL.md to `.aider/skills/patchright-cli/` in your project |
|
|
345
358
|
|
|
359
|
+
Or auto-install for all detected agents:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
pip install patchright-cli
|
|
363
|
+
patchright-cli install --skills
|
|
364
|
+
```
|
|
365
|
+
|
|
346
366
|
Or just tell your agent:
|
|
347
367
|
|
|
348
368
|
> Install patchright-cli skill from https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/skills/patchright-cli/SKILL.md
|
|
@@ -369,9 +389,7 @@ patchright-cli aims for full command parity with [Microsoft's playwright-cli](ht
|
|
|
369
389
|
| Feature | Reason |
|
|
370
390
|
|---------|--------|
|
|
371
391
|
| `--browser=firefox/webkit/msedge` | Patchright only supports Chromium/Chrome. Anti-detect patches are Chrome-specific. |
|
|
372
|
-
| `--config=<file>` | No config file system. Options are passed as CLI flags. |
|
|
373
392
|
| `--extension` | Browser extension connection not supported. Patchright uses CDP directly. |
|
|
374
|
-
| `show` / `devtools-start` | DevTools are available natively in headed mode (default). |
|
|
375
393
|
| `install` / `install-browser` | Use `python -m patchright install chromium` instead. |
|
|
376
394
|
|
|
377
395
|
All other commands and options are fully supported.
|
|
@@ -387,6 +405,16 @@ All other commands and options are fully supported.
|
|
|
387
405
|
|
|
388
406
|
---
|
|
389
407
|
|
|
408
|
+
## Credits
|
|
409
|
+
|
|
410
|
+
This project builds on the work of:
|
|
411
|
+
|
|
412
|
+
- **[Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright)** by [@Kaliiiiiiiiii-Vinyzu](https://github.com/Kaliiiiiiiiii-Vinyzu) — the undetected Playwright fork that makes this possible. All anti-detect capabilities come from Patchright.
|
|
413
|
+
- **[playwright-cli](https://github.com/microsoft/playwright-cli)** by Microsoft — the original CLI that patchright-cli's command interface is modeled after.
|
|
414
|
+
- **[camoufox-cli](https://github.com/Bin-Huang/camoufox-cli)** by [@Bin-Huang](https://github.com/Bin-Huang) — inspiration for the CLI-first approach to anti-detect browser automation and the SKILL.md agent integration pattern.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
390
418
|
## License
|
|
391
419
|
|
|
392
420
|
Apache 2.0 — same as [playwright-cli](https://github.com/microsoft/playwright-cli)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# CDP Attach Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add `attach --cdp=<url>` support so patchright-cli can connect to an already-running Chrome/Edge instance via the Chrome DevTools Protocol.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a new `attach` CLI command and corresponding daemon handler. When a `cdp_endpoint` is provided, the daemon uses `chromium.connect_over_cdp()` instead of `launch_persistent_context()`, then creates a fresh browser context and page from the connected `Browser` object.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.10+, Patchright (Playwright)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
| File | Responsibility |
|
|
16
|
+
|------|----------------|
|
|
17
|
+
| `src/patchright_cli/cli.py` | Add `attach` command help and map `--cdp` flag. |
|
|
18
|
+
| `src/patchright_cli/daemon.py` | Handle `attach` command, update `get_or_create_session` to support `cdp_endpoint`, `cdp_headers`, `cdp_timeout`. |
|
|
19
|
+
| `tests/test_daemon_handlers.py` | Async test for `attach` handler. |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### Task 1: CLI command definition
|
|
24
|
+
|
|
25
|
+
**Files:**
|
|
26
|
+
- Modify: `src/patchright_cli/cli.py`
|
|
27
|
+
|
|
28
|
+
- [ ] **Step 1: Add `attach` to command help**
|
|
29
|
+
|
|
30
|
+
In `COMMANDS_HELP`, add:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
"attach": "attach --cdp=<url> Attach to running Chrome via CDP",
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
In `_print_help()`, under Browser lifecycle (near `open`), change the category to include `attach`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
(
|
|
40
|
+
"Core",
|
|
41
|
+
[
|
|
42
|
+
"open",
|
|
43
|
+
"attach",
|
|
44
|
+
"goto",
|
|
45
|
+
...
|
|
46
|
+
],
|
|
47
|
+
),
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- [ ] **Step 2: Add `--cdp` global option parsing**
|
|
51
|
+
|
|
52
|
+
In `main()`, inside the option-parsing loop, add:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
elif arg.startswith("--cdp="):
|
|
56
|
+
extra_opts["cdp"] = arg.split("=", 1)[1]
|
|
57
|
+
elif arg == "--cdp" and i + 1 < len(argv):
|
|
58
|
+
i += 1
|
|
59
|
+
extra_opts["cdp"] = argv[i]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
And in `_print_help()`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
click.echo(" --cdp=<url> Attach to Chrome via CDP endpoint")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- [ ] **Step 3: Commit**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git add src/patchright_cli/cli.py
|
|
72
|
+
git commit -m "feat: add attach command and --cdp flag to CLI"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Task 2: Daemon handler and session creation
|
|
78
|
+
|
|
79
|
+
**Files:**
|
|
80
|
+
- Modify: `src/patchright_cli/daemon.py`
|
|
81
|
+
- Test: `tests/test_daemon_handlers.py`
|
|
82
|
+
|
|
83
|
+
- [ ] **Step 1: Write the failing test**
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
@pytest.mark.asyncio
|
|
87
|
+
async def test_attach_cdp(mock_state, mock_session):
|
|
88
|
+
mock_state.sessions = {}
|
|
89
|
+
mock_browser = MagicMock()
|
|
90
|
+
mock_context = MagicMock()
|
|
91
|
+
mock_page = MagicMock()
|
|
92
|
+
mock_page.url = "about:blank"
|
|
93
|
+
mock_page.title = AsyncMock(return_value="Attached")
|
|
94
|
+
mock_browser.new_context = AsyncMock(return_value=mock_context)
|
|
95
|
+
mock_context.new_page = AsyncMock(return_value=mock_page)
|
|
96
|
+
mock_context.pages = [mock_page]
|
|
97
|
+
mock_state.playwright.chromium.connect_over_cdp = AsyncMock(return_value=mock_browser)
|
|
98
|
+
|
|
99
|
+
with patch("patchright_cli.daemon.take_snapshot", new_callable=AsyncMock) as mock_snap:
|
|
100
|
+
mock_snap.return_value = ("snapshot-text", MagicMock())
|
|
101
|
+
with patch("patchright_cli.daemon.save_snapshot", return_value="/tmp/snap.yml"):
|
|
102
|
+
response = await handle_command(
|
|
103
|
+
mock_state,
|
|
104
|
+
{"command": "attach", "args": [], "options": {"cdp": "http://localhost:9222"}}
|
|
105
|
+
)
|
|
106
|
+
assert response["success"] is True
|
|
107
|
+
mock_state.playwright.chromium.connect_over_cdp.assert_awaited_once_with(
|
|
108
|
+
"http://localhost:9222", headers=None, timeout=30000
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
113
|
+
|
|
114
|
+
Run: `pytest tests/test_daemon_handlers.py::test_attach_cdp -v`
|
|
115
|
+
Expected: FAIL — command unknown or `connect_over_cdp` not called.
|
|
116
|
+
|
|
117
|
+
- [ ] **Step 3: Update `get_or_create_session` for CDP**
|
|
118
|
+
|
|
119
|
+
In `DaemonState.get_or_create_session` signature, add:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
cdp_endpoint: str | None = None,
|
|
123
|
+
cdp_headers: dict | None = None,
|
|
124
|
+
cdp_timeout: int = 30000,
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then replace the `launch_kwargs` and `launch_persistent_context` block with branching logic:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
if cdp_endpoint:
|
|
131
|
+
browser = await self.playwright.chromium.connect_over_cdp(
|
|
132
|
+
cdp_endpoint, headers=cdp_headers, timeout=cdp_timeout
|
|
133
|
+
)
|
|
134
|
+
context = await browser.new_context()
|
|
135
|
+
pages = context.pages or []
|
|
136
|
+
if not pages:
|
|
137
|
+
page = await context.new_page()
|
|
138
|
+
pages = [page]
|
|
139
|
+
else:
|
|
140
|
+
launch_kwargs = {
|
|
141
|
+
"channel": "chrome",
|
|
142
|
+
"headless": use_headless,
|
|
143
|
+
"no_viewport": True,
|
|
144
|
+
"args": ["--disable-blink-features=AutomationControlled"],
|
|
145
|
+
}
|
|
146
|
+
if proxy:
|
|
147
|
+
parsed = urlparse(proxy)
|
|
148
|
+
if parsed.username or parsed.password:
|
|
149
|
+
import base64
|
|
150
|
+
creds = base64.b64encode(f"{parsed.username or ''}:{parsed.password or ''}".encode()).decode()
|
|
151
|
+
launch_kwargs["extra_http_headers"] = {"Proxy-Authorization": f"Basic {creds}"}
|
|
152
|
+
netloc = parsed.hostname or ""
|
|
153
|
+
if parsed.port:
|
|
154
|
+
netloc += f":{parsed.port}"
|
|
155
|
+
cleaned = parsed._replace(netloc=netloc).geturl()
|
|
156
|
+
launch_kwargs["proxy"] = {"server": cleaned}
|
|
157
|
+
else:
|
|
158
|
+
launch_kwargs["proxy"] = {"server": proxy}
|
|
159
|
+
launch_kwargs.update(context_options)
|
|
160
|
+
context = await self.playwright.chromium.launch_persistent_context(
|
|
161
|
+
profile_dir,
|
|
162
|
+
**launch_kwargs,
|
|
163
|
+
)
|
|
164
|
+
pages = context.pages or []
|
|
165
|
+
if not pages:
|
|
166
|
+
page = await context.new_page()
|
|
167
|
+
pages = [page]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Wait, we need to make sure `context_options` is still built for the non-CDP path. And for CDP, device/viewport options should also be applied. Actually, when using `browser.new_context()` after CDP connect, we can pass context options directly to `new_context()`:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
context_options = {}
|
|
174
|
+
if device and self.playwright is not None:
|
|
175
|
+
device_descriptor = self.playwright.devices.get(device)
|
|
176
|
+
if device_descriptor:
|
|
177
|
+
context_options.update(device_descriptor)
|
|
178
|
+
if viewport:
|
|
179
|
+
context_options["viewport"] = {"width": int(viewport["width"]), "height": int(viewport["height"])}
|
|
180
|
+
if locale:
|
|
181
|
+
context_options["locale"] = locale
|
|
182
|
+
if timezone:
|
|
183
|
+
context_options["timezone_id"] = timezone
|
|
184
|
+
if geolocation:
|
|
185
|
+
context_options["geolocation"] = {
|
|
186
|
+
"latitude": float(geolocation["lat"]),
|
|
187
|
+
"longitude": float(geolocation["lon"]),
|
|
188
|
+
}
|
|
189
|
+
context_options["permissions"] = context_options.get("permissions", []) + ["geolocation"]
|
|
190
|
+
if user_agent:
|
|
191
|
+
context_options["user_agent"] = user_agent
|
|
192
|
+
if grant_permissions:
|
|
193
|
+
perms = [p.strip() for p in grant_permissions.split(",") if p.strip()]
|
|
194
|
+
context_options["permissions"] = list(set(context_options.get("permissions", []) + perms))
|
|
195
|
+
|
|
196
|
+
if cdp_endpoint:
|
|
197
|
+
browser = await self.playwright.chromium.connect_over_cdp(
|
|
198
|
+
cdp_endpoint, headers=cdp_headers, timeout=cdp_timeout
|
|
199
|
+
)
|
|
200
|
+
context = await browser.new_context(**context_options)
|
|
201
|
+
pages = context.pages or []
|
|
202
|
+
if not pages:
|
|
203
|
+
page = await context.new_page()
|
|
204
|
+
pages = [page]
|
|
205
|
+
else:
|
|
206
|
+
launch_kwargs = {
|
|
207
|
+
"channel": "chrome",
|
|
208
|
+
"headless": use_headless,
|
|
209
|
+
"no_viewport": True,
|
|
210
|
+
"args": ["--disable-blink-features=AutomationControlled"],
|
|
211
|
+
}
|
|
212
|
+
if proxy:
|
|
213
|
+
parsed = urlparse(proxy)
|
|
214
|
+
if parsed.username or parsed.password:
|
|
215
|
+
import base64
|
|
216
|
+
creds = base64.b64encode(f"{parsed.username or ''}:{parsed.password or ''}".encode()).decode()
|
|
217
|
+
launch_kwargs["extra_http_headers"] = {"Proxy-Authorization": f"Basic {creds}"}
|
|
218
|
+
netloc = parsed.hostname or ""
|
|
219
|
+
if parsed.port:
|
|
220
|
+
netloc += f":{parsed.port}"
|
|
221
|
+
cleaned = parsed._replace(netloc=netloc).geturl()
|
|
222
|
+
launch_kwargs["proxy"] = {"server": cleaned}
|
|
223
|
+
else:
|
|
224
|
+
launch_kwargs["proxy"] = {"server": proxy}
|
|
225
|
+
launch_kwargs.update(context_options)
|
|
226
|
+
context = await self.playwright.chromium.launch_persistent_context(
|
|
227
|
+
profile_dir,
|
|
228
|
+
**launch_kwargs,
|
|
229
|
+
)
|
|
230
|
+
pages = context.pages or []
|
|
231
|
+
if not pages:
|
|
232
|
+
page = await context.new_page()
|
|
233
|
+
pages = [page]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 4: Add `attach` handler and update `open` handler**
|
|
237
|
+
|
|
238
|
+
In `handle_command`, add the `attach` branch right after the `open` branch:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
if cmd == "attach":
|
|
242
|
+
cdp = options.get("cdp")
|
|
243
|
+
if not cdp:
|
|
244
|
+
return {"success": False, "output": "Usage: attach --cdp=<url>"}
|
|
245
|
+
headless = options.get("headless", False)
|
|
246
|
+
session = await state.get_or_create_session(
|
|
247
|
+
session_name,
|
|
248
|
+
headless=headless,
|
|
249
|
+
cdp_endpoint=cdp,
|
|
250
|
+
cdp_headers=options.get("cdp-headers"),
|
|
251
|
+
cdp_timeout=int(options.get("cdp-timeout", 30000)),
|
|
252
|
+
device=options.get("device"),
|
|
253
|
+
viewport=options.get("viewport"),
|
|
254
|
+
locale=options.get("locale"),
|
|
255
|
+
timezone=options.get("timezone"),
|
|
256
|
+
geolocation=options.get("geolocation"),
|
|
257
|
+
user_agent=options.get("user-agent") or options.get("userAgent"),
|
|
258
|
+
grant_permissions=options.get("grant-permissions") or options.get("grantPermissions"),
|
|
259
|
+
)
|
|
260
|
+
if session.page:
|
|
261
|
+
session.push_history(session.page.url)
|
|
262
|
+
return await _page_info(session, cwd)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- [ ] **Step 5: Run test**
|
|
266
|
+
|
|
267
|
+
Run: `pytest tests/test_daemon_handlers.py::test_attach_cdp -v`
|
|
268
|
+
Expected: PASS
|
|
269
|
+
|
|
270
|
+
- [ ] **Step 6: Run full handler test suite**
|
|
271
|
+
|
|
272
|
+
Run: `pytest tests/test_daemon_handlers.py -v`
|
|
273
|
+
Expected: All PASS
|
|
274
|
+
|
|
275
|
+
- [ ] **Step 7: Commit**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
git add tests/test_daemon_handlers.py src/patchright_cli/daemon.py
|
|
279
|
+
git commit -m "feat: attach to running Chrome via CDP endpoint"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Self-Review
|
|
285
|
+
|
|
286
|
+
**1. Spec coverage:**
|
|
287
|
+
- `attach --cdp=<url>` → Task 1 + Task 2
|
|
288
|
+
- CDP headers/timeout support → Task 2 via `get_or_create_session`
|
|
289
|
+
- All requirements covered.
|
|
290
|
+
|
|
291
|
+
**2. Placeholder scan:**
|
|
292
|
+
- No TBDs or vague steps.
|
|
293
|
+
- Exact code provided for every change.
|
|
294
|
+
|
|
295
|
+
**3. Type consistency:**
|
|
296
|
+
- `cdp_endpoint` is consistently `str | None`.
|
|
297
|
+
- `cdp_timeout` defaults to `30000` (int) throughout.
|