browserwright 0.7.2__tar.gz → 0.7.4__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.
- {browserwright-0.7.2 → browserwright-0.7.4}/PKG-INFO +2 -2
- {browserwright-0.7.2 → browserwright-0.7.4}/README.md +32 -106
- {browserwright-0.7.2 → browserwright-0.7.4}/pyproject.toml +2 -2
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/process.py +1 -1
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/api.py +12 -17
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/cdp.py +7 -65
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/cli.py +24 -170
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/_ipc.py +12 -86
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/_stale.py +3 -6
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/backends/__init__.py +1 -8
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/backends/base.py +8 -43
- browserwright-0.7.4/src/browserwright/daemon/backends/cdp.py +286 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/backends/extension.py +0 -5
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/cli.py +134 -470
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/config.py +22 -56
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/doctor.py +4 -28
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/launch_chrome.py +0 -8
- browserwright-0.7.4/src/browserwright/daemon/observability.py +80 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/platforms.py +6 -29
- browserwright-0.7.4/src/browserwright/daemon/resolver.py +57 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/daemon.py +4 -4
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/executor_registry.py +1 -27
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/extension_upstream.py +72 -93
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/facade.py +39 -9
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/facade_extension.py +60 -84
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/listener.py +33 -109
- browserwright-0.7.4/src/browserwright/daemon/server/proxy.py +664 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/relay.py +1 -20
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/state.py +9 -122
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/upstream.py +2 -23
- browserwright-0.7.4/src/browserwright/daemon/server/verbs.py +1155 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/discovery.py +5 -11
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/errors.py +3 -3
- browserwright-0.7.4/src/browserwright/install.py +240 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/__init__.py +0 -3
- browserwright-0.7.4/src/browserwright/memory/_lock.py +46 -0
- browserwright-0.7.4/src/browserwright/memory/_yaml.py +54 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/global_mem.py +1 -56
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/site_mem.py +1 -32
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/mode_b_client.py +19 -246
- browserwright-0.7.4/src/browserwright/primitives/__init__.py +35 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/primitives/site.py +2 -2
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/inline.py +0 -10
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/playwright_handle.py +9 -8
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session.py +2 -43
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session_create.py +0 -34
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session_registry.py +7 -11
- browserwright-0.7.4/src/browserwright/session_runtime.py +386 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/skill_doc.py +0 -40
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/skill_runtime.md +1 -1
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/task_runner.py +2 -2
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/PKG-INFO +2 -2
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/SOURCES.txt +3 -13
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/requires.txt +1 -1
- browserwright-0.7.2/src/browserwright/daemon/active_tab.py +0 -183
- browserwright-0.7.2/src/browserwright/daemon/auth.py +0 -395
- browserwright-0.7.2/src/browserwright/daemon/backends/cloud.py +0 -222
- browserwright-0.7.2/src/browserwright/daemon/backends/env.py +0 -119
- browserwright-0.7.2/src/browserwright/daemon/backends/rdp.py +0 -214
- browserwright-0.7.2/src/browserwright/daemon/observability.py +0 -181
- browserwright-0.7.2/src/browserwright/daemon/resolver.py +0 -72
- browserwright-0.7.2/src/browserwright/daemon/server/proxy.py +0 -2108
- browserwright-0.7.2/src/browserwright/install.py +0 -655
- browserwright-0.7.2/src/browserwright/memory/_yaml.py +0 -217
- browserwright-0.7.2/src/browserwright/memory/repl_mem.py +0 -28
- browserwright-0.7.2/src/browserwright/memory/session_decisions.py +0 -53
- browserwright-0.7.2/src/browserwright/multitask.py +0 -120
- browserwright-0.7.2/src/browserwright/primitives/__init__.py +0 -83
- browserwright-0.7.2/src/browserwright/primitives/inspect.py +0 -887
- browserwright-0.7.2/src/browserwright/primitives/interact.py +0 -518
- browserwright-0.7.2/src/browserwright/primitives/page.py +0 -564
- browserwright-0.7.2/src/browserwright/release_install.py +0 -466
- browserwright-0.7.2/src/browserwright/session_runtime.py +0 -133
- browserwright-0.7.2/src/browserwright/subscriptions.py +0 -213
- {browserwright-0.7.2 → browserwright-0.7.4}/setup.cfg +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/__init__.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/__main__.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/__init__.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/__main__.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/client.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/protocol.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/__init__.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/errors.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/__init__.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/userscripts.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/health.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/_md.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/output_schema.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/primitives/discovery_api.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/primitives/http.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/__init__.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/_namespace.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/_smart_goto.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/snapshot.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session_ctx.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/version.py +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/dependency_links.txt +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/entry_points.txt +0 -0
- {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browserwright
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
|
-
Requires-Dist: cdp-use==1.4.5
|
|
7
6
|
Requires-Dist: websockets==15.0.1
|
|
8
7
|
Requires-Dist: pillow==12.2.0
|
|
9
8
|
Requires-Dist: httpx>=0.27
|
|
10
9
|
Requires-Dist: playwright>=1.60.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
11
11
|
Provides-Extra: ux
|
|
12
12
|
Requires-Dist: rich>=13; extra == "ux"
|
|
@@ -4,21 +4,27 @@ Let an AI/code agent drive a real (or isolated) Chrome from the terminal over CD
|
|
|
4
4
|
|
|
5
5
|
One installable package, two CLIs that work together:
|
|
6
6
|
|
|
7
|
-
- **`browserwright-daemon`** (Layer 1) — resolves a Chrome CDP WebSocket URL and proxies it. Backends: `env / rdp / extension
|
|
7
|
+
- **`browserwright-daemon`** (Layer 1) — resolves a Chrome CDP WebSocket URL and proxies it. Backends: `env / rdp / extension`. Also `launch-chrome` to spawn an isolated Chrome.
|
|
8
8
|
- **`browserwright`** (Layer 2) — the agent-facing surface: sessions, heredoc scripting with pre-imported primitives, reusable site tasks, memory, and userscript management.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
.
|
|
12
12
|
├── src/browserwright/ the package
|
|
13
13
|
│ ├── … Layer 2 — sessions / primitives / site skills / memory / userscripts
|
|
14
|
-
│ └── daemon/ Layer 1 — CDP URL resolver + proxy (env/rdp/extension
|
|
14
|
+
│ └── daemon/ Layer 1 — CDP URL resolver + proxy (env/rdp/extension backends)
|
|
15
15
|
├── chrome-extension/ unpacked relay extension for the `extension` backend
|
|
16
16
|
├── skill/ Agent skill bundle (symlinked into Claude Code, Codex, and Pi)
|
|
17
17
|
├── tests/{skill,daemon}/ test suites
|
|
18
|
-
|
|
19
|
-
└── browser-connection.md why this stack exists (CDP discovery, Chrome 144+ popups)
|
|
18
|
+
└── docs/ deeper docs (architecture.md, daemon.md, session-workspaces.md, …)
|
|
20
19
|
```
|
|
21
20
|
|
|
21
|
+
Contributing or hacking on this repo? Start at **[AGENTS.md](AGENTS.md)** — it
|
|
22
|
+
points to [ONBOARD.md](ONBOARD.md) (clone → install → test loop),
|
|
23
|
+
[docs/architecture.md](docs/architecture.md) (architecture orientation),
|
|
24
|
+
[TESTING.md](TESTING.md) (test-suite map), and [RELEASING.md](RELEASING.md)
|
|
25
|
+
(releases + updating the global install). This README covers the end-user path
|
|
26
|
+
only.
|
|
27
|
+
|
|
22
28
|
## Prerequisites
|
|
23
29
|
|
|
24
30
|
- macOS or Linux
|
|
@@ -96,100 +102,11 @@ browserwright-daemon stop
|
|
|
96
102
|
browserwright-daemon serve
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
###
|
|
100
|
-
|
|
101
|
-
PyPI releases are driven by git tags. For published artifacts, the git tag is
|
|
102
|
-
the source of truth for the package version; the release workflow rewrites
|
|
103
|
-
`pyproject.toml` to that tag version before building the sdist and wheel.
|
|
104
|
-
|
|
105
|
-
After a PR has landed on `main`, publish by pushing a version tag:
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
git checkout main
|
|
109
|
-
git pull
|
|
110
|
-
git tag v0.6.3
|
|
111
|
-
git push origin v0.6.3
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
The GitHub Action only runs for `v*` tags. A tag like `v0.6.3` publishes package
|
|
115
|
-
version `0.6.3`.
|
|
116
|
-
|
|
117
|
-
Each `v*` GitHub Release also includes a
|
|
118
|
-
`browserwright-extension-<version>.zip` asset. Use that zip as the official
|
|
119
|
-
extension download path when installing outside PyPI or before the Chrome Web
|
|
120
|
-
Store path is available.
|
|
121
|
-
|
|
122
|
-
### Local development link
|
|
123
|
-
|
|
124
|
-
For development only, `mise run dev-link` links the current checkout into
|
|
125
|
-
global PATH and skill dirs. That is convenient for debugging but can break
|
|
126
|
-
global agents if the checkout is broken.
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
mise run dev-link
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The skill bundle is intentionally a thin shell. The authoritative agent
|
|
133
|
-
instructions come from `browserwright --print-skill`, so installed agents read
|
|
134
|
-
docs generated by the installed CLI.
|
|
135
|
-
|
|
136
|
-
## Update the local global install
|
|
137
|
-
|
|
138
|
-
Use this flow when you want the global Code Agent install on this machine to
|
|
139
|
-
pick up the latest published release:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
# 1. Install/upgrade browserwright from PyPI.
|
|
143
|
-
# 2. Download the matching GitHub Release extension artifact.
|
|
144
|
-
# 3. Restart the daemon, reload the unpacked extension, and verify versions.
|
|
145
|
-
mise run upgrade-global
|
|
146
|
-
|
|
147
|
-
# 4. Optional explicit verification.
|
|
148
|
-
browserwright version check
|
|
149
|
-
browserwright-daemon version check
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
If the daemon is running manually instead of as the macOS LaunchAgent, restart it manually:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
browserwright-daemon stop
|
|
156
|
-
browserwright-daemon serve
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
`upgrade-global` unpacks the GitHub Release asset into the stable local load
|
|
160
|
-
path:
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
/Users/metajs/Library/Mobile Documents/com~apple~CloudDocs/etc/chrome-extension/browserwright
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
If the task reports that the extension changed, it restarts the daemon, runs
|
|
167
|
-
`browserwright-daemon extension reload`, and waits for Chrome to reconnect with
|
|
168
|
-
the new extension version. The stable path does not change across releases; the
|
|
169
|
-
task overwrites its contents. If the reload cannot be confirmed, fall back to
|
|
170
|
-
Chrome's manual reload button in `chrome://extensions/`.
|
|
171
|
-
Reload any existing tab that already shows a duplicated `👀` attach marker so
|
|
172
|
-
the extension can normalize the title marker after the upgrade.
|
|
105
|
+
### Releases, upgrades, and local development
|
|
173
106
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
writes that version into `pyproject.toml` before building. The release workflow
|
|
178
|
-
also writes the same tag version into `chrome-extension/manifest.json` before
|
|
179
|
-
uploading `browserwright-extension-<version>.zip` to the GitHub Release.
|
|
180
|
-
|
|
181
|
-
After installing a release:
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
browserwright version check
|
|
185
|
-
browserwright-daemon version check
|
|
186
|
-
browserwright-daemon restart # when installed as the macOS LaunchAgent
|
|
187
|
-
browserwright-daemon extension reload
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
The extension reload command asks connected unpacked extensions to call
|
|
191
|
-
`chrome.runtime.reload()`. Reload is immediate; the extension reconnect loop
|
|
192
|
-
restores the relay connection after the service worker restarts.
|
|
107
|
+
Cutting a release, updating a machine's global install (`mise run
|
|
108
|
+
upgrade-global`), the extension reload flow, and the dev-link workflow are all
|
|
109
|
+
documented in **[RELEASING.md](RELEASING.md)** and **[ONBOARD.md](ONBOARD.md)**.
|
|
193
110
|
|
|
194
111
|
## Smoke test
|
|
195
112
|
|
|
@@ -254,7 +171,6 @@ browserwright task wikipedia.org/lookup --title="Wikipedia"
|
|
|
254
171
|
| Scripts / iterative work in throwaway profiles | `rdp` + isolated Chrome | `browserwright-daemon launch-chrome --port 9333 --profile bs-dev` + `BD_PORT=9333 BD_BACKEND=rdp` |
|
|
255
172
|
| Fingerprint browser (AdsPower / MultiLogin / 比特浏览器) | `rdp` | point `BD_PORT` at the tool's exposed port |
|
|
256
173
|
| Externally-owned CDP endpoint (anti-detect profile, e.g. CloakBrowser) | `env` | `BD_CDP_WS=ws://… browserwright-daemon serve --backend env`, then `browserwright session new --backend=env --name=…` (attach-owned — never closed on `session end`) |
|
|
257
|
-
| Remote Chrome (Browser Use / Browserless / Hyperbrowser) | `cloud` | `browserwright-daemon serve --provider <name>` + auth env vars |
|
|
258
174
|
|
|
259
175
|
Interactive wizard: `browserwright install` — walks the decision tree and writes your pick.
|
|
260
176
|
|
|
@@ -263,6 +179,16 @@ external profiles with N isolated daemons — each with its own `XDG_RUNTIME_DIR
|
|
|
263
179
|
(distinct socket), `--facade-port`, and `BD_CDP_WS`, one `env` session apiece.
|
|
264
180
|
See [docs/session-workspaces.md](docs/session-workspaces.md) §"Env Backend".
|
|
265
181
|
|
|
182
|
+
**Reaching the facade from another machine (Tailscale/LAN):** the Playwright
|
|
183
|
+
facade binds `127.0.0.1` by default and is *never* exposed off-box unless you
|
|
184
|
+
opt in. Pass `--facade-host <tailnet-ip>` (or `BD_FACADE_HOST` / `facade_host`
|
|
185
|
+
in config.toml; `0.0.0.0` to bind all interfaces) and a remote client can
|
|
186
|
+
`connect_over_cdp("http://<tailnet-ip>:19990/cdp")` — the facade's
|
|
187
|
+
`/json/version` bootstrap rewrites the advertised `webSocketDebuggerUrl` from
|
|
188
|
+
the request's `Host` header, so the ws URL points back at the address the
|
|
189
|
+
client actually used. No auth is added, so only bind an interface you trust
|
|
190
|
+
(a Tailscale IP is private to your tailnet).
|
|
191
|
+
|
|
266
192
|
### Userscripts
|
|
267
193
|
|
|
268
194
|
Author Tampermonkey-style scripts the `extension` backend injects on matching sites:
|
|
@@ -297,7 +223,7 @@ Non-browser helpers (also pre-imported):
|
|
|
297
223
|
|
|
298
224
|
- **HTTP (no browser, for static pages):** `http_get(url)`
|
|
299
225
|
- **Memory:** `remember`, `remember_global`, `remember_preference`, `memory_read`
|
|
300
|
-
- **Site-skills / tasks:** `list_site_skills`, `load_site_skill`, `run_task`, `
|
|
226
|
+
- **Site-skills / tasks:** `list_site_skills`, `load_site_skill`, `run_task`, `bootstrap_site`
|
|
301
227
|
|
|
302
228
|
Full catalogue and guidance in `skill/SKILL.md`.
|
|
303
229
|
|
|
@@ -305,9 +231,8 @@ Full catalogue and guidance in `skill/SKILL.md`.
|
|
|
305
231
|
|
|
306
232
|
```bash
|
|
307
233
|
browserwright-daemon doctor # which backends are live, why each is/isn't usable
|
|
308
|
-
browserwright-daemon
|
|
234
|
+
browserwright-daemon status --json # daemon liveness + endpoint + facade port
|
|
309
235
|
browserwright doctor # skill-side health
|
|
310
|
-
browserwright-daemon stats # observability counters when `serve` is running
|
|
311
236
|
```
|
|
312
237
|
|
|
313
238
|
## Agent integrations
|
|
@@ -327,12 +252,13 @@ rm -rf ~/.cache/browserwright-daemon ~/.browserwright
|
|
|
327
252
|
|
|
328
253
|
## Further reading
|
|
329
254
|
|
|
330
|
-
- `
|
|
331
|
-
- `
|
|
255
|
+
- `AGENTS.md` — entry point for contributors and code agents
|
|
256
|
+
- `docs/architecture.md` — architecture tour + isolated local dev
|
|
257
|
+
- `docs/session-workspaces.md` — the session workspace / isolation model
|
|
332
258
|
- `docs/daemon.md` — backend internals, env vars, `config.toml`
|
|
333
|
-
- `docs/skill.md` —
|
|
334
|
-
- `
|
|
335
|
-
- `
|
|
259
|
+
- `docs/skill.md` — the Layer 2 agent-facing surface
|
|
260
|
+
- `TESTING.md` — map of the test suites and how to run them
|
|
261
|
+
- `docs/archive/browser-connection.md` — *why* this stack exists (CDP discovery paths, Chrome 144+ popup mechanics)
|
|
336
262
|
|
|
337
263
|
## License
|
|
338
264
|
|
|
@@ -4,11 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "browserwright"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.4"
|
|
8
8
|
description = "Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends)."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"cdp-use==1.4.5",
|
|
12
11
|
"websockets==15.0.1",
|
|
13
12
|
"pillow==12.2.0",
|
|
14
13
|
"httpx>=0.27",
|
|
@@ -18,6 +17,7 @@ dependencies = [
|
|
|
18
17
|
# for tests that launch a browser; the daemon-facade path drives the
|
|
19
18
|
# daemon-resolved Chrome and does not need Playwright's bundled browser.
|
|
20
19
|
"playwright>=1.60.0",
|
|
20
|
+
"pyyaml>=6.0.3",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.optional-dependencies]
|
|
@@ -192,7 +192,7 @@ class _Worker:
|
|
|
192
192
|
Reuses the shared connect+bind free functions (single source of truth
|
|
193
193
|
for the FATAL "no Playwright CDP session over the extension facade"
|
|
194
194
|
constraint). Bind the session FIRST so ``current_session()`` /
|
|
195
|
-
``
|
|
195
|
+
``resolve_current_target()`` resolve the right ledger record."""
|
|
196
196
|
if self._connected:
|
|
197
197
|
return
|
|
198
198
|
from ..session import Session, set_session
|
|
@@ -5,23 +5,20 @@ this module. Keeping the list in one place means an agent who imports
|
|
|
5
5
|
``browserwright`` directly from a saved task gets the same names the REPL
|
|
6
6
|
gave them.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
The legacy CDP browser-driving primitives (``open``/``goto_url``/
|
|
9
|
+
``click_at_xy``/``js``/``cdp``/``capture_screenshot``/``snapshot``/… — the
|
|
10
|
+
whole page/tab interaction surface) are DELETED. The agent drives the browser
|
|
11
|
+
with **real Playwright** via the injected ``page`` / ``context`` (bound to the
|
|
12
|
+
session's current tab, reused across heredocs) and observes with
|
|
13
|
+
``snapshot()`` (a first-party AI aria snapshot whose ``[ref=eN]`` refs feed
|
|
14
|
+
``page.locator("aria-ref=eN")``). Those three names are injected per-heredoc
|
|
15
|
+
by ``repl/_namespace.build_globals``, NOT exported here. The internal tab
|
|
16
|
+
lifecycle the Playwright binding glue relies on lives in
|
|
17
|
+
``browserwright.session_runtime``.
|
|
17
18
|
|
|
18
19
|
What remains in EXPORTS is the set of NON-browser-driving helpers that do not
|
|
19
20
|
overlap Playwright: ``http_get`` (no-browser escape hatch), the memory verbs,
|
|
20
|
-
and the site-skill / task layer.
|
|
21
|
-
``primitives/`` still define the old functions (``current_page``, ``list_tabs``,
|
|
22
|
-
the daemon-driving glue, …); they are kept as INTERNAL functions the Phase C
|
|
23
|
-
binding glue (``repl/playwright_handle.py``) and the memory/site helpers rely
|
|
24
|
-
on — they are simply no longer part of the agent-callable surface.
|
|
21
|
+
and the site-skill / task layer.
|
|
25
22
|
"""
|
|
26
23
|
from .errors import (
|
|
27
24
|
AuthWall,
|
|
@@ -34,7 +31,6 @@ from .errors import (
|
|
|
34
31
|
NetworkError,
|
|
35
32
|
PageLoadFailed,
|
|
36
33
|
)
|
|
37
|
-
from .multitask import run_tasks_concurrent
|
|
38
34
|
from .primitives import (
|
|
39
35
|
bootstrap_site,
|
|
40
36
|
http_get,
|
|
@@ -53,10 +49,9 @@ EXPORTS = [
|
|
|
53
49
|
# memory + site
|
|
54
50
|
"bootstrap_site", "remember", "remember_global", "remember_preference",
|
|
55
51
|
"memory_read",
|
|
56
|
-
# task
|
|
52
|
+
# task (site-skills run on the Playwright surface — see
|
|
57
53
|
# task_runner.run_task, which injects page/context into the task module)
|
|
58
54
|
"list_site_skills", "load_site_skill", "run_task",
|
|
59
|
-
"run_tasks_concurrent",
|
|
60
55
|
# errors
|
|
61
56
|
"BrowserwrightError", "PageLoadFailed", "ElementNotFound", "AuthWall",
|
|
62
57
|
"Captcha", "NetworkError", "DaemonUnavailable", "CDPError",
|
|
@@ -6,21 +6,17 @@ Design:
|
|
|
6
6
|
no session for ``Target.*`` etc.
|
|
7
7
|
- Auto-attach to a tab on demand via ``attach(targetId)``; the resulting
|
|
8
8
|
session id is cached so subsequent calls reuse it.
|
|
9
|
-
- Events for the attached session are stashed in a per-session ring buffer
|
|
10
|
-
and exposed via ``drain_events()``.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
wrappers").
|
|
10
|
+
The whole client is < 200 lines of plain websockets — easier to reason
|
|
11
|
+
about, easier to unit test, and we never need typed wrappers (spec §3
|
|
12
|
+
"raw CDP strings over typed wrappers").
|
|
16
13
|
"""
|
|
17
14
|
from __future__ import annotations
|
|
18
15
|
|
|
19
16
|
import json
|
|
20
17
|
import threading
|
|
21
18
|
import time
|
|
22
|
-
from
|
|
23
|
-
from typing import Any, Optional
|
|
19
|
+
from typing import Optional
|
|
24
20
|
|
|
25
21
|
from websockets.exceptions import ConnectionClosed
|
|
26
22
|
from websockets.sync.client import connect as ws_connect
|
|
@@ -28,9 +24,6 @@ from websockets.sync.client import connect as ws_connect
|
|
|
28
24
|
from .errors import CDPError
|
|
29
25
|
|
|
30
26
|
|
|
31
|
-
_EVENT_RING_LIMIT = 1024
|
|
32
|
-
|
|
33
|
-
|
|
34
27
|
class _UnixSocketAdapter:
|
|
35
28
|
"""Wrap an ``AF_UNIX`` socket so ``setsockopt(IPPROTO_TCP, ...)`` becomes
|
|
36
29
|
a no-op. websockets unconditionally calls
|
|
@@ -113,7 +106,7 @@ class CDPSession:
|
|
|
113
106
|
|
|
114
107
|
All sends are synchronous: send → block on response with matching id →
|
|
115
108
|
return result. Events arrive on the same socket; the reader thread
|
|
116
|
-
|
|
109
|
+
discards them (nothing downstream consumes raw CDP events).
|
|
117
110
|
"""
|
|
118
111
|
|
|
119
112
|
def __init__(self, ws_url: str, connect_timeout: float = 8.0):
|
|
@@ -145,7 +138,6 @@ class CDPSession:
|
|
|
145
138
|
self._next_id = 1
|
|
146
139
|
self._inflight: dict[int, dict] = {}
|
|
147
140
|
self._inflight_cv = threading.Condition(self._lock)
|
|
148
|
-
self._events: dict[Optional[str], deque] = {None: deque(maxlen=_EVENT_RING_LIMIT)}
|
|
149
141
|
self._closed = False
|
|
150
142
|
self._closed_reason: Optional[str] = None
|
|
151
143
|
self._reader = threading.Thread(target=self._read_loop, name="cdp-reader", daemon=True)
|
|
@@ -201,8 +193,7 @@ class CDPSession:
|
|
|
201
193
|
res = self.send("Target.attachToTarget", targetId=target_id, flatten=True)
|
|
202
194
|
sid = res["sessionId"]
|
|
203
195
|
self._sessions[target_id] = sid
|
|
204
|
-
|
|
205
|
-
# Enable the usual domains so wait_for_load / drain_events have data.
|
|
196
|
+
# Enable the usual domains so wait_for_load has data.
|
|
206
197
|
for domain in ("Page", "Runtime", "DOM", "Network"):
|
|
207
198
|
try:
|
|
208
199
|
self.send(f"{domain}.enable", session=sid)
|
|
@@ -210,49 +201,6 @@ class CDPSession:
|
|
|
210
201
|
pass # Some domains are noop in some Chrome builds.
|
|
211
202
|
return sid
|
|
212
203
|
|
|
213
|
-
def attach_readonly(self, target_id: str) -> str:
|
|
214
|
-
"""Daemon v0.3 H7 shared-read attach.
|
|
215
|
-
|
|
216
|
-
Requests a session via ``flags.allowSecondaryReadOnly=True`` — daemon
|
|
217
|
-
returns a sessionId that receives this target's events but rejects
|
|
218
|
-
any command other than ``Target.detachFromTarget`` (`-32602`). Useful
|
|
219
|
-
for tail-following another agent's session for monitoring / drift
|
|
220
|
-
detection.
|
|
221
|
-
|
|
222
|
-
Note: this opens a *second* session on the same target if some other
|
|
223
|
-
client / process already owns it. If we own it ourselves, prefer
|
|
224
|
-
``attach()``.
|
|
225
|
-
"""
|
|
226
|
-
res = self.send(
|
|
227
|
-
"Target.attachToTarget",
|
|
228
|
-
targetId=target_id,
|
|
229
|
-
flatten=True,
|
|
230
|
-
flags={"allowSecondaryReadOnly": True},
|
|
231
|
-
)
|
|
232
|
-
sid = res["sessionId"]
|
|
233
|
-
self._events.setdefault(sid, deque(maxlen=_EVENT_RING_LIMIT))
|
|
234
|
-
# We deliberately *don't* register sid in ``self._sessions`` — that
|
|
235
|
-
# map tracks owning attachments, and a readonly attachment isn't one.
|
|
236
|
-
return sid
|
|
237
|
-
|
|
238
|
-
def detach(self, target_id: str) -> None:
|
|
239
|
-
sid = self._sessions.pop(target_id, None)
|
|
240
|
-
if sid:
|
|
241
|
-
try:
|
|
242
|
-
self.send("Target.detachFromTarget", sessionId=sid)
|
|
243
|
-
except CDPError:
|
|
244
|
-
pass
|
|
245
|
-
self._events.pop(sid, None)
|
|
246
|
-
|
|
247
|
-
def drain_events(self, session: Optional[str] = None) -> list[dict]:
|
|
248
|
-
buf = self._events.get(session)
|
|
249
|
-
if not buf:
|
|
250
|
-
return []
|
|
251
|
-
with self._lock:
|
|
252
|
-
out = list(buf)
|
|
253
|
-
buf.clear()
|
|
254
|
-
return out
|
|
255
|
-
|
|
256
204
|
def close(self) -> None:
|
|
257
205
|
if self._closed:
|
|
258
206
|
return
|
|
@@ -279,13 +227,7 @@ class CDPSession:
|
|
|
279
227
|
if mid in self._inflight:
|
|
280
228
|
self._inflight[mid] = msg
|
|
281
229
|
self._inflight_cv.notify_all()
|
|
282
|
-
|
|
283
|
-
# Event.
|
|
284
|
-
sid = msg.get("sessionId")
|
|
285
|
-
buf = self._events.get(sid)
|
|
286
|
-
if buf is None:
|
|
287
|
-
buf = self._events.setdefault(sid, deque(maxlen=_EVENT_RING_LIMIT))
|
|
288
|
-
buf.append({"method": msg.get("method"), "params": msg.get("params", {}), "sessionId": sid})
|
|
230
|
+
# Events (no id) are dropped — nothing consumes them.
|
|
289
231
|
except ConnectionClosed as e:
|
|
290
232
|
self._closed_reason = str(e)
|
|
291
233
|
except Exception as e: # noqa: BLE001
|
|
@@ -41,12 +41,6 @@ Usage:
|
|
|
41
41
|
browserwright -s <session-id> task <site>/<name> [--key=value ...] [--isolated]
|
|
42
42
|
browserwright list-tasks [--site SITE] [--query Q] [--json]
|
|
43
43
|
|
|
44
|
-
browserwright sub add <git-url> [--name NAME]
|
|
45
|
-
browserwright sub list [--json]
|
|
46
|
-
browserwright sub update [--name NAME]
|
|
47
|
-
browserwright sub remove --name NAME
|
|
48
|
-
browserwright release {install-local|status|list|activate} ...
|
|
49
|
-
|
|
50
44
|
browserwright install
|
|
51
45
|
browserwright doctor [--json]
|
|
52
46
|
browserwright index rebuild
|
|
@@ -384,76 +378,6 @@ def _cmd_index(args: list[str]) -> int:
|
|
|
384
378
|
return 1
|
|
385
379
|
|
|
386
380
|
|
|
387
|
-
def _cmd_sub(args: list[str]) -> int:
|
|
388
|
-
"""``browserwright sub {add|list|update|remove} ...``."""
|
|
389
|
-
if not args:
|
|
390
|
-
print("usage: browserwright sub {add|list|update|remove} ...", file=sys.stderr)
|
|
391
|
-
return 1
|
|
392
|
-
sub = args[0]
|
|
393
|
-
rest = args[1:]
|
|
394
|
-
from . import subscriptions
|
|
395
|
-
|
|
396
|
-
if sub == "add":
|
|
397
|
-
if not rest or rest[0].startswith("--"):
|
|
398
|
-
print("usage: browserwright sub add <git-url> [--name NAME]", file=sys.stderr)
|
|
399
|
-
return 1
|
|
400
|
-
url = rest[0]
|
|
401
|
-
kw = _parse_kv_args(rest[1:])
|
|
402
|
-
try:
|
|
403
|
-
r = subscriptions.add(url, name=kw.get("name"))
|
|
404
|
-
except subscriptions.SubscriptionError as e:
|
|
405
|
-
print(f"sub add failed: {e}", file=sys.stderr)
|
|
406
|
-
return 1
|
|
407
|
-
if kw.get("json"):
|
|
408
|
-
sys.stdout.write(json.dumps(r, default=str) + "\n")
|
|
409
|
-
else:
|
|
410
|
-
print(f"{r['status']}: {r['name']} → {r['path']}")
|
|
411
|
-
return 0
|
|
412
|
-
|
|
413
|
-
if sub == "list":
|
|
414
|
-
kw = _parse_kv_args(rest)
|
|
415
|
-
rows = subscriptions.list_all()
|
|
416
|
-
if kw.get("json"):
|
|
417
|
-
sys.stdout.write(json.dumps(rows, indent=2, default=str) + "\n")
|
|
418
|
-
return 0
|
|
419
|
-
if not rows:
|
|
420
|
-
print("(no subscriptions)")
|
|
421
|
-
return 0
|
|
422
|
-
for r in rows:
|
|
423
|
-
tag = "" if r["exists"] else " [MISSING]"
|
|
424
|
-
print(f" {r['name']:24s} {r['url']}{tag}")
|
|
425
|
-
return 0
|
|
426
|
-
|
|
427
|
-
if sub == "update":
|
|
428
|
-
kw = _parse_kv_args(rest)
|
|
429
|
-
names = [kw["name"]] if kw.get("name") else None
|
|
430
|
-
try:
|
|
431
|
-
results = subscriptions.update(names)
|
|
432
|
-
except subscriptions.SubscriptionError as e:
|
|
433
|
-
print(f"sub update failed: {e}", file=sys.stderr)
|
|
434
|
-
return 1
|
|
435
|
-
for r in results:
|
|
436
|
-
print(f" {r['name']:24s} {r['status']}: {r.get('detail','')}")
|
|
437
|
-
return 0 if all(r["status"] in ("updated", "missing") for r in results) else 1
|
|
438
|
-
|
|
439
|
-
if sub == "remove":
|
|
440
|
-
kw = _parse_kv_args(rest)
|
|
441
|
-
name = kw.get("name")
|
|
442
|
-
if not name:
|
|
443
|
-
print("usage: browserwright sub remove --name NAME", file=sys.stderr)
|
|
444
|
-
return 1
|
|
445
|
-
try:
|
|
446
|
-
subscriptions.remove(name)
|
|
447
|
-
except subscriptions.SubscriptionError as e:
|
|
448
|
-
print(f"sub remove failed: {e}", file=sys.stderr)
|
|
449
|
-
return 1
|
|
450
|
-
print(f"removed {name}")
|
|
451
|
-
return 0
|
|
452
|
-
|
|
453
|
-
print(f"unknown sub subcommand: {sub}", file=sys.stderr)
|
|
454
|
-
return 1
|
|
455
|
-
|
|
456
|
-
|
|
457
381
|
def _cmd_memory(args: list[str]) -> int:
|
|
458
382
|
if not args:
|
|
459
383
|
print("usage: browserwright memory {show|forget|replace} ...", file=sys.stderr)
|
|
@@ -623,6 +547,16 @@ def _cmd_session(args: list[str], *, session_id: Optional[str] = None) -> int:
|
|
|
623
547
|
return 1
|
|
624
548
|
|
|
625
549
|
|
|
550
|
+
def _fresh_screenshot_path() -> str:
|
|
551
|
+
"""A non-colliding /tmp png path for the userscript --verify screenshot."""
|
|
552
|
+
i = 0
|
|
553
|
+
while True:
|
|
554
|
+
cand = Path("/tmp") / f"browserwright-shot-{os.getpid()}-{i}.png"
|
|
555
|
+
if not cand.exists():
|
|
556
|
+
return str(cand)
|
|
557
|
+
i += 1
|
|
558
|
+
|
|
559
|
+
|
|
626
560
|
def _cmd_userscript(args: list[str], *, session_id: Optional[str] = None) -> int:
|
|
627
561
|
if not args or args[0] in {"-h", "--help"}:
|
|
628
562
|
sys.stdout.write(USERSCRIPT_HELP)
|
|
@@ -661,14 +595,20 @@ def _cmd_userscript(args: list[str], *, session_id: Optional[str] = None) -> int
|
|
|
661
595
|
raise RuntimeError(
|
|
662
596
|
"no drivable session bound; pass -s <id> or set BD_SESSION"
|
|
663
597
|
)
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
#
|
|
667
|
-
from .
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
598
|
+
# Drive the verify through the same Playwright path the agent
|
|
599
|
+
# uses (the legacy CDP primitives are gone): bind the session's
|
|
600
|
+
# current tab, reload it, screenshot it, print the path.
|
|
601
|
+
from .repl.playwright_handle import PlaywrightHandle
|
|
602
|
+
|
|
603
|
+
handle = PlaywrightHandle()
|
|
604
|
+
try:
|
|
605
|
+
page = handle.page
|
|
606
|
+
page.reload(wait_until="load")
|
|
607
|
+
shot = _fresh_screenshot_path()
|
|
608
|
+
page.screenshot(path=shot)
|
|
609
|
+
print(shot)
|
|
610
|
+
finally:
|
|
611
|
+
handle.close()
|
|
672
612
|
except Exception as e:
|
|
673
613
|
print(f"pushed OK — --verify skipped (no drivable tab): {e}",
|
|
674
614
|
file=sys.stderr)
|
|
@@ -765,88 +705,6 @@ def _extension_relay_status() -> dict | None:
|
|
|
765
705
|
return None
|
|
766
706
|
|
|
767
707
|
|
|
768
|
-
def _cmd_release(args: list[str]) -> int:
|
|
769
|
-
if not args:
|
|
770
|
-
print(
|
|
771
|
-
"usage: browserwright release {install-local|status|list|activate} ...",
|
|
772
|
-
file=sys.stderr,
|
|
773
|
-
)
|
|
774
|
-
return 1
|
|
775
|
-
from . import release_install
|
|
776
|
-
|
|
777
|
-
sub = args[0]
|
|
778
|
-
rest = args[1:]
|
|
779
|
-
kw = _parse_kv_args(rest)
|
|
780
|
-
try:
|
|
781
|
-
if sub == "install-local":
|
|
782
|
-
info = release_install.install_local(
|
|
783
|
-
force=bool(kw.get("force")),
|
|
784
|
-
activate_release=not bool(kw.get("no-activate")),
|
|
785
|
-
)
|
|
786
|
-
if kw.get("restart-daemon") and info["actions"].get("restart_daemon"):
|
|
787
|
-
subprocess.run(["browserwright-daemon", "restart"], check=False)
|
|
788
|
-
if kw.get("json"):
|
|
789
|
-
sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
|
|
790
|
-
else:
|
|
791
|
-
print(f"installed browserwright {info['version']} -> {info['path']}")
|
|
792
|
-
if info.get("activated"):
|
|
793
|
-
print("activated global entry points")
|
|
794
|
-
if info["actions"].get("restart_daemon"):
|
|
795
|
-
print("next: restart daemon (`browserwright-daemon restart`)")
|
|
796
|
-
if info["actions"].get("reload_chrome_extension"):
|
|
797
|
-
print(
|
|
798
|
-
"next: reload Chrome unpacked extension from "
|
|
799
|
-
f"{info.get('chrome_extension_sync', {}).get('path') or info['path'] + '/chrome-extension'}"
|
|
800
|
-
)
|
|
801
|
-
return 0
|
|
802
|
-
|
|
803
|
-
if sub == "status":
|
|
804
|
-
info = release_install.status()
|
|
805
|
-
if kw.get("json"):
|
|
806
|
-
sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
|
|
807
|
-
return 0
|
|
808
|
-
version = info.get("installed_version") or "(none)"
|
|
809
|
-
print(f"installed release: {version}")
|
|
810
|
-
daemon = info.get("daemon") or {}
|
|
811
|
-
daemon_version = daemon.get("version") or "(not running)"
|
|
812
|
-
suffix = " restart required" if daemon.get("restart_required") else ""
|
|
813
|
-
print(f"running daemon: {daemon_version}{suffix}")
|
|
814
|
-
ok = all(row.get("ok") for row in info.get("skill", []))
|
|
815
|
-
print(f"skill install: {'copied ok' if ok else 'needs reinstall'}")
|
|
816
|
-
return 0
|
|
817
|
-
|
|
818
|
-
if sub == "list":
|
|
819
|
-
rows = release_install.list_releases()
|
|
820
|
-
if kw.get("json"):
|
|
821
|
-
sys.stdout.write(json.dumps(rows, indent=2, sort_keys=True) + "\n")
|
|
822
|
-
return 0
|
|
823
|
-
if not rows:
|
|
824
|
-
print("(no releases installed)")
|
|
825
|
-
return 0
|
|
826
|
-
for row in rows:
|
|
827
|
-
mark = "*" if row.get("active") else " "
|
|
828
|
-
print(f"{mark} {row.get('version')} {row.get('path')}")
|
|
829
|
-
return 0
|
|
830
|
-
|
|
831
|
-
if sub == "activate":
|
|
832
|
-
if not rest or rest[0].startswith("--"):
|
|
833
|
-
print("usage: browserwright release activate <version>", file=sys.stderr)
|
|
834
|
-
return 1
|
|
835
|
-
info = release_install.activate(rest[0])
|
|
836
|
-
if kw.get("json"):
|
|
837
|
-
sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
|
|
838
|
-
else:
|
|
839
|
-
print(f"activated browserwright {info['version']} -> {info['path']}")
|
|
840
|
-
print("next: restart daemon and reload Chrome extension if that release differs")
|
|
841
|
-
return 0
|
|
842
|
-
except release_install.ReleaseError as e:
|
|
843
|
-
print(f"release error: {e}", file=sys.stderr)
|
|
844
|
-
return 1
|
|
845
|
-
|
|
846
|
-
print(f"unknown release subcommand: {sub}", file=sys.stderr)
|
|
847
|
-
return 1
|
|
848
|
-
|
|
849
|
-
|
|
850
708
|
def main(argv: Optional[list[str]] = None) -> None:
|
|
851
709
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
852
710
|
|
|
@@ -897,10 +755,6 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|
|
897
755
|
sys.exit(_cmd_index(rest))
|
|
898
756
|
if cmd == "memory":
|
|
899
757
|
sys.exit(_cmd_memory(rest))
|
|
900
|
-
if cmd == "sub":
|
|
901
|
-
sys.exit(_cmd_sub(rest))
|
|
902
|
-
if cmd == "release":
|
|
903
|
-
sys.exit(_cmd_release(rest))
|
|
904
758
|
if cmd == "session":
|
|
905
759
|
sys.exit(_cmd_session(rest, session_id=global_session))
|
|
906
760
|
if cmd == "whoami":
|