browserctl 0.11.0 → 0.13.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 +4 -3
- data/bin/browserctl +171 -115
- data/bin/browserd +8 -1
- data/lib/browserctl/callable_definition.rb +114 -0
- data/lib/browserctl/client.rb +3 -30
- data/lib/browserctl/commands/cli_output.rb +38 -4
- data/lib/browserctl/commands/daemon.rb +10 -6
- data/lib/browserctl/commands/flow.rb +7 -5
- data/lib/browserctl/commands/init.rb +20 -7
- data/lib/browserctl/commands/migrate.rb +142 -0
- data/lib/browserctl/commands/output_format.rb +144 -0
- data/lib/browserctl/commands/page.rb +9 -5
- data/lib/browserctl/commands/{record.rb → recording.rb} +14 -13
- data/lib/browserctl/commands/resume.rb +1 -1
- data/lib/browserctl/commands/screenshot.rb +2 -2
- data/lib/browserctl/commands/snapshot.rb +8 -3
- data/lib/browserctl/commands/state.rb +3 -2
- data/lib/browserctl/commands/trace.rb +216 -0
- data/lib/browserctl/commands/workflow.rb +9 -7
- data/lib/browserctl/constants.rb +3 -1
- data/lib/browserctl/contextual_persistence.rb +58 -0
- data/lib/browserctl/crash_report.rb +96 -0
- data/lib/browserctl/driver/cdp.rb +2 -3
- data/lib/browserctl/encryption_service.rb +84 -0
- data/lib/browserctl/error/codes.rb +44 -0
- data/lib/browserctl/error/exit_codes.rb +54 -0
- data/lib/browserctl/error/suggested_actions.rb +41 -0
- data/lib/browserctl/errors.rb +44 -14
- data/lib/browserctl/flow.rb +35 -59
- data/lib/browserctl/flows/stdlib/cloudflare_solve.rb +4 -4
- data/lib/browserctl/format_version.rb +37 -0
- data/lib/browserctl/logger.rb +102 -9
- data/lib/browserctl/migrations.rb +216 -0
- data/lib/browserctl/recording/log_writer.rb +82 -0
- data/lib/browserctl/recording/redactor.rb +58 -0
- data/lib/browserctl/recording/state.rb +44 -0
- data/lib/browserctl/recording/workflow_renderer.rb +214 -0
- data/lib/browserctl/recording.rb +39 -268
- data/lib/browserctl/redactor.rb +58 -0
- data/lib/browserctl/rubocop/cops/typed_error.rb +69 -0
- data/lib/browserctl/runner.rb +12 -6
- data/lib/browserctl/secret_resolver_registry.rb +23 -4
- data/lib/browserctl/server/command_dispatcher.rb +28 -16
- data/lib/browserctl/server/handlers/daemon_control.rb +5 -1
- data/lib/browserctl/server/handlers/error_payload.rb +27 -0
- data/lib/browserctl/server/handlers/interaction.rb +21 -3
- data/lib/browserctl/server/handlers/navigation.rb +19 -3
- data/lib/browserctl/server/handlers/state.rb +7 -5
- data/lib/browserctl/server.rb +2 -1
- data/lib/browserctl/state/bundle.rb +63 -49
- data/lib/browserctl/state.rb +46 -9
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow/flow_wrapper.rb +1 -1
- data/lib/browserctl/workflow/recovery_manager.rb +87 -0
- data/lib/browserctl/workflow.rb +117 -238
- metadata +25 -14
- data/examples/session_reuse.rb +0 -75
- data/lib/browserctl/commands/session.rb +0 -243
- data/lib/browserctl/driver/base.rb +0 -13
- data/lib/browserctl/driver.rb +0 -5
- data/lib/browserctl/server/handlers/session.rb +0 -94
- data/lib/browserctl/session.rb +0 -206
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41aa6c550f8a9a6403781639ebbd381c653aedb03cfa95f732b74de3ee0a931d
|
|
4
|
+
data.tar.gz: 3feea758e797eca81e1ab269ca5d9011f82778510dab1ee45e31b80b9aa38048
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e100a2bce4a66b7ccd1cfaebfd5e5acb3b0f9c32aafec8427efe0f8d2b613ae954d9043a8d4da536284b7739508d4b318dab5e2abe16a83113938a23fb000796
|
|
7
|
+
data.tar.gz: b2171d7d6043c5c89084a21f50a5c53bbaa0ce1e891babeb30b4beb66ede7e41e8f87e7382980bb9f9229e453121cc99bd252aa4d54c48598d01bdbc7fbdadab
|
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.13.0](https://github.com/patrick204nqh/browserctl/compare/v0.12.0...v0.13.0) (2026-05-10)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### ⚠ BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* `--output` is now a reserved global flag on every browserctl command. Existing invocations are unaffected (default mode `text` preserves prior byte-for-byte output), but commands that previously consumed `--output` as a positional or sub-flag would now collide. None ship in this gem.
|
|
19
|
+
* The CLI subcommands `snapshot`, `screenshot`, and `record` are removed with no aliases. Use `page snapshot`, `page screenshot`, and `recording start|stop|status` respectively.
|
|
20
|
+
* The `session` CLI commands and the `session_*` JSON-RPC wire commands are removed. Users on v0.12 sessions must regenerate state via `state save` before upgrading. The workflow DSL methods `save_session`, `load_session`, and `list_sessions` are also removed; use `save_state` / `load_state` instead.
|
|
21
|
+
* `store` and `fetch` wire commands are no longer part of the Fixed zone. They move to Extension and may change between minor releases with a changelog entry. The wire shape is unchanged in v0.13; the change is to the stability promise.
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* narrow Fixed zone — store/fetch and known overlaps ([#158](https://github.com/patrick204nqh/browserctl/issues/158)) ([067cb42](https://github.com/patrick204nqh/browserctl/commit/067cb423b5966784abc8be2395808b5e4fa92ac0))
|
|
26
|
+
* noun-verb CLI consistency ([#162](https://github.com/patrick204nqh/browserctl/issues/162)) ([8ceb729](https://github.com/patrick204nqh/browserctl/commit/8ceb72998008a8e6983cb4bad13ac5f286ad9ee8))
|
|
27
|
+
* remove session commands and code path ([#161](https://github.com/patrick204nqh/browserctl/issues/161)) ([12d58d9](https://github.com/patrick204nqh/browserctl/commit/12d58d9aa26b2609c98e4b34ce072a6dbb8baafe))
|
|
28
|
+
* unified --output {json,text,silent} on every CLI command ([#164](https://github.com/patrick204nqh/browserctl/issues/164)) ([f8947c6](https://github.com/patrick204nqh/browserctl/commit/f8947c655bc0b1f21bbf6067a059d6ab0b7d8b9e))
|
|
29
|
+
|
|
30
|
+
## [0.12.0](https://github.com/patrick204nqh/browserctl/compare/v0.11.0...v0.12.0) (2026-05-10)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### ⚠ BREAKING CHANGES
|
|
34
|
+
|
|
35
|
+
* Scripts that hard-coded `exit 7 = AUTH_REQUIRED` must update — AUTH_REQUIRED is now exit 3, and 7 is STATE_EXPIRED. Scripts treating any non-zero status as a generic failure are unaffected. Prior to this change the CLI exited 1 for everything except AUTH_REQUIRED (which already used 7); typed errors now surface their dedicated codes.
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
* benchmark harness + bundle_codec target ([#154](https://github.com/patrick204nqh/browserctl/issues/154)) ([f4b815a](https://github.com/patrick204nqh/browserctl/commit/f4b815a617d4a7d22473e6028ce2a0f90467fa87))
|
|
40
|
+
* bundle format version ([#132](https://github.com/patrick204nqh/browserctl/issues/132)) ([b52d325](https://github.com/patrick204nqh/browserctl/commit/b52d32546d1b9ec3915fac014fecd8f929161ddb))
|
|
41
|
+
* crash reports ([#135](https://github.com/patrick204nqh/browserctl/issues/135)) ([b02adf4](https://github.com/patrick204nqh/browserctl/commit/b02adf4d41295dbe547d2965d838168d48efb8bf))
|
|
42
|
+
* error code enum ([#128](https://github.com/patrick204nqh/browserctl/issues/128)) ([feb0522](https://github.com/patrick204nqh/browserctl/commit/feb0522887935737fd96ad8a8b47d1407a5f585e))
|
|
43
|
+
* exit code map ([#140](https://github.com/patrick204nqh/browserctl/issues/140)) ([1a1d2b2](https://github.com/patrick204nqh/browserctl/commit/1a1d2b2f50693863cb508b5354e5546f340a2771))
|
|
44
|
+
* format version header convention ([#129](https://github.com/patrick204nqh/browserctl/issues/129)) ([405774d](https://github.com/patrick204nqh/browserctl/commit/405774dd6c99cc96f62f0c092010b30e7e710d41))
|
|
45
|
+
* migration helpers ([#143](https://github.com/patrick204nqh/browserctl/issues/143)) ([1c1762e](https://github.com/patrick204nqh/browserctl/commit/1c1762e755f83445c7cdd0df75fc5324a820fa19))
|
|
46
|
+
* recording format version ([#136](https://github.com/patrick204nqh/browserctl/issues/136)) ([4f1bc46](https://github.com/patrick204nqh/browserctl/commit/4f1bc467e653ecc1f508a85b3c329adf048b783d))
|
|
47
|
+
* structured error payload ([#137](https://github.com/patrick204nqh/browserctl/issues/137)) ([f028410](https://github.com/patrick204nqh/browserctl/commit/f02841069f68de749a76ba9a98a38b0c20836d76))
|
|
48
|
+
* structured JSONL logs ([#131](https://github.com/patrick204nqh/browserctl/issues/131)) ([c6daff4](https://github.com/patrick204nqh/browserctl/commit/c6daff488460606659c0641dd8cee6c23561be05))
|
|
49
|
+
* trace --redact ([#139](https://github.com/patrick204nqh/browserctl/issues/139)) ([19b4cec](https://github.com/patrick204nqh/browserctl/commit/19b4cec37daf95a06b0513780d991d0522069593))
|
|
50
|
+
* trace command ([#133](https://github.com/patrick204nqh/browserctl/issues/133)) ([156f304](https://github.com/patrick204nqh/browserctl/commit/156f304246d24fa6c3e06f8d941ac971629ea77c))
|
|
51
|
+
* workflow format version ([#138](https://github.com/patrick204nqh/browserctl/issues/138)) ([963776b](https://github.com/patrick204nqh/browserctl/commit/963776be785583fc3a3b0c04c97510e16c73ca9a))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
### Miscellaneous Chores
|
|
55
|
+
|
|
56
|
+
* target next release as 0.12.0 ([#156](https://github.com/patrick204nqh/browserctl/issues/156)) ([9797fcc](https://github.com/patrick204nqh/browserctl/commit/9797fcc0bae1a47bb72eec9cec5fda105e698406))
|
|
57
|
+
|
|
13
58
|
## [0.11.0](https://github.com/patrick204nqh/browserctl/compare/v0.10.0...v0.11.0) (2026-05-10)
|
|
14
59
|
|
|
15
60
|
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Every browser automation tool restarts the browser when your script ends. That m
|
|
|
21
21
|
```bash
|
|
22
22
|
browserd & # start the daemon (headless)
|
|
23
23
|
browserctl page open main --url https://example.com/login
|
|
24
|
-
browserctl snapshot main # AI-friendly JSON snapshot with ref IDs
|
|
24
|
+
browserctl page snapshot main # AI-friendly JSON snapshot with ref IDs
|
|
25
25
|
browserctl fill main --ref e1 --value me@example.com # interact by ref, no selectors needed
|
|
26
26
|
browserctl click main --ref e2
|
|
27
27
|
browserctl daemon stop
|
|
@@ -42,7 +42,7 @@ browserd &
|
|
|
42
42
|
browserctl page open main --url https://moatazeldebsy.github.io/test-automation-practices/#/auth
|
|
43
43
|
|
|
44
44
|
# 4. Snapshot — returns JSON with a ref ID per interactable element
|
|
45
|
-
browserctl snapshot main
|
|
45
|
+
browserctl page snapshot main
|
|
46
46
|
# → [{"ref":"e1","tag":"input","attrs":{"data-test":"username-input"}}, {"ref":"e2",...}, {"ref":"e3","tag":"button","text":"Login",...}]
|
|
47
47
|
|
|
48
48
|
# 5. Interact using the ref IDs from the snapshot
|
|
@@ -52,7 +52,7 @@ browserctl click main --ref e3
|
|
|
52
52
|
|
|
53
53
|
# 6. Observe
|
|
54
54
|
browserctl url main
|
|
55
|
-
browserctl snapshot main --diff
|
|
55
|
+
browserctl page snapshot main --diff # only what changed
|
|
56
56
|
|
|
57
57
|
# Session persistence: save now, pick up later
|
|
58
58
|
browserctl session save my-session
|
|
@@ -198,6 +198,7 @@ The daemon shuts itself down after 30 minutes of inactivity.
|
|
|
198
198
|
| [Agent Integration](docs/guides/agent-integration.md) | Call browserctl from Python, shell, or Anthropic tool-use agents |
|
|
199
199
|
| [Concepts](docs/concepts/) | Sessions, snapshots, [state](docs/concepts/state.md), [flows](docs/concepts/flows.md), human-in-the-loop |
|
|
200
200
|
| [Guides](docs/guides/) | Writing workflows, handling challenges, smoke testing |
|
|
201
|
+
| [Debugging](docs/guides/debugging.md) | Read traces, redaction, crash reports, filing a good issue |
|
|
201
202
|
| [Examples](examples/) | Runnable scripts: session reuse, Cloudflare HITL, and more |
|
|
202
203
|
| [Command Reference](docs/reference/commands.md) | Every command and flag |
|
|
203
204
|
| [API Stability](docs/reference/api-stability.md) | Wire protocol contract and stability zones |
|
data/bin/browserctl
CHANGED
|
@@ -14,31 +14,48 @@ require "json"
|
|
|
14
14
|
require "optimist"
|
|
15
15
|
require "browserctl"
|
|
16
16
|
require "browserctl/commands/cli_output"
|
|
17
|
+
require "browserctl/commands/output_format"
|
|
17
18
|
require "browserctl/commands/fill"
|
|
18
19
|
require "browserctl/commands/click"
|
|
19
20
|
require "browserctl/commands/snapshot"
|
|
20
21
|
require "browserctl/commands/screenshot"
|
|
21
|
-
require "browserctl/commands/
|
|
22
|
+
require "browserctl/commands/recording"
|
|
22
23
|
require "browserctl/commands/resume"
|
|
23
24
|
require "browserctl/commands/init"
|
|
24
25
|
require "browserctl/commands/page"
|
|
25
26
|
require "browserctl/commands/cookie"
|
|
26
27
|
require "browserctl/commands/storage"
|
|
27
|
-
require "browserctl/commands/session"
|
|
28
28
|
require "browserctl/commands/state"
|
|
29
29
|
require "browserctl/commands/daemon"
|
|
30
30
|
require "browserctl/commands/workflow"
|
|
31
31
|
require "browserctl/commands/flow"
|
|
32
32
|
require "browserctl/commands/dialog"
|
|
33
33
|
require "browserctl/commands/ask"
|
|
34
|
+
require "browserctl/commands/trace"
|
|
35
|
+
require "browserctl/commands/migrate"
|
|
34
36
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
def structured_stderr_payload(res, message)
|
|
38
|
+
code = (res[:code] || res["code"] || Browserctl::Error::Codes::GENERIC).to_s
|
|
39
|
+
context = res[:context] || res["context"] || {}
|
|
40
|
+
action = res[:suggested_action] || res["suggested_action"] ||
|
|
41
|
+
Browserctl::Error::SuggestedActions.for(code)
|
|
42
|
+
[code, JSON.generate(code: code, message: message, context: context, suggested_action: action)]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def print_result(res, text_block = nil)
|
|
46
|
+
fmt = Browserctl::Commands::OutputFormat.current
|
|
47
|
+
|
|
48
|
+
unless res.is_a?(Hash) && (res[:error] || res["error"])
|
|
49
|
+
fmt.emit(res, text_block)
|
|
50
|
+
return
|
|
40
51
|
end
|
|
41
|
-
|
|
52
|
+
|
|
53
|
+
message = res[:error] || res["error"]
|
|
54
|
+
code, payload = structured_stderr_payload(res, message)
|
|
55
|
+
warn "Error: #{message}"
|
|
56
|
+
warn payload
|
|
57
|
+
puts res.to_json unless fmt.silent?
|
|
58
|
+
exit Browserctl::Error::ExitCodes.for(code)
|
|
42
59
|
end
|
|
43
60
|
|
|
44
61
|
def usage
|
|
@@ -49,10 +66,12 @@ def usage
|
|
|
49
66
|
init
|
|
50
67
|
|
|
51
68
|
Page:
|
|
52
|
-
page open
|
|
53
|
-
page close
|
|
69
|
+
page open <name> [--url URL]
|
|
70
|
+
page close <name>
|
|
54
71
|
page list
|
|
55
|
-
page focus
|
|
72
|
+
page focus <name>
|
|
73
|
+
page snapshot <name> [--format elements|html] [--diff]
|
|
74
|
+
page screenshot <name> [--out PATH] [--full]
|
|
56
75
|
|
|
57
76
|
Interaction (page is always first arg after verb):
|
|
58
77
|
navigate <page> <url>
|
|
@@ -60,8 +79,6 @@ def usage
|
|
|
60
79
|
<page> --ref <ref> --value <value>
|
|
61
80
|
click <page> <selector>
|
|
62
81
|
<page> --ref <ref>
|
|
63
|
-
snapshot <page> [--format elements|html] [--diff]
|
|
64
|
-
screenshot <page> [--out PATH] [--full]
|
|
65
82
|
evaluate <page> <expression>
|
|
66
83
|
url <page>
|
|
67
84
|
wait <page> <selector> [--timeout N]
|
|
@@ -90,14 +107,6 @@ def usage
|
|
|
90
107
|
storage import <page> <path>
|
|
91
108
|
storage delete <page> [--store local|session|all]
|
|
92
109
|
|
|
93
|
-
Session:
|
|
94
|
-
session save <name>
|
|
95
|
-
session load <name>
|
|
96
|
-
session list
|
|
97
|
-
session delete <name>
|
|
98
|
-
session export <name> <path>
|
|
99
|
-
session import <path>
|
|
100
|
-
|
|
101
110
|
Auth detection:
|
|
102
111
|
auth-check <page> [--cookies] [--state NAME] [--flow NAME]
|
|
103
112
|
# Exits 7 with code: AUTH_REQUIRED when the page needs login.
|
|
@@ -113,9 +122,9 @@ def usage
|
|
|
113
122
|
state import <source> [--name NAME]
|
|
114
123
|
|
|
115
124
|
Recording:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
recording start <name>
|
|
126
|
+
recording stop [--out PATH]
|
|
127
|
+
recording status
|
|
119
128
|
|
|
120
129
|
Workflow:
|
|
121
130
|
workflow run <name|file> [--check] [--params file] [--key value ...]
|
|
@@ -129,6 +138,15 @@ def usage
|
|
|
129
138
|
flow list
|
|
130
139
|
flow describe <name>
|
|
131
140
|
|
|
141
|
+
Trace:
|
|
142
|
+
trace [<session>] Pretty timeline of CLI + daemon log events.
|
|
143
|
+
|
|
144
|
+
Migrate:
|
|
145
|
+
migrate <path> [--to-version N] [--dry-run]
|
|
146
|
+
# Upgrades a persisted artifact (.bctl / .jsonl recording / workflow .rb)
|
|
147
|
+
# using registered migrations. Empty registry in v0.12 — first real
|
|
148
|
+
# migration ships post-1.0 when a format actually changes.
|
|
149
|
+
|
|
132
150
|
Daemon:
|
|
133
151
|
daemon start [--headed] [--name NAME]
|
|
134
152
|
daemon stop
|
|
@@ -138,17 +156,40 @@ def usage
|
|
|
138
156
|
|
|
139
157
|
Global options:
|
|
140
158
|
--daemon <name> Connect to named or auto-indexed daemon (d1, d2, work, ...)
|
|
159
|
+
--output <fmt> Output format: json | text | silent (default: text;
|
|
160
|
+
override with BROWSERCTL_OUTPUT)
|
|
141
161
|
--version, -v
|
|
142
162
|
USAGE
|
|
143
163
|
exit 0
|
|
144
164
|
end
|
|
145
165
|
|
|
166
|
+
# Resolve --output {json,text,silent} (or BROWSERCTL_OUTPUT) once, globally,
|
|
167
|
+
# before per-command parsers run. Strips the flag from ARGV in place so
|
|
168
|
+
# downstream Optimist/positional parsers don't see it.
|
|
169
|
+
begin
|
|
170
|
+
Browserctl::Commands::OutputFormat.install!(ARGV)
|
|
171
|
+
rescue Browserctl::Commands::OutputFormat::InvalidFormat => e
|
|
172
|
+
warn "Error: #{e.message}"
|
|
173
|
+
exit 2
|
|
174
|
+
end
|
|
175
|
+
|
|
146
176
|
daemon_idx = ARGV.index("--daemon")
|
|
147
177
|
daemon_name = if daemon_idx
|
|
148
178
|
ARGV.delete_at(daemon_idx)
|
|
149
179
|
ARGV.delete_at(daemon_idx)
|
|
150
180
|
end
|
|
151
181
|
|
|
182
|
+
log_level_idx = ARGV.index("--log-level") || ARGV.index("-l")
|
|
183
|
+
log_level = if log_level_idx
|
|
184
|
+
ARGV.delete_at(log_level_idx)
|
|
185
|
+
ARGV.delete_at(log_level_idx)
|
|
186
|
+
end || ENV["BROWSERCTL_LOG_LEVEL"] || "info"
|
|
187
|
+
|
|
188
|
+
# CLI invocations write structured JSON Lines to ~/.browserctl/logs/cli.log
|
|
189
|
+
# alongside the existing stderr behaviour. Stderr stays human-readable so
|
|
190
|
+
# scripted callers see no change.
|
|
191
|
+
Browserctl.logger = Browserctl.build_logger(log_level, component: "cli")
|
|
192
|
+
|
|
152
193
|
cmd = ARGV.shift
|
|
153
194
|
args = ARGV.dup
|
|
154
195
|
|
|
@@ -156,99 +197,114 @@ usage if cmd.nil? || %w[-h --help help].include?(cmd)
|
|
|
156
197
|
|
|
157
198
|
runner = Browserctl::Runner.new
|
|
158
199
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
when "
|
|
162
|
-
when "
|
|
163
|
-
when "
|
|
200
|
+
begin
|
|
201
|
+
case cmd
|
|
202
|
+
when "workflow" then Browserctl::Commands::Workflow.run(runner, args)
|
|
203
|
+
when "recording" then Browserctl::Commands::Recording.run(args)
|
|
204
|
+
when "init" then Browserctl::Commands::Init.run(args)
|
|
205
|
+
when "ask" then Browserctl::Commands::Ask.run(args)
|
|
206
|
+
when "trace" then Browserctl::Commands::Trace.run(args)
|
|
207
|
+
when "migrate" then Browserctl::Commands::Migrate.run(args)
|
|
164
208
|
|
|
165
|
-
else
|
|
166
|
-
|
|
209
|
+
else
|
|
210
|
+
client = Browserctl::Client.new(Browserctl.socket_path(daemon_name))
|
|
167
211
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
case cmd
|
|
213
|
+
when "flow" then Browserctl::Commands::Flow.run(client, args)
|
|
214
|
+
when "page" then Browserctl::Commands::Page.run(client, args)
|
|
215
|
+
when "cookie" then Browserctl::Commands::Cookie.run(client, args)
|
|
216
|
+
when "storage" then Browserctl::Commands::Storage.run(client, args)
|
|
217
|
+
when "state" then Browserctl::Commands::State.run(client, args)
|
|
218
|
+
when "daemon" then Browserctl::Commands::Daemon.run(client, args)
|
|
219
|
+
when "auth-check"
|
|
220
|
+
page_name = args.shift or abort "usage: browserctl auth-check <page> [--cookies] [--state NAME] [--flow NAME]"
|
|
221
|
+
include_cookies = args.delete("--cookies") ? true : false
|
|
222
|
+
state_idx = args.index("--state")
|
|
223
|
+
state_arg = if state_idx
|
|
224
|
+
(args.delete_at(state_idx)
|
|
225
|
+
args.delete_at(state_idx))
|
|
226
|
+
end
|
|
227
|
+
flow_idx = args.index("--flow")
|
|
228
|
+
flow_arg = if flow_idx
|
|
229
|
+
(args.delete_at(flow_idx)
|
|
230
|
+
args.delete_at(flow_idx))
|
|
231
|
+
end
|
|
232
|
+
print_result(client.auth_check(page_name, include_cookies: include_cookies,
|
|
233
|
+
state: state_arg, suggested_flow: flow_arg))
|
|
234
|
+
when "navigate" then print_result(client.navigate(args[0], args[1]))
|
|
235
|
+
when "fill" then Browserctl::Commands::Fill.run(client, args)
|
|
236
|
+
when "click" then Browserctl::Commands::Click.run(client, args)
|
|
237
|
+
when "evaluate" then print_result(client.evaluate(args[0], args[1]))
|
|
238
|
+
when "url" then print_result(client.url(args[0]))
|
|
239
|
+
when "wait"
|
|
240
|
+
opts = Optimist.options(args) do
|
|
241
|
+
opt :timeout, "Seconds to wait (default: 30)", default: 30.0, short: "-t"
|
|
242
|
+
end
|
|
243
|
+
name = args.shift
|
|
244
|
+
selector = args.shift
|
|
245
|
+
abort "usage: browserctl wait <page> <selector> [--timeout N]" unless name && selector
|
|
246
|
+
print_result(client.wait(name, selector, timeout: opts[:timeout]))
|
|
247
|
+
when "pause"
|
|
248
|
+
opts = Optimist.options(args) do
|
|
249
|
+
opt :message, "Message shown to human", type: :string, short: "-m"
|
|
250
|
+
end
|
|
251
|
+
name = args.shift or abort "usage: browserctl pause <page> [--message MSG]"
|
|
252
|
+
res = client.pause(name, message: opts[:message])
|
|
253
|
+
if res[:error]
|
|
254
|
+
warn "Error: #{res[:error]}"
|
|
255
|
+
exit 1
|
|
256
|
+
end
|
|
257
|
+
print_result(res.merge(paused: name, message: opts[:message])) do
|
|
258
|
+
lines = ["Page '#{name}' paused. Browser is live — interact freely."]
|
|
259
|
+
lines << "(#{opts[:message]})" if opts[:message]
|
|
260
|
+
lines << "When done: browserctl resume #{name}"
|
|
261
|
+
lines.join("\n")
|
|
262
|
+
end
|
|
263
|
+
when "resume" then Browserctl::Commands::Resume.run(client, args)
|
|
264
|
+
when "press"
|
|
265
|
+
name = args.shift or abort "usage: browserctl press <page> <key>"
|
|
266
|
+
key = args.shift or abort "usage: browserctl press <page> <key>"
|
|
267
|
+
print_result(client.press(name, key))
|
|
268
|
+
when "hover"
|
|
269
|
+
name = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
270
|
+
selector = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
271
|
+
print_result(client.hover(name, selector))
|
|
272
|
+
when "upload"
|
|
273
|
+
name = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
274
|
+
selector = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
275
|
+
path = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
276
|
+
print_result(client.upload(name, selector, path))
|
|
277
|
+
when "select"
|
|
278
|
+
name = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
279
|
+
selector = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
280
|
+
value = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
281
|
+
print_result(client.select(name, selector, value))
|
|
282
|
+
when "dialog" then Browserctl::Commands::Dialog.run(client, args)
|
|
283
|
+
when "devtools"
|
|
284
|
+
name = args.shift or abort "usage: browserctl devtools <page>"
|
|
285
|
+
res = client.devtools(name)
|
|
286
|
+
if res[:error]
|
|
287
|
+
warn "Error: #{res[:error]}"
|
|
288
|
+
exit 1
|
|
289
|
+
end
|
|
290
|
+
url = res[:devtools_url]
|
|
291
|
+
print_result(res) do
|
|
292
|
+
"Opening DevTools for '#{name}':\n #{url}"
|
|
293
|
+
end
|
|
294
|
+
opener = RUBY_PLATFORM =~ /darwin/ ? "open" : "xdg-open"
|
|
295
|
+
system(opener, url)
|
|
296
|
+
else
|
|
297
|
+
abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
|
|
201
298
|
end
|
|
202
|
-
name = args.shift
|
|
203
|
-
selector = args.shift
|
|
204
|
-
abort "usage: browserctl wait <page> <selector> [--timeout N]" unless name && selector
|
|
205
|
-
print_result(client.wait(name, selector, timeout: opts[:timeout]))
|
|
206
|
-
when "pause"
|
|
207
|
-
opts = Optimist.options(args) do
|
|
208
|
-
opt :message, "Message shown to human", type: :string, short: "-m"
|
|
209
|
-
end
|
|
210
|
-
name = args.shift or abort "usage: browserctl pause <page> [--message MSG]"
|
|
211
|
-
res = client.pause(name, message: opts[:message])
|
|
212
|
-
if res[:error]
|
|
213
|
-
warn "Error: #{res[:error]}"
|
|
214
|
-
exit 1
|
|
215
|
-
end
|
|
216
|
-
puts "Page '#{name}' paused. Browser is live — interact freely."
|
|
217
|
-
puts "(#{opts[:message]})" if opts[:message]
|
|
218
|
-
puts "When done: browserctl resume #{name}"
|
|
219
|
-
when "resume" then Browserctl::Commands::Resume.run(client, args)
|
|
220
|
-
when "press"
|
|
221
|
-
name = args.shift or abort "usage: browserctl press <page> <key>"
|
|
222
|
-
key = args.shift or abort "usage: browserctl press <page> <key>"
|
|
223
|
-
print_result(client.press(name, key))
|
|
224
|
-
when "hover"
|
|
225
|
-
name = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
226
|
-
selector = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
227
|
-
print_result(client.hover(name, selector))
|
|
228
|
-
when "upload"
|
|
229
|
-
name = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
230
|
-
selector = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
231
|
-
path = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
232
|
-
print_result(client.upload(name, selector, path))
|
|
233
|
-
when "select"
|
|
234
|
-
name = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
235
|
-
selector = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
236
|
-
value = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
237
|
-
print_result(client.select(name, selector, value))
|
|
238
|
-
when "dialog" then Browserctl::Commands::Dialog.run(client, args)
|
|
239
|
-
when "devtools"
|
|
240
|
-
name = args.shift or abort "usage: browserctl devtools <page>"
|
|
241
|
-
res = client.devtools(name)
|
|
242
|
-
if res[:error]
|
|
243
|
-
warn "Error: #{res[:error]}"
|
|
244
|
-
exit 1
|
|
245
|
-
end
|
|
246
|
-
url = res[:devtools_url]
|
|
247
|
-
puts "Opening DevTools for '#{name}':"
|
|
248
|
-
puts " #{url}"
|
|
249
|
-
opener = RUBY_PLATFORM =~ /darwin/ ? "open" : "xdg-open"
|
|
250
|
-
system(opener, url)
|
|
251
|
-
else
|
|
252
|
-
abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
|
|
253
299
|
end
|
|
300
|
+
rescue Browserctl::Error => e
|
|
301
|
+
payload = {
|
|
302
|
+
code: e.code,
|
|
303
|
+
message: e.message,
|
|
304
|
+
context: e.respond_to?(:context) ? (e.context || {}) : {},
|
|
305
|
+
suggested_action: Browserctl::Error::SuggestedActions.for(e.code)
|
|
306
|
+
}
|
|
307
|
+
warn "Error: #{e.message}"
|
|
308
|
+
warn JSON.generate(payload)
|
|
309
|
+
exit Browserctl::Error::ExitCodes.for(e.code)
|
|
254
310
|
end
|
data/bin/browserd
CHANGED
|
@@ -5,6 +5,7 @@ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
|
5
5
|
|
|
6
6
|
require "optimist"
|
|
7
7
|
require "nokogiri"
|
|
8
|
+
require "browserctl/crash_report"
|
|
8
9
|
require "browserctl/logger"
|
|
9
10
|
require "browserctl/server"
|
|
10
11
|
require "browserctl/version"
|
|
@@ -35,7 +36,9 @@ end
|
|
|
35
36
|
|
|
36
37
|
log_path = Browserctl.log_path(assigned_name)
|
|
37
38
|
warn "browserd starting — log: #{log_path}"
|
|
38
|
-
|
|
39
|
+
warn " if browserd crashes, attach the crash report from ~/.browserctl/logs/crash-*.json"
|
|
40
|
+
Browserctl.logger = Browserctl.build_logger(opts[:log_level], log_path: log_path, component: "daemon")
|
|
41
|
+
daemon_jsonl_log = File.join(Browserctl.log_dir, "daemon.log")
|
|
39
42
|
begin
|
|
40
43
|
Browserctl::Server.new(
|
|
41
44
|
headless: !opts[:headed],
|
|
@@ -45,4 +48,8 @@ begin
|
|
|
45
48
|
).run
|
|
46
49
|
rescue Browserctl::BrowserNotFound => e
|
|
47
50
|
abort e.message
|
|
51
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
52
|
+
crash_path = Browserctl::CrashReport.write(error: e, log_path: daemon_jsonl_log)
|
|
53
|
+
warn "browserd crashed — crash report: #{crash_path}" if crash_path
|
|
54
|
+
raise
|
|
48
55
|
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "secret_resolvers"
|
|
6
|
+
|
|
7
|
+
module Browserctl
|
|
8
|
+
# Shared base for {Flow} and {WorkflowDefinition}. Holds the duplicated
|
|
9
|
+
# DSL surface (`desc`, `param`, `step`) and the shared execution helpers
|
|
10
|
+
# for resolving params and running step blocks with retry + timeout.
|
|
11
|
+
#
|
|
12
|
+
# Subclasses provide:
|
|
13
|
+
# - their own `call`/`run` entry point and context object;
|
|
14
|
+
# - {#callable_kind} so cross-type composition can be rejected at
|
|
15
|
+
# definition time (e.g. a flow trying to `compose` a workflow);
|
|
16
|
+
# - {#step_failure_message} for the typed error wording.
|
|
17
|
+
#
|
|
18
|
+
# The persistence DSL (`store`/`fetch`/`save_state`/`load_state`) is
|
|
19
|
+
# mixed into `WorkflowContext` via {ContextualPersistence} and is
|
|
20
|
+
# deliberately absent from `FlowContext` — flows return state, workflows
|
|
21
|
+
# share state.
|
|
22
|
+
class CallableDefinition
|
|
23
|
+
ParamDef = Struct.new(:name, :required, :secret, :default, :secret_ref, keyword_init: true)
|
|
24
|
+
StepDef = Struct.new(:label, :block, :retry_count, :timeout, keyword_init: true)
|
|
25
|
+
|
|
26
|
+
attr_reader :name, :description, :param_defs, :steps
|
|
27
|
+
|
|
28
|
+
def initialize(name)
|
|
29
|
+
@name = name.to_s
|
|
30
|
+
@description = nil
|
|
31
|
+
@param_defs = {}
|
|
32
|
+
@steps = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def desc(text)
|
|
36
|
+
@description = text.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def param(name, required: false, secret: false, default: nil, secret_ref: nil)
|
|
40
|
+
secret = true if secret_ref
|
|
41
|
+
@param_defs[name] = ParamDef.new(
|
|
42
|
+
name: name,
|
|
43
|
+
required: required,
|
|
44
|
+
secret: secret,
|
|
45
|
+
default: default,
|
|
46
|
+
secret_ref: secret_ref
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def step(label, retry_count: 0, timeout: nil, &block)
|
|
51
|
+
raise ArgumentError, "#{callable_kind} step '#{label}' requires a block" unless block
|
|
52
|
+
|
|
53
|
+
@steps << StepDef.new(label: label, block: block, retry_count: retry_count, timeout: timeout)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Subclasses override these to specialise the base behaviour.
|
|
57
|
+
|
|
58
|
+
# @return [Symbol] :flow or :workflow — used for cross-type composition checks.
|
|
59
|
+
def callable_kind
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def resolve_params(provided)
|
|
66
|
+
@param_defs.each_with_object({}) do |(name, defn), out|
|
|
67
|
+
val = if defn.secret_ref
|
|
68
|
+
SecretResolverRegistry.resolve(defn.secret_ref)
|
|
69
|
+
elsif provided.key?(name)
|
|
70
|
+
provided[name]
|
|
71
|
+
else
|
|
72
|
+
defn.default
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
raise missing_param_error(name) if defn.required && val.nil?
|
|
76
|
+
|
|
77
|
+
out[name] = val
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Override for typed error wording.
|
|
82
|
+
def missing_param_error(_name)
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Runs a step block with retry + timeout. Yields the recoverable error
|
|
87
|
+
# to the caller (via the returned exception in `:error`) so subclasses
|
|
88
|
+
# can decide how to surface a failure (raise vs StepResult).
|
|
89
|
+
def execute_step_block(ctx, defn)
|
|
90
|
+
if defn.timeout
|
|
91
|
+
::Timeout.timeout(defn.timeout) { ctx.instance_exec(&defn.block) }
|
|
92
|
+
else
|
|
93
|
+
ctx.instance_exec(&defn.block)
|
|
94
|
+
end
|
|
95
|
+
rescue ::Timeout::Error
|
|
96
|
+
raise step_timeout_error(defn)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def with_retries(defn)
|
|
100
|
+
last_error = nil
|
|
101
|
+
(defn.retry_count + 1).times do
|
|
102
|
+
return yield
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
last_error = e
|
|
105
|
+
end
|
|
106
|
+
[:error, last_error]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Subclasses override to raise their typed timeout error.
|
|
110
|
+
def step_timeout_error(_defn)
|
|
111
|
+
raise NotImplementedError
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|