browserctl 0.11.0 → 0.12.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 +28 -0
- data/README.md +1 -0
- data/bin/browserctl +143 -94
- data/bin/browserd +8 -1
- data/lib/browserctl/client.rb +3 -3
- data/lib/browserctl/commands/cli_output.rb +21 -1
- data/lib/browserctl/commands/migrate.rb +94 -0
- data/lib/browserctl/commands/trace.rb +187 -0
- data/lib/browserctl/constants.rb +3 -1
- data/lib/browserctl/crash_report.rb +96 -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/format_version.rb +37 -0
- data/lib/browserctl/logger.rb +102 -9
- data/lib/browserctl/migrations.rb +216 -0
- data/lib/browserctl/recording.rb +34 -2
- 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 +3 -0
- 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/session.rb +1 -1
- data/lib/browserctl/state/bundle.rb +43 -2
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow/flow_wrapper.rb +1 -1
- data/lib/browserctl/workflow.rb +56 -1
- metadata +15 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a7eb052c2bbfc4e1f5afc24ba08c5b2b0d471a85d2acf6ea61a7f160cbe201b
|
|
4
|
+
data.tar.gz: 2d4259da9b4a13a50ad80e68d404f11eda89b6249e5315c58247c8d80514d21f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca34ca3125de0686417b06919a8cb0eaf3311eaedd7ebe6538c1f159941d737ca549f3d0c3d8a10da73146bd23602cb145ed010539a48de97c3d8552d4f317c9
|
|
7
|
+
data.tar.gz: 61c89f8a4b2e61076ddd5fa603710e226ebfe1e176914ffb37f47a79ee35a2161b3700406a35c878d6fdc6cb129f051f00943d080b636076f6d1452b6ee290b3
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,34 @@ 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.12.0](https://github.com/patrick204nqh/browserctl/compare/v0.11.0...v0.12.0) (2026-05-10)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### ⚠ BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* 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.
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* benchmark harness + bundle_codec target ([#154](https://github.com/patrick204nqh/browserctl/issues/154)) ([f4b815a](https://github.com/patrick204nqh/browserctl/commit/f4b815a617d4a7d22473e6028ce2a0f90467fa87))
|
|
23
|
+
* bundle format version ([#132](https://github.com/patrick204nqh/browserctl/issues/132)) ([b52d325](https://github.com/patrick204nqh/browserctl/commit/b52d32546d1b9ec3915fac014fecd8f929161ddb))
|
|
24
|
+
* crash reports ([#135](https://github.com/patrick204nqh/browserctl/issues/135)) ([b02adf4](https://github.com/patrick204nqh/browserctl/commit/b02adf4d41295dbe547d2965d838168d48efb8bf))
|
|
25
|
+
* error code enum ([#128](https://github.com/patrick204nqh/browserctl/issues/128)) ([feb0522](https://github.com/patrick204nqh/browserctl/commit/feb0522887935737fd96ad8a8b47d1407a5f585e))
|
|
26
|
+
* exit code map ([#140](https://github.com/patrick204nqh/browserctl/issues/140)) ([1a1d2b2](https://github.com/patrick204nqh/browserctl/commit/1a1d2b2f50693863cb508b5354e5546f340a2771))
|
|
27
|
+
* format version header convention ([#129](https://github.com/patrick204nqh/browserctl/issues/129)) ([405774d](https://github.com/patrick204nqh/browserctl/commit/405774dd6c99cc96f62f0c092010b30e7e710d41))
|
|
28
|
+
* migration helpers ([#143](https://github.com/patrick204nqh/browserctl/issues/143)) ([1c1762e](https://github.com/patrick204nqh/browserctl/commit/1c1762e755f83445c7cdd0df75fc5324a820fa19))
|
|
29
|
+
* recording format version ([#136](https://github.com/patrick204nqh/browserctl/issues/136)) ([4f1bc46](https://github.com/patrick204nqh/browserctl/commit/4f1bc467e653ecc1f508a85b3c329adf048b783d))
|
|
30
|
+
* structured error payload ([#137](https://github.com/patrick204nqh/browserctl/issues/137)) ([f028410](https://github.com/patrick204nqh/browserctl/commit/f02841069f68de749a76ba9a98a38b0c20836d76))
|
|
31
|
+
* structured JSONL logs ([#131](https://github.com/patrick204nqh/browserctl/issues/131)) ([c6daff4](https://github.com/patrick204nqh/browserctl/commit/c6daff488460606659c0641dd8cee6c23561be05))
|
|
32
|
+
* trace --redact ([#139](https://github.com/patrick204nqh/browserctl/issues/139)) ([19b4cec](https://github.com/patrick204nqh/browserctl/commit/19b4cec37daf95a06b0513780d991d0522069593))
|
|
33
|
+
* trace command ([#133](https://github.com/patrick204nqh/browserctl/issues/133)) ([156f304](https://github.com/patrick204nqh/browserctl/commit/156f304246d24fa6c3e06f8d941ac971629ea77c))
|
|
34
|
+
* workflow format version ([#138](https://github.com/patrick204nqh/browserctl/issues/138)) ([963776b](https://github.com/patrick204nqh/browserctl/commit/963776be785583fc3a3b0c04c97510e16c73ca9a))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Miscellaneous Chores
|
|
38
|
+
|
|
39
|
+
* target next release as 0.12.0 ([#156](https://github.com/patrick204nqh/browserctl/issues/156)) ([9797fcc](https://github.com/patrick204nqh/browserctl/commit/9797fcc0bae1a47bb72eec9cec5fda105e698406))
|
|
40
|
+
|
|
13
41
|
## [0.11.0](https://github.com/patrick204nqh/browserctl/compare/v0.10.0...v0.11.0) (2026-05-10)
|
|
14
42
|
|
|
15
43
|
|
data/README.md
CHANGED
|
@@ -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
|
@@ -31,14 +31,29 @@ 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"
|
|
36
|
+
|
|
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
|
|
34
44
|
|
|
35
45
|
def print_result(res)
|
|
36
|
-
|
|
37
|
-
warn "Error: #{res[:error] || res['error']}"
|
|
46
|
+
unless res.is_a?(Hash) && (res[:error] || res["error"])
|
|
38
47
|
puts res.to_json
|
|
39
|
-
|
|
48
|
+
return
|
|
40
49
|
end
|
|
50
|
+
|
|
51
|
+
message = res[:error] || res["error"]
|
|
52
|
+
code, payload = structured_stderr_payload(res, message)
|
|
53
|
+
warn "Error: #{message}"
|
|
54
|
+
warn payload
|
|
41
55
|
puts res.to_json
|
|
56
|
+
exit Browserctl::Error::ExitCodes.for(code)
|
|
42
57
|
end
|
|
43
58
|
|
|
44
59
|
def usage
|
|
@@ -129,6 +144,15 @@ def usage
|
|
|
129
144
|
flow list
|
|
130
145
|
flow describe <name>
|
|
131
146
|
|
|
147
|
+
Trace:
|
|
148
|
+
trace [<session>] Pretty timeline of CLI + daemon log events.
|
|
149
|
+
|
|
150
|
+
Migrate:
|
|
151
|
+
migrate <path> [--to-version N] [--dry-run]
|
|
152
|
+
# Upgrades a persisted artifact (.bctl / .jsonl recording / workflow .rb)
|
|
153
|
+
# using registered migrations. Empty registry in v0.12 — first real
|
|
154
|
+
# migration ships post-1.0 when a format actually changes.
|
|
155
|
+
|
|
132
156
|
Daemon:
|
|
133
157
|
daemon start [--headed] [--name NAME]
|
|
134
158
|
daemon stop
|
|
@@ -149,6 +173,17 @@ daemon_name = if daemon_idx
|
|
|
149
173
|
ARGV.delete_at(daemon_idx)
|
|
150
174
|
end
|
|
151
175
|
|
|
176
|
+
log_level_idx = ARGV.index("--log-level") || ARGV.index("-l")
|
|
177
|
+
log_level = if log_level_idx
|
|
178
|
+
ARGV.delete_at(log_level_idx)
|
|
179
|
+
ARGV.delete_at(log_level_idx)
|
|
180
|
+
end || ENV["BROWSERCTL_LOG_LEVEL"] || "info"
|
|
181
|
+
|
|
182
|
+
# CLI invocations write structured JSON Lines to ~/.browserctl/logs/cli.log
|
|
183
|
+
# alongside the existing stderr behaviour. Stderr stays human-readable so
|
|
184
|
+
# scripted callers see no change.
|
|
185
|
+
Browserctl.logger = Browserctl.build_logger(log_level, component: "cli")
|
|
186
|
+
|
|
152
187
|
cmd = ARGV.shift
|
|
153
188
|
args = ARGV.dup
|
|
154
189
|
|
|
@@ -156,99 +191,113 @@ usage if cmd.nil? || %w[-h --help help].include?(cmd)
|
|
|
156
191
|
|
|
157
192
|
runner = Browserctl::Runner.new
|
|
158
193
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
when "
|
|
162
|
-
when "
|
|
163
|
-
when "
|
|
194
|
+
begin
|
|
195
|
+
case cmd
|
|
196
|
+
when "workflow" then Browserctl::Commands::Workflow.run(runner, args)
|
|
197
|
+
when "record" then Browserctl::Commands::Record.run(args)
|
|
198
|
+
when "init" then Browserctl::Commands::Init.run(args)
|
|
199
|
+
when "ask" then Browserctl::Commands::Ask.run(args)
|
|
200
|
+
when "trace" then Browserctl::Commands::Trace.run(args)
|
|
201
|
+
when "migrate" then Browserctl::Commands::Migrate.run(args)
|
|
164
202
|
|
|
165
|
-
else
|
|
166
|
-
|
|
203
|
+
else
|
|
204
|
+
client = Browserctl::Client.new(Browserctl.socket_path(daemon_name))
|
|
167
205
|
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
case cmd
|
|
207
|
+
when "flow" then Browserctl::Commands::Flow.run(client, args)
|
|
208
|
+
when "page" then Browserctl::Commands::Page.run(client, args)
|
|
209
|
+
when "cookie" then Browserctl::Commands::Cookie.run(client, args)
|
|
210
|
+
when "storage" then Browserctl::Commands::Storage.run(client, args)
|
|
211
|
+
when "session" then Browserctl::Commands::Session.run(client, args)
|
|
212
|
+
when "state" then Browserctl::Commands::State.run(client, args)
|
|
213
|
+
when "daemon" then Browserctl::Commands::Daemon.run(client, args)
|
|
214
|
+
when "auth-check"
|
|
215
|
+
page_name = args.shift or abort "usage: browserctl auth-check <page> [--cookies] [--state NAME] [--flow NAME]"
|
|
216
|
+
include_cookies = args.delete("--cookies") ? true : false
|
|
217
|
+
state_idx = args.index("--state")
|
|
218
|
+
state_arg = if state_idx
|
|
219
|
+
(args.delete_at(state_idx)
|
|
220
|
+
args.delete_at(state_idx))
|
|
221
|
+
end
|
|
222
|
+
flow_idx = args.index("--flow")
|
|
223
|
+
flow_arg = if flow_idx
|
|
224
|
+
(args.delete_at(flow_idx)
|
|
225
|
+
args.delete_at(flow_idx))
|
|
226
|
+
end
|
|
227
|
+
print_result(client.auth_check(page_name, include_cookies: include_cookies,
|
|
228
|
+
state: state_arg, suggested_flow: flow_arg))
|
|
229
|
+
when "navigate" then print_result(client.navigate(args[0], args[1]))
|
|
230
|
+
when "fill" then Browserctl::Commands::Fill.run(client, args)
|
|
231
|
+
when "click" then Browserctl::Commands::Click.run(client, args)
|
|
232
|
+
when "snapshot" then Browserctl::Commands::Snapshot.run(client, args)
|
|
233
|
+
when "screenshot" then Browserctl::Commands::Screenshot.run(client, args)
|
|
234
|
+
when "evaluate" then print_result(client.evaluate(args[0], args[1]))
|
|
235
|
+
when "url" then print_result(client.url(args[0]))
|
|
236
|
+
when "wait"
|
|
237
|
+
opts = Optimist.options(args) do
|
|
238
|
+
opt :timeout, "Seconds to wait (default: 30)", default: 30.0, short: "-t"
|
|
239
|
+
end
|
|
240
|
+
name = args.shift
|
|
241
|
+
selector = args.shift
|
|
242
|
+
abort "usage: browserctl wait <page> <selector> [--timeout N]" unless name && selector
|
|
243
|
+
print_result(client.wait(name, selector, timeout: opts[:timeout]))
|
|
244
|
+
when "pause"
|
|
245
|
+
opts = Optimist.options(args) do
|
|
246
|
+
opt :message, "Message shown to human", type: :string, short: "-m"
|
|
247
|
+
end
|
|
248
|
+
name = args.shift or abort "usage: browserctl pause <page> [--message MSG]"
|
|
249
|
+
res = client.pause(name, message: opts[:message])
|
|
250
|
+
if res[:error]
|
|
251
|
+
warn "Error: #{res[:error]}"
|
|
252
|
+
exit 1
|
|
253
|
+
end
|
|
254
|
+
puts "Page '#{name}' paused. Browser is live — interact freely."
|
|
255
|
+
puts "(#{opts[:message]})" if opts[:message]
|
|
256
|
+
puts "When done: browserctl resume #{name}"
|
|
257
|
+
when "resume" then Browserctl::Commands::Resume.run(client, args)
|
|
258
|
+
when "press"
|
|
259
|
+
name = args.shift or abort "usage: browserctl press <page> <key>"
|
|
260
|
+
key = args.shift or abort "usage: browserctl press <page> <key>"
|
|
261
|
+
print_result(client.press(name, key))
|
|
262
|
+
when "hover"
|
|
263
|
+
name = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
264
|
+
selector = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
265
|
+
print_result(client.hover(name, selector))
|
|
266
|
+
when "upload"
|
|
267
|
+
name = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
268
|
+
selector = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
269
|
+
path = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
270
|
+
print_result(client.upload(name, selector, path))
|
|
271
|
+
when "select"
|
|
272
|
+
name = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
273
|
+
selector = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
274
|
+
value = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
275
|
+
print_result(client.select(name, selector, value))
|
|
276
|
+
when "dialog" then Browserctl::Commands::Dialog.run(client, args)
|
|
277
|
+
when "devtools"
|
|
278
|
+
name = args.shift or abort "usage: browserctl devtools <page>"
|
|
279
|
+
res = client.devtools(name)
|
|
280
|
+
if res[:error]
|
|
281
|
+
warn "Error: #{res[:error]}"
|
|
282
|
+
exit 1
|
|
283
|
+
end
|
|
284
|
+
url = res[:devtools_url]
|
|
285
|
+
puts "Opening DevTools for '#{name}':"
|
|
286
|
+
puts " #{url}"
|
|
287
|
+
opener = RUBY_PLATFORM =~ /darwin/ ? "open" : "xdg-open"
|
|
288
|
+
system(opener, url)
|
|
289
|
+
else
|
|
290
|
+
abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
|
|
209
291
|
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
292
|
end
|
|
293
|
+
rescue Browserctl::Error => e
|
|
294
|
+
payload = {
|
|
295
|
+
code: e.code,
|
|
296
|
+
message: e.message,
|
|
297
|
+
context: e.respond_to?(:context) ? (e.context || {}) : {},
|
|
298
|
+
suggested_action: Browserctl::Error::SuggestedActions.for(e.code)
|
|
299
|
+
}
|
|
300
|
+
warn "Error: #{e.message}"
|
|
301
|
+
warn JSON.generate(payload)
|
|
302
|
+
exit Browserctl::Error::ExitCodes.for(e.code)
|
|
254
303
|
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
|
data/lib/browserctl/client.rb
CHANGED
|
@@ -182,7 +182,7 @@ module Browserctl
|
|
|
182
182
|
# @param path [String] file path to read cookies from
|
|
183
183
|
# @return [Hash] `{ ok: true, count: }` or `{ error: }`
|
|
184
184
|
def import_cookies(name, path)
|
|
185
|
-
raise "cookie file not found: #{path}" unless File.exist?(path)
|
|
185
|
+
raise Browserctl::Error, "cookie file not found: #{path}" unless File.exist?(path)
|
|
186
186
|
|
|
187
187
|
cookies = JSON.parse(File.read(path), symbolize_names: true)
|
|
188
188
|
call("import_cookies", name: name, cookies: cookies)
|
|
@@ -371,10 +371,10 @@ module Browserctl
|
|
|
371
371
|
end
|
|
372
372
|
|
|
373
373
|
def read_response(sock)
|
|
374
|
-
raise "browserd response timeout after 60s" unless sock.wait_readable(60)
|
|
374
|
+
raise DaemonUnavailableError, "browserd response timeout after 60s" unless sock.wait_readable(60)
|
|
375
375
|
|
|
376
376
|
raw = sock.gets
|
|
377
|
-
raise "browserd closed connection" unless raw
|
|
377
|
+
raise DaemonUnavailableError, "browserd closed connection" unless raw
|
|
378
378
|
|
|
379
379
|
JSON.parse(raw.chomp, symbolize_names: true)
|
|
380
380
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require_relative "../errors"
|
|
5
|
+
require_relative "../error/suggested_actions"
|
|
4
6
|
|
|
5
7
|
module Browserctl
|
|
6
8
|
module Commands
|
|
@@ -9,7 +11,9 @@ module Browserctl
|
|
|
9
11
|
|
|
10
12
|
def print_result(res)
|
|
11
13
|
if res.is_a?(Hash) && (res[:error] || res["error"])
|
|
12
|
-
|
|
14
|
+
message = res[:error] || res["error"]
|
|
15
|
+
warn "Error: #{message}"
|
|
16
|
+
warn structured_error_line(res, message)
|
|
13
17
|
puts res.to_json
|
|
14
18
|
exit exit_code_for(res)
|
|
15
19
|
end
|
|
@@ -23,6 +27,22 @@ module Browserctl
|
|
|
23
27
|
|
|
24
28
|
1
|
|
25
29
|
end
|
|
30
|
+
|
|
31
|
+
# Builds the single-line structured payload emitted to stderr after
|
|
32
|
+
# the human-readable line. Agents parse this JSON deterministically.
|
|
33
|
+
# Shape: { code, message, context, suggested_action }.
|
|
34
|
+
def structured_error_line(res, message)
|
|
35
|
+
code = (res[:code] || res["code"] || Browserctl::Error::Codes::GENERIC).to_s
|
|
36
|
+
context = res[:context] || res["context"] || {}
|
|
37
|
+
action = res[:suggested_action] || res["suggested_action"] ||
|
|
38
|
+
Browserctl::Error::SuggestedActions.for(code)
|
|
39
|
+
JSON.generate(
|
|
40
|
+
code: code,
|
|
41
|
+
message: message,
|
|
42
|
+
context: context,
|
|
43
|
+
suggested_action: action
|
|
44
|
+
)
|
|
45
|
+
end
|
|
26
46
|
end
|
|
27
47
|
end
|
|
28
48
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../migrations"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
require_relative "../error/codes"
|
|
6
|
+
require_relative "../error/exit_codes"
|
|
7
|
+
|
|
8
|
+
module Browserctl
|
|
9
|
+
module Commands
|
|
10
|
+
# `browserctl migrate <path> [--to-version N] [--dry-run]` — operator
|
|
11
|
+
# entry point for the {Browserctl::Migrations} registry. Detects the
|
|
12
|
+
# artifact's format and version, plans a chain of registered upgraders,
|
|
13
|
+
# and applies them in order (unless `--dry-run`).
|
|
14
|
+
#
|
|
15
|
+
# The registry ships empty in v0.12; this command exists so operators
|
|
16
|
+
# have a stable invocation the moment a real migration lands. On an
|
|
17
|
+
# already-current artifact the command is a no-op and exits 0.
|
|
18
|
+
module Migrate
|
|
19
|
+
USAGE = "Usage: browserctl migrate <path> [--to-version N] [--dry-run]"
|
|
20
|
+
|
|
21
|
+
def self.run(args, out: $stdout, err: $stderr)
|
|
22
|
+
abort USAGE if args.empty? || args.include?("-h") || args.include?("--help")
|
|
23
|
+
args = args.dup
|
|
24
|
+
|
|
25
|
+
dry_run = !args.delete("--dry-run").nil?
|
|
26
|
+
target_idx = args.index("--to-version")
|
|
27
|
+
target = if target_idx
|
|
28
|
+
args.delete_at(target_idx)
|
|
29
|
+
Integer(args.delete_at(target_idx))
|
|
30
|
+
end
|
|
31
|
+
path = args.shift
|
|
32
|
+
abort USAGE unless path
|
|
33
|
+
|
|
34
|
+
unless File.exist?(path)
|
|
35
|
+
err.puts "Error: file not found: #{path}"
|
|
36
|
+
exit Browserctl::Error::ExitCodes::GENERIC
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
execute(path, target_version: target, dry_run: dry_run, out: out, err: err)
|
|
40
|
+
rescue Browserctl::ProtocolMismatch => e
|
|
41
|
+
err.puts "Error: #{e.message}"
|
|
42
|
+
exit Browserctl::Error::ExitCodes.for(e.code)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.execute(path, target_version:, dry_run:, out:, err:)
|
|
46
|
+
format = Browserctl::Migrations.detect_format(path)
|
|
47
|
+
unless format
|
|
48
|
+
err.puts "Error: could not detect format for #{path} (expected .bctl, .jsonl, or .rb)"
|
|
49
|
+
exit Browserctl::Error::ExitCodes::PROTOCOL_MISMATCH
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
current = Browserctl::Migrations.detect_version(path, format)
|
|
53
|
+
out.puts "Detected: format=#{format} version=#{current.inspect} path=#{path}"
|
|
54
|
+
|
|
55
|
+
if dry_run
|
|
56
|
+
plan_dry_run(format, current, target_version, out)
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result = Browserctl::Migrations.run(path, target_version: target_version)
|
|
61
|
+
if result.applied.empty?
|
|
62
|
+
out.puts "No migrations registered for #{format} v#{current}; nothing to do."
|
|
63
|
+
else
|
|
64
|
+
out.puts "Applied #{result.applied.size} migration(s): #{result.from} -> #{result.to}"
|
|
65
|
+
result.applied.each { |m| out.puts " - #{format} v#{m.from_version} -> v#{m.to_version}" }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.plan_dry_run(format, current, target_version, out)
|
|
70
|
+
target = target_version || latest_target(format, current)
|
|
71
|
+
chain = Browserctl::Migrations.find_path(format: format, from: current, to: target)
|
|
72
|
+
|
|
73
|
+
if chain.nil?
|
|
74
|
+
out.puts "No migration path #{format} v#{current} -> v#{target} (registered: " \
|
|
75
|
+
"#{registered_for(format).inspect})"
|
|
76
|
+
elsif chain.empty?
|
|
77
|
+
out.puts "Already at v#{target}; no migrations would run."
|
|
78
|
+
else
|
|
79
|
+
out.puts "Plan (#{chain.size} step(s)):"
|
|
80
|
+
chain.each { |m| out.puts " - #{format} v#{m.from_version} -> v#{m.to_version}" }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.latest_target(format, current)
|
|
85
|
+
targets = registered_for(format)
|
|
86
|
+
targets.empty? ? current : targets.max
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.registered_for(format)
|
|
90
|
+
Browserctl::Migrations.all.select { |m| m.format == format }.map(&:to_version)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|