browserctl 0.4.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +97 -55
- data/bin/browserctl +117 -108
- data/bin/browserd +9 -3
- data/bin/setup +7 -3
- data/examples/cloudflare_hitl.rb +6 -6
- data/examples/smoke/params_file.rb +3 -2
- data/examples/smoke/store_fetch.rb +5 -5
- data/examples/test_automation_practices/checkboxes.rb +39 -0
- data/examples/test_automation_practices/dynamic_elements.rb +40 -0
- data/examples/test_automation_practices/key_press.rb +41 -0
- data/examples/test_automation_practices/login.rb +34 -0
- data/examples/test_automation_practices/login_negative.rb +28 -0
- data/examples/test_automation_practices/notifications.rb +57 -0
- data/examples/the_internet/add_remove_elements.rb +1 -1
- data/examples/the_internet/checkboxes.rb +1 -1
- data/examples/the_internet/dropdown.rb +1 -1
- data/examples/the_internet/dynamic_loading.rb +2 -2
- data/examples/the_internet/login.rb +1 -1
- data/lib/browserctl/client.rb +112 -28
- data/lib/browserctl/commands/cookie.rb +59 -0
- data/lib/browserctl/commands/daemon.rb +77 -0
- data/lib/browserctl/commands/page.rb +47 -0
- data/lib/browserctl/commands/record.rb +1 -1
- data/lib/browserctl/commands/screenshot.rb +2 -2
- data/lib/browserctl/commands/session.rb +69 -0
- data/lib/browserctl/commands/snapshot.rb +5 -5
- data/lib/browserctl/commands/storage.rb +67 -0
- data/lib/browserctl/commands/workflow.rb +64 -0
- data/lib/browserctl/constants.rb +20 -1
- data/lib/browserctl/detectors.rb +23 -0
- data/lib/browserctl/errors.rb +25 -0
- data/lib/browserctl/logger.rb +4 -4
- data/lib/browserctl/policy.rb +36 -0
- data/lib/browserctl/recording.rb +4 -4
- data/lib/browserctl/runner.rb +4 -4
- data/lib/browserctl/server/command_dispatcher.rb +49 -258
- data/lib/browserctl/server/handlers/cookies.rb +57 -0
- data/lib/browserctl/server/handlers/daemon_control.rb +29 -0
- data/lib/browserctl/server/handlers/devtools.rb +22 -0
- data/lib/browserctl/server/handlers/hitl.rb +31 -0
- data/lib/browserctl/server/handlers/navigation.rb +94 -0
- data/lib/browserctl/server/handlers/observation.rb +87 -0
- data/lib/browserctl/server/handlers/page_lifecycle.rb +36 -0
- data/lib/browserctl/server/handlers/session.rb +93 -0
- data/lib/browserctl/server/handlers/storage.rb +109 -0
- data/lib/browserctl/server.rb +4 -3
- data/lib/browserctl/session.rb +79 -0
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow.rb +58 -17
- data/lib/browserctl.rb +12 -2
- metadata +43 -11
- data/lib/browserctl/commands/export_cookies.rb +0 -18
- data/lib/browserctl/commands/import_cookies.rb +0 -23
- data/lib/browserctl/commands/inspect.rb +0 -21
- data/lib/browserctl/commands/open_page.rb +0 -21
- data/lib/browserctl/commands/pause.rb +0 -22
- data/lib/browserctl/commands/status.rb +0 -30
- data/lib/browserctl/commands/watch.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37a7ccbe134ac1d42b514959c3db2ed17fd0229fdaed839842cf0af4a872d132
|
|
4
|
+
data.tar.gz: cea2a653eab69d4f544612122deb1bffe7827fca546b74c6f17f5fa8a16c6e8d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bcf1b0d2200b774305555ec8d777d243556b4cb02c3aa7fc015392e6adec331613d17b020b0933212983914a8920beeab511b4dddadab7a2ee45f163897b84b
|
|
7
|
+
data.tar.gz: 551860c7cefd52148d764146e98083deb5c2cdea07e67fe458e33abf420c05a26e2783e763839e3bdd9011a0668a8fb1819f9284c97a47e746057afc3afeb6c3
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,51 @@ All notable changes to this project will be documented in this file.
|
|
|
10
10
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
11
11
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
12
12
|
|
|
13
|
+
## [0.6.0](https://github.com/patrick204nqh/browserctl/compare/v0.5.0...v0.6.0) (2026-04-28)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* add feedback capture guidelines and update .gitignore to include feedback directory ([03a436d](https://github.com/patrick204nqh/browserctl/commit/03a436d69d37826a6c24662b70d6852eac78aa54))
|
|
19
|
+
* **v0.6:** CLI redesign — noun-verb commands, storage/session, daemon auto-index ([#41](https://github.com/patrick204nqh/browserctl/issues/41)) ([b40040f](https://github.com/patrick204nqh/browserctl/commit/b40040f6fba7b78791e845e55af156be46bde3c3))
|
|
20
|
+
* **v0.6:** page_focus command, integration specs, and doc polish ([#42](https://github.com/patrick204nqh/browserctl/issues/42)) ([8cf006d](https://github.com/patrick204nqh/browserctl/commit/8cf006d0e3698bb0698c59d7af5b87f72a3cd154))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* correct initial checkbox state assertion to [false, true, false] ([#33](https://github.com/patrick204nqh/browserctl/issues/33)) ([5ab0fe4](https://github.com/patrick204nqh/browserctl/commit/5ab0fe4fbe5d5f244c795235f0531e0c970d75c7))
|
|
26
|
+
* dismiss on-load notifications before asserting counts ([#35](https://github.com/patrick204nqh/browserctl/issues/35)) ([b508ba6](https://github.com/patrick204nqh/browserctl/commit/b508ba6748d245526f6321a731e6a7bf4ad5cc86))
|
|
27
|
+
* fill replaces existing input value instead of appending ([#29](https://github.com/patrick204nqh/browserctl/issues/29)) ([c7abb48](https://github.com/patrick204nqh/browserctl/commit/c7abb48153e1c64fcd73e22a5bf376a98555f68c))
|
|
28
|
+
* notifications workflow — exclude container from counts, use evaluate for dismiss ([#34](https://github.com/patrick204nqh/browserctl/issues/34)) ([de43301](https://github.com/patrick204nqh/browserctl/commit/de43301942eb7a6335bc9fc7130c22ecf3dd8b38))
|
|
29
|
+
* README & CONTRIBUTING polish + Rakefile v0.6 CI fix ([#43](https://github.com/patrick204nqh/browserctl/issues/43)) ([422c614](https://github.com/patrick204nqh/browserctl/commit/422c6141035d47bb3fd4bc86f732029b44e35206))
|
|
30
|
+
* replace all remaining stale v0.5 CLI commands with v0.6 equivalents ([#45](https://github.com/patrick204nqh/browserctl/issues/45)) ([8d00923](https://github.com/patrick204nqh/browserctl/commit/8d0092380bbc7890761e6d8f5da7864358ce88f7))
|
|
31
|
+
* update demo assets installation and improve login tape requirements ([cf02e3a](https://github.com/patrick204nqh/browserctl/commit/cf02e3ad11fc99f1483904e396160d4345363e17))
|
|
32
|
+
* update login demo tape to use data-test selectors for input fields and buttons ([016fcda](https://github.com/patrick204nqh/browserctl/commit/016fcdaf77ce542d06efe898312063581fc3fffe))
|
|
33
|
+
* update stale `browserctl run` references to `browserctl workflow run` ([#44](https://github.com/patrick204nqh/browserctl/issues/44)) ([7f7005a](https://github.com/patrick204nqh/browserctl/commit/7f7005ac6fd6517c8751cf808378233343e9ace8))
|
|
34
|
+
* use baseline delta assertions instead of absolute notification counts ([#37](https://github.com/patrick204nqh/browserctl/issues/37)) ([33ac119](https://github.com/patrick204nqh/browserctl/commit/33ac119bbb43c0290fe03da06ca3f8545095f7a2))
|
|
35
|
+
* wait for notification-container before dismissing on-load toasts ([#36](https://github.com/patrick204nqh/browserctl/issues/36)) ([883cdf8](https://github.com/patrick204nqh/browserctl/commit/883cdf839bcef2d57bcb4764ff89862aa1969486))
|
|
36
|
+
|
|
37
|
+
## [0.5.0](https://github.com/patrick204nqh/browserctl/compare/v0.4.0...v0.5.0) (2026-04-25)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Features
|
|
41
|
+
|
|
42
|
+
* add cookie export/import commands and refine interaction guidance ([1dc8b2c](https://github.com/patrick204nqh/browserctl/commit/1dc8b2c4c744a5f0930c28d3bcf93fd017c368b1))
|
|
43
|
+
* rename snapshot format 'ai' to 'elements' ([#22](https://github.com/patrick204nqh/browserctl/issues/22)) ([9fde6af](https://github.com/patrick204nqh/browserctl/commit/9fde6afb9e53b9556c84b4c24987777a5c266adf))
|
|
44
|
+
* v0.5 architecture & protocol lock ([#20](https://github.com/patrick204nqh/browserctl/issues/20)) ([1224f2f](https://github.com/patrick204nqh/browserctl/commit/1224f2fe2fb05119053831e7901b286fe93ad4fc))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### Bug Fixes
|
|
48
|
+
|
|
49
|
+
* add rake as development dependency ([13902d8](https://github.com/patrick204nqh/browserctl/commit/13902d81fdb986c6ca754fcb16ce61ee850e27ce))
|
|
50
|
+
* improve browser GIF quality and fix terminal font rendering ([17afdb2](https://github.com/patrick204nqh/browserctl/commit/17afdb210916d8ebdabd237130da1fc534cdbd3e))
|
|
51
|
+
* open PR for demo assets instead of pushing directly to main ([f051316](https://github.com/patrick204nqh/browserctl/commit/f051316962d308419c08f60550cfb9910b148f14))
|
|
52
|
+
* quote filtergraph and add -update 1 for palette in browser GIF pipeline ([053d074](https://github.com/patrick204nqh/browserctl/commit/053d074b5fdd3e0ea6fc51589593d72d5f7fcd74))
|
|
53
|
+
* replace undefined REGISTRY constant with Browserctl.registry_snapshot ([d19a6bf](https://github.com/patrick204nqh/browserctl/commit/d19a6bf7ded8a5211f5b2a75511a9532f94e3845))
|
|
54
|
+
* update README for improved clarity and add Quick Start section ([881d914](https://github.com/patrick204nqh/browserctl/commit/881d914c2bf46625ee4ff5c1ca8ef21b462c533d))
|
|
55
|
+
* use app-slug output instead of gh api /app in assets workflow ([ae6a0f8](https://github.com/patrick204nqh/browserctl/commit/ae6a0f80ba2b66c76d681b1f258de5d72bc34c72))
|
|
56
|
+
* use CSS selectors for browser GIF and add full login flow frames ([717b86b](https://github.com/patrick204nqh/browserctl/commit/717b86befa9af6bab1823f984668ebdc29f2d7f8))
|
|
57
|
+
|
|
13
58
|
## [0.4.0](https://github.com/patrick204nqh/browserctl/compare/v0.3.1...v0.4.0) (2026-04-25)
|
|
14
59
|
|
|
15
60
|
|
data/README.md
CHANGED
|
@@ -2,27 +2,96 @@
|
|
|
2
2
|
<img src=".github/logo.svg" width="96" height="96" alt="browserctl logo"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<h1 align="center">browserctl</h1>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
A browser daemon that keeps sessions alive between commands — for AI agents and iterative dev workflows.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/patrick204nqh/browserctl/actions/workflows/ci.yml"><img src="https://github.com/patrick204nqh/browserctl/actions/workflows/ci.yml/badge.svg" alt="CI"/></a>
|
|
13
|
+
<a href="https://badge.fury.io/rb/browserctl"><img src="https://badge.fury.io/rb/browserctl.svg" alt="Gem Version"/></a>
|
|
14
|
+
<a href="https://rubygems.org/gems/browserctl"><img src="https://img.shields.io/gem/dt/browserctl" alt="Downloads"/></a>
|
|
15
|
+
</p>
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
---
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
Every browser automation tool restarts the browser when your script ends. That means re-authenticating, re-navigating, re-loading state — on every run. browserctl doesn't restart. The session stays alive between commands, so you pick up exactly where you left off.
|
|
14
20
|
|
|
15
21
|
```bash
|
|
16
22
|
browserd & # start the daemon (headless)
|
|
17
|
-
browserctl open
|
|
18
|
-
browserctl
|
|
19
|
-
browserctl fill
|
|
20
|
-
browserctl click
|
|
21
|
-
browserctl
|
|
23
|
+
browserctl page open main --url https://example.com/login
|
|
24
|
+
browserctl snapshot main # AI-friendly JSON snapshot with ref IDs
|
|
25
|
+
browserctl fill main --ref e1 --value me@example.com # interact by ref, no selectors needed
|
|
26
|
+
browserctl click main --ref e2
|
|
27
|
+
browserctl daemon stop
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## See it in action
|
|
33
|
+
|
|
34
|
+
<table align="center"><tr>
|
|
35
|
+
<td align="center" width="50%">
|
|
36
|
+
|
|
37
|
+
**Terminal**<br/>
|
|
38
|
+
<sub>CLI commands, live output, session persistence proof</sub>
|
|
39
|
+
|
|
40
|
+
<img src="docs/assets/terminal.gif" alt="browserctl terminal demo"/>
|
|
41
|
+
|
|
42
|
+
</td>
|
|
43
|
+
<td align="center" width="50%">
|
|
44
|
+
|
|
45
|
+
**Browser**<br/>
|
|
46
|
+
<sub>What the browser sees as those commands run</sub>
|
|
47
|
+
|
|
48
|
+
<img src="docs/assets/browser_demo.gif" alt="browserctl browser demo"/>
|
|
49
|
+
|
|
50
|
+
</td>
|
|
51
|
+
</tr></table>
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# 1. Install
|
|
59
|
+
gem install browserctl
|
|
60
|
+
|
|
61
|
+
# 2. Start the daemon
|
|
62
|
+
browserd &
|
|
63
|
+
|
|
64
|
+
# 3. Open a named page
|
|
65
|
+
browserctl page open main --url https://moatazeldebsy.github.io/test-automation-practices/#/auth
|
|
66
|
+
|
|
67
|
+
# 4. Snapshot — returns JSON with a ref ID per interactable element
|
|
68
|
+
browserctl snapshot main
|
|
69
|
+
# → [{"ref":"e1","tag":"input","attrs":{"data-test":"username-input"}}, {"ref":"e2",...}, {"ref":"e3","tag":"button","text":"Login",...}]
|
|
70
|
+
|
|
71
|
+
# 5. Interact using the ref IDs from the snapshot
|
|
72
|
+
browserctl fill main --ref e1 --value admin
|
|
73
|
+
browserctl fill main --ref e2 --value admin
|
|
74
|
+
browserctl click main --ref e3
|
|
75
|
+
|
|
76
|
+
# 6. Observe
|
|
77
|
+
browserctl url main
|
|
78
|
+
browserctl snapshot main --diff # only what changed
|
|
79
|
+
|
|
80
|
+
# 7. Done
|
|
81
|
+
browserctl daemon stop
|
|
22
82
|
```
|
|
23
83
|
|
|
24
|
-
|
|
25
|
-
|
|
84
|
+
→ [Full Getting Started guide](docs/getting-started.md)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Use cases
|
|
89
|
+
|
|
90
|
+
**AI coding agent authenticating into a staging environment** — the agent logs in once, the session persists, subsequent commands run inside the authenticated context without re-authenticating between steps.
|
|
91
|
+
|
|
92
|
+
**Developer reproducing a multi-step bug report** — navigate to the failure point once, then iterate on the fix with the browser already in the right state; no restarting from the home page each run.
|
|
93
|
+
|
|
94
|
+
**Automated smoke test that needs human sign-off** — the test runs until it hits something ambiguous, calls `browserctl pause`, lets a human inspect and act, then `browserctl resume` hands control back to the script with all state intact.
|
|
26
95
|
|
|
27
96
|
---
|
|
28
97
|
|
|
@@ -30,7 +99,7 @@ browserctl shutdown
|
|
|
30
99
|
|
|
31
100
|
Most automation tools are stateless — every script spins up a fresh browser and tears it down. browserctl doesn't.
|
|
32
101
|
|
|
33
|
-
| | browserctl | Playwright / Selenium |
|
|
102
|
+
| Capability | browserctl | Playwright / Selenium |
|
|
34
103
|
|---|---|---|
|
|
35
104
|
| Session persists across commands | ✓ | ✗ (per-script lifecycle) |
|
|
36
105
|
| Named page handles | ✓ | ✗ |
|
|
@@ -46,15 +115,10 @@ Most automation tools are stateless — every script spins up a fresh browser an
|
|
|
46
115
|
|
|
47
116
|
---
|
|
48
117
|
|
|
49
|
-
## Requirements
|
|
50
|
-
|
|
51
|
-
- Ruby >= 3.3
|
|
52
|
-
- Chrome or Chromium on your `PATH`
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
118
|
## Installation
|
|
57
119
|
|
|
120
|
+
**Requirements:** Ruby >= 3.3 · Chrome or Chromium installed
|
|
121
|
+
|
|
58
122
|
```bash
|
|
59
123
|
gem install browserctl
|
|
60
124
|
```
|
|
@@ -71,14 +135,14 @@ gem "browserctl"
|
|
|
71
135
|
|
|
72
136
|
browserctl ships as a Claude Code plugin. Install it once and Claude automatically knows how to use the daemon, ref-based interaction, HITL patterns, and workflow authoring.
|
|
73
137
|
|
|
74
|
-
**
|
|
138
|
+
**Interactive install**
|
|
75
139
|
|
|
76
140
|
```
|
|
77
141
|
/plugin marketplace add patrick204nqh/browserctl
|
|
78
142
|
/plugin install browserctl@browserctl
|
|
79
143
|
```
|
|
80
144
|
|
|
81
|
-
**
|
|
145
|
+
**Project settings** — commit `.claude/settings.json` to share with your team:
|
|
82
146
|
|
|
83
147
|
```json
|
|
84
148
|
{
|
|
@@ -93,36 +157,7 @@ browserctl ships as a Claude Code plugin. Install it once and Claude automatical
|
|
|
93
157
|
}
|
|
94
158
|
```
|
|
95
159
|
|
|
96
|
-
Once installed,
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Quick Start
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
# 1. Start the daemon
|
|
104
|
-
browserd &
|
|
105
|
-
|
|
106
|
-
# 2. Open a named page
|
|
107
|
-
browserctl open main --url https://the-internet.herokuapp.com/login
|
|
108
|
-
|
|
109
|
-
# 3. Snapshot the page — get AI-friendly JSON with ref IDs
|
|
110
|
-
browserctl snap main
|
|
111
|
-
|
|
112
|
-
# 4. Interact using refs
|
|
113
|
-
browserctl fill main --ref e1 --value tomsmith
|
|
114
|
-
browserctl fill main --ref e2 --value SuperSecretPassword!
|
|
115
|
-
browserctl click main --ref e3
|
|
116
|
-
|
|
117
|
-
# 5. Observe
|
|
118
|
-
browserctl url main
|
|
119
|
-
browserctl snap main --diff # only what changed
|
|
120
|
-
|
|
121
|
-
# 6. Done
|
|
122
|
-
browserctl shutdown
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
→ [Full Getting Started guide](docs/getting-started.md)
|
|
160
|
+
Once installed, the `browserctl` skill loads automatically.
|
|
126
161
|
|
|
127
162
|
---
|
|
128
163
|
|
|
@@ -135,7 +170,7 @@ Start multiple named instances for agent isolation:
|
|
|
135
170
|
```bash
|
|
136
171
|
browserd --name agent-a &
|
|
137
172
|
browserd --name agent-b &
|
|
138
|
-
browserctl --daemon agent-a open main --url https://app.example.com
|
|
173
|
+
browserctl --daemon agent-a page open main --url https://app.example.com
|
|
139
174
|
```
|
|
140
175
|
|
|
141
176
|
The daemon shuts itself down after 30 minutes of inactivity.
|
|
@@ -162,12 +197,19 @@ The daemon shuts itself down after 30 minutes of inactivity.
|
|
|
162
197
|
```bash
|
|
163
198
|
git clone https://github.com/patrick204nqh/browserctl
|
|
164
199
|
cd browserctl
|
|
165
|
-
bin/setup #
|
|
200
|
+
bin/setup # brew bundle (macOS) + bundle install + Chrome check
|
|
166
201
|
|
|
167
202
|
bundle exec rspec # run tests
|
|
168
203
|
bundle exec rubocop # lint
|
|
204
|
+
|
|
205
|
+
rake demo # full pipeline: screenshots + browser GIF + terminal GIF
|
|
206
|
+
rake demo:screenshots # smoke test screenshots only
|
|
207
|
+
rake demo:browser_gif # browser animation only (requires: ffmpeg)
|
|
208
|
+
rake demo:terminal # terminal GIF only (requires: vhs)
|
|
169
209
|
```
|
|
170
210
|
|
|
211
|
+
> Demo assets are regenerated automatically on every push to `main` that touches `demo/` or the login example.
|
|
212
|
+
|
|
171
213
|
---
|
|
172
214
|
|
|
173
215
|
## Contributing
|
data/bin/browserctl
CHANGED
|
@@ -14,20 +14,19 @@ require "json"
|
|
|
14
14
|
require "optimist"
|
|
15
15
|
require "browserctl"
|
|
16
16
|
require "browserctl/commands/cli_output"
|
|
17
|
-
require "browserctl/commands/open_page"
|
|
18
17
|
require "browserctl/commands/fill"
|
|
19
18
|
require "browserctl/commands/click"
|
|
20
19
|
require "browserctl/commands/snapshot"
|
|
21
20
|
require "browserctl/commands/screenshot"
|
|
22
|
-
require "browserctl/commands/watch"
|
|
23
21
|
require "browserctl/commands/record"
|
|
24
|
-
require "browserctl/commands/pause"
|
|
25
22
|
require "browserctl/commands/resume"
|
|
26
23
|
require "browserctl/commands/init"
|
|
27
|
-
require "browserctl/commands/
|
|
28
|
-
require "browserctl/commands/
|
|
29
|
-
require "browserctl/commands/
|
|
30
|
-
require "browserctl/commands/
|
|
24
|
+
require "browserctl/commands/page"
|
|
25
|
+
require "browserctl/commands/cookie"
|
|
26
|
+
require "browserctl/commands/storage"
|
|
27
|
+
require "browserctl/commands/session"
|
|
28
|
+
require "browserctl/commands/daemon"
|
|
29
|
+
require "browserctl/commands/workflow"
|
|
31
30
|
|
|
32
31
|
def print_result(res)
|
|
33
32
|
if res.is_a?(Hash) && res[:error]
|
|
@@ -37,59 +36,79 @@ def print_result(res)
|
|
|
37
36
|
puts res.to_json
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
# rubocop:disable Metrics/MethodLength
|
|
41
39
|
def usage
|
|
42
40
|
puts <<~USAGE
|
|
43
41
|
Usage: browserctl <command> [args]
|
|
44
42
|
|
|
45
43
|
Setup:
|
|
46
|
-
init
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
open
|
|
50
|
-
close
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
44
|
+
init
|
|
45
|
+
|
|
46
|
+
Page:
|
|
47
|
+
page open <name> [--url URL]
|
|
48
|
+
page close <name>
|
|
49
|
+
page list
|
|
50
|
+
page focus <name>
|
|
51
|
+
|
|
52
|
+
Interaction (page is always first arg after verb):
|
|
53
|
+
navigate <page> <url>
|
|
54
|
+
fill <page> <selector> <value>
|
|
55
|
+
<page> --ref <ref> --value <value>
|
|
56
|
+
click <page> <selector>
|
|
57
|
+
<page> --ref <ref>
|
|
58
|
+
snapshot <page> [--format elements|html] [--diff]
|
|
59
|
+
screenshot <page> [--out PATH] [--full]
|
|
60
|
+
evaluate <page> <expression>
|
|
61
|
+
url <page>
|
|
62
|
+
wait <page> <selector> [--timeout N]
|
|
63
|
+
pause <page> [--message MSG]
|
|
64
|
+
resume <page>
|
|
65
|
+
devtools <page>
|
|
66
|
+
|
|
67
|
+
Cookie:
|
|
68
|
+
cookie list <page>
|
|
69
|
+
cookie set <page> <name> <value> --domain DOMAIN [--path /]
|
|
70
|
+
cookie delete <page>
|
|
71
|
+
cookie export <page> <path>
|
|
72
|
+
cookie import <page> <path>
|
|
73
|
+
|
|
74
|
+
Storage:
|
|
75
|
+
storage get <page> <key> [--store local|session]
|
|
76
|
+
storage set <page> <key> <value> [--store local|session]
|
|
77
|
+
storage export <page> <path> [--store local|session|all]
|
|
78
|
+
storage import <page> <path>
|
|
79
|
+
storage delete <page> [--store local|session|all]
|
|
80
|
+
|
|
81
|
+
Session:
|
|
82
|
+
session save <name>
|
|
83
|
+
session load <name>
|
|
84
|
+
session list
|
|
85
|
+
session delete <name>
|
|
86
|
+
session export <name> <path>
|
|
87
|
+
session import <path>
|
|
88
|
+
|
|
89
|
+
Recording:
|
|
90
|
+
record start <name>
|
|
91
|
+
record stop [--out PATH]
|
|
92
|
+
record status
|
|
93
|
+
|
|
94
|
+
Workflow:
|
|
95
|
+
workflow run <name|file> [--params file] [--key value ...]
|
|
96
|
+
workflow list
|
|
97
|
+
workflow describe <name>
|
|
98
|
+
|
|
99
|
+
Daemon:
|
|
100
|
+
daemon start [--headed] [--name NAME]
|
|
101
|
+
daemon stop
|
|
102
|
+
daemon status
|
|
103
|
+
daemon ping
|
|
104
|
+
daemon list
|
|
105
|
+
|
|
106
|
+
Global options:
|
|
107
|
+
--daemon <name> Connect to named or auto-indexed daemon (d1, d2, work, ...)
|
|
108
|
+
--version, -v
|
|
89
109
|
USAGE
|
|
90
110
|
exit 0
|
|
91
111
|
end
|
|
92
|
-
# rubocop:enable Metrics/MethodLength
|
|
93
112
|
|
|
94
113
|
daemon_idx = ARGV.index("--daemon")
|
|
95
114
|
daemon_name = if daemon_idx
|
|
@@ -105,70 +124,60 @@ usage if cmd.nil? || %w[-h --help help].include?(cmd)
|
|
|
105
124
|
runner = Browserctl::Runner.new
|
|
106
125
|
|
|
107
126
|
case cmd
|
|
108
|
-
when "run
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
before = Browserctl::REGISTRY.keys.dup
|
|
112
|
-
load File.expand_path(name)
|
|
113
|
-
name = (Browserctl::REGISTRY.keys - before).first || File.basename(name, ".rb")
|
|
114
|
-
end
|
|
115
|
-
params_file_idx = args.index("--params")
|
|
116
|
-
file_params = {}
|
|
117
|
-
if params_file_idx
|
|
118
|
-
params_path = args.delete_at(params_file_idx + 1)
|
|
119
|
-
args.delete_at(params_file_idx)
|
|
120
|
-
begin
|
|
121
|
-
file_params = Browserctl::Runner.load_params_file(params_path)
|
|
122
|
-
rescue StandardError => e
|
|
123
|
-
abort "Error loading params file: #{e.message}"
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
cli_params = {}
|
|
127
|
-
args.each_slice(2) do |flag, val|
|
|
128
|
-
key = flag.sub(/\A--/, "").to_sym
|
|
129
|
-
cli_params[key] = val
|
|
130
|
-
end
|
|
131
|
-
params = file_params.merge(cli_params)
|
|
132
|
-
success = runner.run_workflow(name, **params)
|
|
133
|
-
exit(success ? 0 : 1)
|
|
134
|
-
|
|
135
|
-
when "workflows"
|
|
136
|
-
list = runner.list_workflows
|
|
137
|
-
list.each { |w| puts "#{w[:name].ljust(24)} #{w[:desc]}" }
|
|
138
|
-
|
|
139
|
-
when "describe"
|
|
140
|
-
name = args.shift or abort "usage: browserctl describe <workflow_name>"
|
|
141
|
-
puts JSON.pretty_generate(runner.describe_workflow(name))
|
|
142
|
-
|
|
143
|
-
when "record" then Browserctl::Commands::Record.run(args)
|
|
144
|
-
when "init" then Browserctl::Commands::Init.run(args)
|
|
127
|
+
when "workflow" then Browserctl::Commands::Workflow.run(runner, args)
|
|
128
|
+
when "record" then Browserctl::Commands::Record.run(args)
|
|
129
|
+
when "init" then Browserctl::Commands::Init.run(args)
|
|
145
130
|
|
|
146
131
|
else
|
|
147
132
|
client = Browserctl::Client.new(Browserctl.socket_path(daemon_name))
|
|
148
133
|
|
|
149
134
|
case cmd
|
|
150
|
-
when "
|
|
151
|
-
when "
|
|
152
|
-
when "
|
|
153
|
-
when "
|
|
135
|
+
when "page" then Browserctl::Commands::Page.run(client, args)
|
|
136
|
+
when "cookie" then Browserctl::Commands::Cookie.run(client, args)
|
|
137
|
+
when "storage" then Browserctl::Commands::Storage.run(client, args)
|
|
138
|
+
when "session" then Browserctl::Commands::Session.run(client, args)
|
|
139
|
+
when "daemon" then Browserctl::Commands::Daemon.run(client, args)
|
|
140
|
+
when "navigate" then print_result(client.navigate(args[0], args[1]))
|
|
154
141
|
when "fill" then Browserctl::Commands::Fill.run(client, args)
|
|
155
142
|
when "click" then Browserctl::Commands::Click.run(client, args)
|
|
156
|
-
when "
|
|
157
|
-
when "
|
|
143
|
+
when "snapshot" then Browserctl::Commands::Snapshot.run(client, args)
|
|
144
|
+
when "screenshot" then Browserctl::Commands::Screenshot.run(client, args)
|
|
145
|
+
when "evaluate" then print_result(client.evaluate(args[0], args[1]))
|
|
158
146
|
when "url" then print_result(client.url(args[0]))
|
|
159
|
-
when "
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
when "
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
when "wait"
|
|
148
|
+
opts = Optimist.options(args) do
|
|
149
|
+
opt :timeout, "Seconds to wait (default: 30)", default: 30.0, short: "-t"
|
|
150
|
+
end
|
|
151
|
+
name = args.shift
|
|
152
|
+
selector = args.shift
|
|
153
|
+
abort "usage: browserctl wait <page> <selector> [--timeout N]" unless name && selector
|
|
154
|
+
print_result(client.wait(name, selector, timeout: opts[:timeout]))
|
|
155
|
+
when "pause"
|
|
156
|
+
opts = Optimist.options(args) do
|
|
157
|
+
opt :message, "Message shown to human", type: :string, short: "-m"
|
|
158
|
+
end
|
|
159
|
+
name = args.shift or abort "usage: browserctl pause <page> [--message MSG]"
|
|
160
|
+
res = client.pause(name, message: opts[:message])
|
|
161
|
+
if res[:error]
|
|
162
|
+
warn "Error: #{res[:error]}"
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
puts "Page '#{name}' paused. Browser is live — interact freely."
|
|
166
|
+
puts "(#{opts[:message]})" if opts[:message]
|
|
167
|
+
puts "When done: browserctl resume #{name}"
|
|
168
|
+
when "resume" then Browserctl::Commands::Resume.run(client, args)
|
|
169
|
+
when "devtools"
|
|
170
|
+
name = args.shift or abort "usage: browserctl devtools <page>"
|
|
171
|
+
res = client.devtools(name)
|
|
172
|
+
if res[:error]
|
|
173
|
+
warn "Error: #{res[:error]}"
|
|
174
|
+
exit 1
|
|
175
|
+
end
|
|
176
|
+
url = res[:devtools_url]
|
|
177
|
+
puts "Opening DevTools for '#{name}':"
|
|
178
|
+
puts " #{url}"
|
|
179
|
+
opener = RUBY_PLATFORM =~ /darwin/ ? "open" : "xdg-open"
|
|
180
|
+
system(opener, url)
|
|
172
181
|
else
|
|
173
182
|
abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
|
|
174
183
|
end
|
data/bin/browserd
CHANGED
|
@@ -20,11 +20,17 @@ if opts[:name] && opts[:name] !~ /\A[a-zA-Z0-9_-]{1,64}\z/
|
|
|
20
20
|
abort "Invalid daemon name #{opts[:name].inspect} — use only letters, digits, _ or -"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
assigned_name = opts[:name] || Browserctl.next_daemon_name
|
|
24
|
+
if assigned_name && !opts[:name]
|
|
25
|
+
warn "browserd: default slot taken — starting as '#{assigned_name}'"
|
|
26
|
+
warn " to connect: browserctl --daemon #{assigned_name} <command>"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
log_path = Browserctl.log_path(assigned_name)
|
|
24
30
|
warn "browserd starting — log: #{log_path}"
|
|
25
31
|
Browserctl.logger = Browserctl.build_logger(opts[:log_level], log_path: log_path)
|
|
26
32
|
Browserctl::Server.new(
|
|
27
33
|
headless: !opts[:headed],
|
|
28
|
-
socket_path: Browserctl.socket_path(
|
|
29
|
-
pid_path: Browserctl.pid_path(
|
|
34
|
+
socket_path: Browserctl.socket_path(assigned_name),
|
|
35
|
+
pid_path: Browserctl.pid_path(assigned_name)
|
|
30
36
|
).run
|
data/bin/setup
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
if [[ "$(uname)" == "Darwin" ]] && command -v brew &>/dev/null; then
|
|
5
|
+
echo "==> Installing Homebrew dependencies (Brewfile)..."
|
|
6
|
+
brew bundle --no-upgrade
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
echo "==> Installing gem dependencies..."
|
|
5
10
|
bundle install
|
|
6
11
|
|
|
7
12
|
echo "==> Checking for Chrome/Chromium..."
|
|
8
13
|
if ! command -v google-chrome &>/dev/null && ! command -v chromium-browser &>/dev/null && ! command -v chromium &>/dev/null; then
|
|
9
14
|
echo "WARNING: Chrome/Chromium not found on PATH."
|
|
10
|
-
echo "
|
|
11
|
-
echo " macOS: brew install --cask google-chrome"
|
|
15
|
+
echo " macOS: brew bundle (includes google-chrome)"
|
|
12
16
|
echo " Ubuntu: sudo apt-get install -y chromium-browser"
|
|
13
17
|
fi
|
|
14
18
|
|
data/examples/cloudflare_hitl.rb
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# pauses automation so a human can solve it in the live browser, then resumes.
|
|
8
8
|
#
|
|
9
9
|
# Run:
|
|
10
|
-
# browserctl run examples/cloudflare_hitl.rb --url https://example.com
|
|
10
|
+
# browserctl workflow run examples/cloudflare_hitl.rb --url https://example.com
|
|
11
11
|
#
|
|
12
12
|
# Note: modern Cloudflare often passes a real headed Chrome without challenge.
|
|
13
13
|
# The pause/resume branch fires only when the challenge page is actually served
|
|
@@ -22,11 +22,11 @@ Browserctl.workflow "cloudflare_hitl" do
|
|
|
22
22
|
param :selector, default: "body"
|
|
23
23
|
|
|
24
24
|
step "open page" do
|
|
25
|
-
|
|
25
|
+
open_page(:main)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
step "navigate to target URL" do
|
|
29
|
-
res = client.
|
|
29
|
+
res = client.navigate("main", url)
|
|
30
30
|
|
|
31
31
|
if res[:challenge]
|
|
32
32
|
$stdout.puts ""
|
|
@@ -54,12 +54,12 @@ Browserctl.workflow "cloudflare_hitl" do
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
step "wait for content and snapshot" do
|
|
57
|
-
page(:main).
|
|
58
|
-
result = page(:main).snapshot(format: "
|
|
57
|
+
page(:main).wait(selector, timeout: 15)
|
|
58
|
+
result = page(:main).snapshot(format: "elements")
|
|
59
59
|
$stdout.puts " Snapshot: #{result[:snapshot]&.length || 0} elements captured"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
step "close page" do
|
|
63
|
-
|
|
63
|
+
close_page(:main)
|
|
64
64
|
end
|
|
65
65
|
end
|