browserctl 0.14.0 → 0.15.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 +19 -0
- data/README.md +7 -3
- data/bin/browserctl +10 -2
- data/examples/tracing_otel.rb +46 -0
- data/lib/browserctl/callable_definition.rb +2 -2
- data/lib/browserctl/client.rb +41 -0
- data/lib/browserctl/commands/cookie.rb +17 -0
- data/lib/browserctl/commands/data.rb +73 -0
- data/lib/browserctl/commands/deprecation_notice.rb +33 -0
- data/lib/browserctl/commands/storage.rb +17 -0
- data/lib/browserctl/detectors/auth_required.rb +1 -1
- data/lib/browserctl/driver/cdp.rb +2 -2
- data/lib/browserctl/driver/ferrum_page_driver.rb +43 -0
- data/lib/browserctl/driver/page_driver.rb +90 -0
- data/lib/browserctl/error/codes.rb +15 -0
- data/lib/browserctl/error/exit_codes.rb +9 -1
- data/lib/browserctl/error/suggested_actions.rb +7 -0
- data/lib/browserctl/flow.rb +1 -1
- data/lib/browserctl/flows/stdlib/cloudflare_solve.rb +1 -1
- data/lib/browserctl/migrations.rb +2 -2
- data/lib/browserctl/replay/context.rb +1 -1
- data/lib/browserctl/replay/fingerprint_matcher.rb +1 -1
- data/lib/browserctl/server/command_dispatcher.rb +25 -17
- data/lib/browserctl/server/handlers/cookies.rb +18 -21
- data/lib/browserctl/server/handlers/data.rb +145 -0
- data/lib/browserctl/server/handlers/devtools.rb +8 -9
- data/lib/browserctl/server/handlers/hitl.rb +10 -0
- data/lib/browserctl/server/handlers/interaction.rb +21 -23
- data/lib/browserctl/server/handlers/navigation.rb +15 -15
- data/lib/browserctl/server/handlers/observation.rb +6 -6
- data/lib/browserctl/server/handlers/page_lifecycle.rb +12 -4
- data/lib/browserctl/server/handlers/state.rb +16 -6
- data/lib/browserctl/server/handlers/storage.rb +8 -8
- data/lib/browserctl/server/page_session.rb +21 -3
- data/lib/browserctl/server/plugin_dispatcher.rb +83 -0
- data/lib/browserctl/state/mutator.rb +1 -1
- data/lib/browserctl/tracing.rb +75 -0
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow/page_proxy.rb +128 -0
- data/lib/browserctl/workflow.rb +3 -117
- data/lib/browserctl.rb +28 -2
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68f01c2bd0b65b0b6ac9243df7986b7d99c7d6a31b9ed346ae96da965b70588b
|
|
4
|
+
data.tar.gz: c1441ed479aad729c2af205313c35ea04793c1b6929a2a538584c907a982cff3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ccabb955b4cfc4581d1afe8ea87b5d2afc432f0f31ef568a6aad2b9c13b37e28bb72b4ea74eef3056d5348aef4d1072059ab84a3721966d7cccb20bc9e69f1d
|
|
7
|
+
data.tar.gz: 2169db821391d766570667b3332672860db014e8ad3b35ab26e953127bd5fa9bba05f1b99c5d0195e01e988ee49409f197ccde4405981200384d7e60b67913b8
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,25 @@ 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.15.0](https://github.com/patrick204nqh/browserctl/compare/v0.14.0...v0.15.0) (2026-05-14)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### ⚠ BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* Cookie and storage wire / CLI / client surfaces are deprecated in favour of the unified `data --scope ...` verb. Old paths still work in v0.15 but emit a one-line deprecation warning to stderr (suppressed under `--output json`) and are removed at 1.0. See docs/architecture/decisions/0021-data-verb-consolidation.md.
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* data verb consolidates cookie and storage commands (v0.15 WS-1 PR 2) ([#207](https://github.com/patrick204nqh/browserctl/issues/207)) ([4b0beca](https://github.com/patrick204nqh/browserctl/commit/4b0becaf014fe27f92dfdc48f03bd1d75e06896d))
|
|
23
|
+
* pluggable tracing hook (v0.15 WS-4 PR 7) ([#203](https://github.com/patrick204nqh/browserctl/issues/203)) ([f8f725e](https://github.com/patrick204nqh/browserctl/commit/f8f725e4022defa2c56e1c3dfda831f348bf2fc4))
|
|
24
|
+
* plugin command timeout and error isolation (v0.15 WS-2 PR 5) ([#202](https://github.com/patrick204nqh/browserctl/issues/202)) ([5bc1e36](https://github.com/patrick204nqh/browserctl/commit/5bc1e3609a1d0739369cdad7e5c5cd27f6116ec7))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* dialog handlers use with_page mutex pattern (v0.15 WS-2 PR 4) ([#193](https://github.com/patrick204nqh/browserctl/issues/193)) ([f35f513](https://github.com/patrick204nqh/browserctl/commit/f35f5137011db40b52a77008bba62e19e73ccb0d))
|
|
30
|
+
* sweep remaining [@pages](https://github.com/pages) raw access in handlers (v0.15 WS-2 PR 4b) ([#208](https://github.com/patrick204nqh/browserctl/issues/208)) ([9d57a6d](https://github.com/patrick204nqh/browserctl/commit/9d57a6d2bb0a20a06c3a04024517448cd37c290a))
|
|
31
|
+
|
|
13
32
|
## [0.14.0](https://github.com/patrick204nqh/browserctl/compare/v0.13.1...v0.14.0) (2026-05-11)
|
|
14
33
|
|
|
15
34
|
|
data/README.md
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
<h1 align="center">browserctl</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
persistent browser sessions with human-in-the-loop.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<sub>Built for AI agents. Useful to the engineers and QA folks who work with them.</sub>
|
|
9
13
|
</p>
|
|
10
14
|
|
|
11
15
|
<p align="center">
|
|
@@ -55,8 +59,8 @@ browserctl url main
|
|
|
55
59
|
browserctl page snapshot main --diff # only what changed
|
|
56
60
|
|
|
57
61
|
# Session persistence: save now, pick up later
|
|
58
|
-
browserctl
|
|
59
|
-
# On a fresh daemon tomorrow: `browserctl
|
|
62
|
+
browserctl state save my-session
|
|
63
|
+
# On a fresh daemon tomorrow: `browserctl state load my-session`
|
|
60
64
|
# → tabs restored, cookies intact, no re-login needed
|
|
61
65
|
|
|
62
66
|
# 7. Done
|
data/bin/browserctl
CHANGED
|
@@ -25,6 +25,7 @@ require "browserctl/commands/init"
|
|
|
25
25
|
require "browserctl/commands/page"
|
|
26
26
|
require "browserctl/commands/cookie"
|
|
27
27
|
require "browserctl/commands/storage"
|
|
28
|
+
require "browserctl/commands/data"
|
|
28
29
|
require "browserctl/commands/state"
|
|
29
30
|
require "browserctl/commands/daemon"
|
|
30
31
|
require "browserctl/commands/workflow"
|
|
@@ -93,14 +94,20 @@ def usage
|
|
|
93
94
|
dialog accept <page> [text]
|
|
94
95
|
dialog dismiss <page>
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
Data (v0.15+, replaces 'cookie *' and 'storage *' — removed at 1.0):
|
|
98
|
+
data get <page> <key> --scope {cookies|localStorage|sessionStorage}
|
|
99
|
+
data set <page> <key> <value> --scope SCOPE [--domain D] [--path /]
|
|
100
|
+
data delete <page> --scope SCOPE
|
|
101
|
+
data list <page> --scope SCOPE
|
|
102
|
+
|
|
103
|
+
Cookie (deprecated, removed at 1.0 — use 'data --scope cookies'):
|
|
97
104
|
cookie list <page>
|
|
98
105
|
cookie set <page> <name> <value> --domain DOMAIN [--path /]
|
|
99
106
|
cookie delete <page>
|
|
100
107
|
cookie export <page> <path>
|
|
101
108
|
cookie import <page> <path>
|
|
102
109
|
|
|
103
|
-
Storage:
|
|
110
|
+
Storage (deprecated, removed at 1.0 — use 'data --scope localStorage|sessionStorage'):
|
|
104
111
|
storage get <page> <key> [--store local|session]
|
|
105
112
|
storage set <page> <key> <value> [--store local|session]
|
|
106
113
|
storage export <page> <path> [--store local|session|all]
|
|
@@ -214,6 +221,7 @@ begin
|
|
|
214
221
|
when "page" then Browserctl::Commands::Page.run(client, args)
|
|
215
222
|
when "cookie" then Browserctl::Commands::Cookie.run(client, args)
|
|
216
223
|
when "storage" then Browserctl::Commands::Storage.run(client, args)
|
|
224
|
+
when "data" then Browserctl::Commands::Data.run(client, args)
|
|
217
225
|
when "state" then Browserctl::Commands::State.run(client, args)
|
|
218
226
|
when "daemon" then Browserctl::Commands::Daemon.run(client, args)
|
|
219
227
|
when "auth-check"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: wire browserctl's tracing seam to an OpenTelemetry exporter.
|
|
4
|
+
#
|
|
5
|
+
# This file is doc-only — it is NOT required by the gem, and OpenTelemetry
|
|
6
|
+
# is NOT a runtime dependency. Add `opentelemetry-sdk` and an exporter to
|
|
7
|
+
# your own application's Gemfile if you want this wiring.
|
|
8
|
+
#
|
|
9
|
+
# Usage (from a host app, before any browserctl commands are dispatched):
|
|
10
|
+
#
|
|
11
|
+
# require "opentelemetry/sdk"
|
|
12
|
+
# require "opentelemetry/exporter/otlp"
|
|
13
|
+
# require "browserctl"
|
|
14
|
+
# require_relative "path/to/tracing_otel"
|
|
15
|
+
#
|
|
16
|
+
# Browserctl::Tracing.backend = OtelTracingBackend.new
|
|
17
|
+
#
|
|
18
|
+
# Every command dispatched by the daemon will then emit a span named
|
|
19
|
+
# `command.<cmd>` with `command`, `page`, and `duration_ms` attributes.
|
|
20
|
+
|
|
21
|
+
require "opentelemetry/sdk"
|
|
22
|
+
|
|
23
|
+
# Adapter from browserctl's Backend contract to the OpenTelemetry Ruby API.
|
|
24
|
+
class OtelTracingBackend
|
|
25
|
+
def initialize(tracer_name: "browserctl", version: Browserctl::VERSION)
|
|
26
|
+
@tracer = OpenTelemetry.tracer_provider.tracer(tracer_name, version)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def start_span(name, attributes: {})
|
|
30
|
+
@tracer.start_span(name, attributes: stringify(attributes))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def end_span(span, status:, attributes: {})
|
|
34
|
+
return if span.nil?
|
|
35
|
+
|
|
36
|
+
attributes.each { |k, v| span.set_attribute(k.to_s, v) unless v.nil? }
|
|
37
|
+
span.status = OpenTelemetry::Trace::Status.error if status == :error
|
|
38
|
+
span.finish
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def stringify(attrs)
|
|
44
|
+
attrs.each_with_object({}) { |(k, v), h| h[k.to_s] = v unless v.nil? }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -20,8 +20,8 @@ module Browserctl
|
|
|
20
20
|
# deliberately absent from `FlowContext` — flows return state, workflows
|
|
21
21
|
# share state.
|
|
22
22
|
class CallableDefinition
|
|
23
|
-
ParamDef =
|
|
24
|
-
StepDef =
|
|
23
|
+
ParamDef = Data.define(:name, :required, :secret, :default, :secret_ref)
|
|
24
|
+
StepDef = Data.define(:label, :block, :retry_count, :timeout)
|
|
25
25
|
|
|
26
26
|
attr_reader :name, :description, :param_defs, :steps
|
|
27
27
|
|
data/lib/browserctl/client.rb
CHANGED
|
@@ -245,6 +245,47 @@ module Browserctl
|
|
|
245
245
|
call("storage_delete", name: name, stores: stores)
|
|
246
246
|
end
|
|
247
247
|
|
|
248
|
+
# Reads a single key from the given browser-side scope.
|
|
249
|
+
# Introduced in v0.15 as the unified replacement for `storage_get` /
|
|
250
|
+
# `cookies` (see ADR-0021).
|
|
251
|
+
# @param name [String] logical page name
|
|
252
|
+
# @param key [String] storage key (n/a for `scope: "cookies"`; use {#data_list})
|
|
253
|
+
# @param scope [String] one of "cookies", "localStorage", "sessionStorage"
|
|
254
|
+
# @return [Hash] `{ ok: true, scope:, key:, value: }` or `{ error:, code: }`
|
|
255
|
+
def data_get(name, key, scope:)
|
|
256
|
+
call("data_get", name: name, key: key, scope: scope)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Writes a single key/value into the given browser-side scope.
|
|
260
|
+
# For `scope: "cookies"`, `domain:` is required and `path:` defaults to "/".
|
|
261
|
+
# @param name [String] logical page name
|
|
262
|
+
# @param key [String] storage key (cookie name when scope is cookies)
|
|
263
|
+
# @param value [String] value to store
|
|
264
|
+
# @param scope [String] one of "cookies", "localStorage", "sessionStorage"
|
|
265
|
+
# @param domain [String, nil] cookie domain (required when scope is cookies)
|
|
266
|
+
# @param path [String] cookie path (default: "/")
|
|
267
|
+
# @return [Hash] `{ ok: true, scope:, key: }` or `{ error:, code: }`
|
|
268
|
+
def data_set(name, key, value, scope:, domain: nil, path: "/") # rubocop:disable Metrics/ParameterLists
|
|
269
|
+
call("data_set", name: name, key: key, value: value,
|
|
270
|
+
scope: scope, domain: domain, path: path)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Clears every entry in the given browser-side scope.
|
|
274
|
+
# @param name [String] logical page name
|
|
275
|
+
# @param scope [String] one of "cookies", "localStorage", "sessionStorage"
|
|
276
|
+
# @return [Hash] `{ ok: true, scope:, deleted: N }` or `{ error:, code: }`
|
|
277
|
+
def data_delete(name, scope:)
|
|
278
|
+
call("data_delete", name: name, scope: scope)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Lists every entry in the given browser-side scope.
|
|
282
|
+
# @param name [String] logical page name
|
|
283
|
+
# @param scope [String] one of "cookies", "localStorage", "sessionStorage"
|
|
284
|
+
# @return [Hash] `{ ok: true, scope:, entries: [...], count: N }` or `{ error:, code: }`
|
|
285
|
+
def data_list(name, scope:)
|
|
286
|
+
call("data_list", name: name, scope: scope)
|
|
287
|
+
end
|
|
288
|
+
|
|
248
289
|
# Fires a keydown + keyup event for the given key name on a page.
|
|
249
290
|
# @param name [String] logical page name
|
|
250
291
|
# @param key [String] key name e.g. "Enter", "Tab", "Escape", "ArrowDown"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "cli_output"
|
|
4
|
+
require_relative "deprecation_notice"
|
|
4
5
|
|
|
5
6
|
module Browserctl
|
|
6
7
|
module Commands
|
|
@@ -11,6 +12,11 @@ module Browserctl
|
|
|
11
12
|
|
|
12
13
|
def self.run(client, args)
|
|
13
14
|
sub = args.shift or abort USAGE
|
|
15
|
+
# Deprecated in v0.15 — see ADR-0021. Removed at 1.0.
|
|
16
|
+
DeprecationNotice.emit(
|
|
17
|
+
"cookie #{sub}",
|
|
18
|
+
deprecation_replacement(sub)
|
|
19
|
+
)
|
|
14
20
|
case sub
|
|
15
21
|
when "list" then run_list(client, args)
|
|
16
22
|
when "set" then run_set(client, args)
|
|
@@ -21,6 +27,17 @@ module Browserctl
|
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
|
|
30
|
+
def self.deprecation_replacement(sub)
|
|
31
|
+
case sub
|
|
32
|
+
when "list" then "data list <page> --scope cookies"
|
|
33
|
+
when "set" then "data set <page> <name> <value> --scope cookies --domain DOMAIN"
|
|
34
|
+
when "delete" then "data delete <page> --scope cookies"
|
|
35
|
+
when "export" then "data list <page> --scope cookies # write to file client-side"
|
|
36
|
+
when "import" then "data set <page> --scope cookies"
|
|
37
|
+
else "data <op> --scope cookies"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
24
41
|
def self.run_list(client, args)
|
|
25
42
|
page = args.shift or abort "usage: browserctl cookie list <page>"
|
|
26
43
|
print_result(client.cookies(page))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cli_output"
|
|
4
|
+
|
|
5
|
+
module Browserctl
|
|
6
|
+
module Commands
|
|
7
|
+
# `browserctl data <op> --scope <scope>` — unified verb for browser-side
|
|
8
|
+
# persistent data. Introduced in v0.15 (ADR-0021) as the replacement for
|
|
9
|
+
# the duplicated `cookie *` and `storage *` families.
|
|
10
|
+
module Data
|
|
11
|
+
extend CliOutput
|
|
12
|
+
|
|
13
|
+
USAGE = "Usage: browserctl data <get|set|delete|list> --scope " \
|
|
14
|
+
"{cookies|localStorage|sessionStorage} [args]"
|
|
15
|
+
|
|
16
|
+
def self.run(client, args)
|
|
17
|
+
sub = args.shift or abort USAGE
|
|
18
|
+
case sub
|
|
19
|
+
when "get" then run_get(client, args)
|
|
20
|
+
when "set" then run_set(client, args)
|
|
21
|
+
when "delete" then run_delete(client, args)
|
|
22
|
+
when "list" then run_list(client, args)
|
|
23
|
+
else abort "unknown data subcommand '#{sub}'\n#{USAGE}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.run_get(client, args)
|
|
28
|
+
page = args.shift or abort "usage: browserctl data get <page> <key> --scope SCOPE"
|
|
29
|
+
key = args.shift or abort "usage: browserctl data get <page> <key> --scope SCOPE"
|
|
30
|
+
scope = extract_required_scope(args)
|
|
31
|
+
print_result(client.data_get(page, key, scope: scope))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
SET_USAGE = "usage: browserctl data set <page> <key> <value> --scope SCOPE [--domain D] [--path /]"
|
|
35
|
+
|
|
36
|
+
def self.run_set(client, args)
|
|
37
|
+
page = args.shift or abort SET_USAGE
|
|
38
|
+
key = args.shift or abort SET_USAGE
|
|
39
|
+
value = args.shift or abort SET_USAGE
|
|
40
|
+
scope = extract_required_scope(args)
|
|
41
|
+
domain = extract_opt(args, "--domain")
|
|
42
|
+
path = extract_opt(args, "--path") || "/"
|
|
43
|
+
print_result(client.data_set(page, key, value, scope: scope, domain: domain, path: path))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.run_delete(client, args)
|
|
47
|
+
page = args.shift or abort "usage: browserctl data delete <page> --scope SCOPE"
|
|
48
|
+
scope = extract_required_scope(args)
|
|
49
|
+
print_result(client.data_delete(page, scope: scope))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.run_list(client, args)
|
|
53
|
+
page = args.shift or abort "usage: browserctl data list <page> --scope SCOPE"
|
|
54
|
+
scope = extract_required_scope(args)
|
|
55
|
+
print_result(client.data_list(page, scope: scope))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.extract_required_scope(args)
|
|
59
|
+
scope = extract_opt(args, "--scope")
|
|
60
|
+
abort "missing required flag: --scope {cookies|localStorage|sessionStorage}" unless scope
|
|
61
|
+
scope
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.extract_opt(args, flag)
|
|
65
|
+
idx = args.index(flag)
|
|
66
|
+
return nil unless idx
|
|
67
|
+
|
|
68
|
+
args.delete_at(idx)
|
|
69
|
+
args.delete_at(idx)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "output_format"
|
|
4
|
+
|
|
5
|
+
module Browserctl
|
|
6
|
+
module Commands
|
|
7
|
+
# One-shot deprecation warning emitter for v0.15 alias verbs.
|
|
8
|
+
#
|
|
9
|
+
# Per ADR-0021: when a CLI invocation uses `cookie *` or `storage *`,
|
|
10
|
+
# we emit exactly one line to stderr — never under `--output json`, so
|
|
11
|
+
# JSON consumers (AI agents, scripts) keep a clean parser input. The
|
|
12
|
+
# warning is memoised process-wide so a single CLI invocation only ever
|
|
13
|
+
# prints it once even if a wrapper calls the dispatcher multiple times.
|
|
14
|
+
module DeprecationNotice
|
|
15
|
+
REMOVAL = "Removed at 1.0."
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def emit(old_verb, replacement, io: $stderr)
|
|
20
|
+
return if @emitted
|
|
21
|
+
return if OutputFormat.current.json?
|
|
22
|
+
|
|
23
|
+
@emitted = true
|
|
24
|
+
io.puts "warning: '#{old_verb}' is deprecated; use '#{replacement}'. #{REMOVAL}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Test-only — reset memoisation between examples.
|
|
28
|
+
def reset!
|
|
29
|
+
@emitted = false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "cli_output"
|
|
4
|
+
require_relative "deprecation_notice"
|
|
4
5
|
|
|
5
6
|
module Browserctl
|
|
6
7
|
module Commands
|
|
@@ -11,6 +12,11 @@ module Browserctl
|
|
|
11
12
|
|
|
12
13
|
def self.run(client, args)
|
|
13
14
|
sub = args.shift or abort USAGE
|
|
15
|
+
# Deprecated in v0.15 — see ADR-0021. Removed at 1.0.
|
|
16
|
+
DeprecationNotice.emit(
|
|
17
|
+
"storage #{sub}",
|
|
18
|
+
deprecation_replacement(sub)
|
|
19
|
+
)
|
|
14
20
|
case sub
|
|
15
21
|
when "get" then run_get(client, args)
|
|
16
22
|
when "set" then run_set(client, args)
|
|
@@ -21,6 +27,17 @@ module Browserctl
|
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
|
|
30
|
+
def self.deprecation_replacement(sub)
|
|
31
|
+
case sub
|
|
32
|
+
when "get" then "data get <page> <key> --scope localStorage"
|
|
33
|
+
when "set" then "data set <page> <key> <value> --scope localStorage"
|
|
34
|
+
when "delete" then "data delete <page> --scope localStorage"
|
|
35
|
+
when "export" then "data list <page> --scope localStorage # write to file client-side"
|
|
36
|
+
when "import" then "data set <page> --scope localStorage"
|
|
37
|
+
else "data <op> --scope localStorage"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
24
41
|
def self.run_get(client, args)
|
|
25
42
|
page = args.shift or abort "usage: browserctl storage get <page> <key> [--store local|session]"
|
|
26
43
|
key = args.shift or abort "usage: browserctl storage get <page> <key> [--store local|session]"
|
|
@@ -21,7 +21,7 @@ module Browserctl
|
|
|
21
21
|
# runs server-side (handlers/observation.rb) and client-side (workflow
|
|
22
22
|
# `load_state` hook).
|
|
23
23
|
module AuthRequired
|
|
24
|
-
Result =
|
|
24
|
+
Result = Data.define(:triggered, :code, :reason, :suggested_flow) do
|
|
25
25
|
def to_h
|
|
26
26
|
{ triggered: triggered, code: code, reason: reason, suggested_flow: suggested_flow }.compact
|
|
27
27
|
end
|
|
@@ -45,9 +45,9 @@ module Browserctl
|
|
|
45
45
|
capability == :devtools
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def devtools_info(
|
|
48
|
+
def devtools_info(page_driver)
|
|
49
49
|
port = @ferrum.process.port
|
|
50
|
-
target_id =
|
|
50
|
+
target_id = page_driver.target_id
|
|
51
51
|
{ port: port, target_id: target_id }
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "page_driver"
|
|
4
|
+
|
|
5
|
+
module Browserctl
|
|
6
|
+
module Driver
|
|
7
|
+
# The only {PageDriver} implementation v0.15 ships. Wraps a Ferrum page
|
|
8
|
+
# (or {CDPPage} delegator over one) and forwards each interface method to
|
|
9
|
+
# the underlying Ferrum API. Intentionally dumb — no caching, no policy,
|
|
10
|
+
# no logging. Anything beyond plain forwarding belongs in the handler or
|
|
11
|
+
# in a dedicated wrapper, not here.
|
|
12
|
+
class FerrumPageDriver
|
|
13
|
+
include PageDriver
|
|
14
|
+
|
|
15
|
+
# @return [Object] the underlying Ferrum/CDP page. Exposed for callers
|
|
16
|
+
# that still bridge through Ferrum-typed APIs (currently only the CDP
|
|
17
|
+
# driver's `devtools_info`). New handler code must not use this.
|
|
18
|
+
attr_reader :raw_page
|
|
19
|
+
|
|
20
|
+
def initialize(page)
|
|
21
|
+
@raw_page = page
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def go_to(url) = @raw_page.go_to(url)
|
|
25
|
+
def current_url = @raw_page.current_url
|
|
26
|
+
def body = @raw_page.body
|
|
27
|
+
def evaluate(expression) = @raw_page.evaluate(expression)
|
|
28
|
+
def at_css(selector) = @raw_page.at_css(selector)
|
|
29
|
+
def screenshot(path:, full: false) = @raw_page.screenshot(path: path, full: full)
|
|
30
|
+
def activate = @raw_page.activate
|
|
31
|
+
def close = @raw_page.close
|
|
32
|
+
def on(event, &) = @raw_page.on(event, &)
|
|
33
|
+
def off(event, id) = @raw_page.off(event, id)
|
|
34
|
+
def keyboard_down(key) = @raw_page.keyboard.down(key)
|
|
35
|
+
def keyboard_up(key) = @raw_page.keyboard.up(key)
|
|
36
|
+
def mouse_move(x:, y:) = @raw_page.mouse.move(x: x, y: y) # rubocop:disable Naming/MethodParameterName
|
|
37
|
+
def cookies_all = @raw_page.cookies.all
|
|
38
|
+
def cookies_set(**) = @raw_page.cookies.set(**)
|
|
39
|
+
def cookies_clear = @raw_page.cookies.clear
|
|
40
|
+
def target_id = @raw_page.target_id
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browserctl
|
|
4
|
+
module Driver
|
|
5
|
+
# PageDriver is the interface every handler in `lib/browserctl/server/handlers/`
|
|
6
|
+
# talks to instead of touching a raw Ferrum page. It exists so unit tests can
|
|
7
|
+
# swap in a `FakePageDriver` (see `spec/support/fake_page_driver.rb`) without
|
|
8
|
+
# spawning a real browser.
|
|
9
|
+
#
|
|
10
|
+
# This module is intentionally a thin contract: Ruby has no real interfaces,
|
|
11
|
+
# so the implementation is duck-typed. The method list below is the entire
|
|
12
|
+
# public surface handlers may use. New entries here require a corresponding
|
|
13
|
+
# implementation in {FerrumPageDriver} **and** the test double, plus a
|
|
14
|
+
# handler that justifies them — do not add methods speculatively.
|
|
15
|
+
#
|
|
16
|
+
# Element-returning methods (currently only {#at_css}) return whatever the
|
|
17
|
+
# underlying driver returns. The fake driver returns stub objects with
|
|
18
|
+
# matching duck-typed methods (`focus`, `type`, `click`, `evaluate`,
|
|
19
|
+
# `select_file`). Handlers should treat these as opaque element handles.
|
|
20
|
+
#
|
|
21
|
+
# PageDriver lives in the **Extension** zone of the public surface
|
|
22
|
+
# (see `docs/reference/api-stability.md`). It is a testing seam, not an
|
|
23
|
+
# invitation to ship a non-Ferrum backend — that path is explicitly a
|
|
24
|
+
# non-goal of v0.15 (`docs/plans/v0.15-lock.md`).
|
|
25
|
+
module PageDriver
|
|
26
|
+
# Navigate the page to the given URL. Blocks until load completes.
|
|
27
|
+
# @param url [String]
|
|
28
|
+
def go_to(url) = raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
# @return [String] the current top-frame URL
|
|
31
|
+
def current_url = raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
# @return [String] the current page body HTML
|
|
34
|
+
def body = raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
# Evaluate a JS expression in the page context.
|
|
37
|
+
# @param expression [String]
|
|
38
|
+
# @return [Object] the JSON-decoded result
|
|
39
|
+
def evaluate(expression) = raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
# Find the first element matching the CSS selector.
|
|
42
|
+
# @param selector [String]
|
|
43
|
+
# @return [Object, nil] an element handle (duck-typed: `focus`, `type`,
|
|
44
|
+
# `click`, `evaluate`, `select_file`) or nil if no match
|
|
45
|
+
def at_css(selector) = raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
# Capture a screenshot to disk.
|
|
48
|
+
# @param path [String]
|
|
49
|
+
# @param full [Boolean]
|
|
50
|
+
def screenshot(path:, full: false) = raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
# Bring the page tab to the foreground (headed mode only).
|
|
53
|
+
def activate = raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
# Close the underlying page/tab.
|
|
56
|
+
def close = raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
# Subscribe to a page event (currently only `:dialog`).
|
|
59
|
+
# @return [Object] a subscription id usable with {#off}
|
|
60
|
+
def on(event, &) = raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
# Remove a subscription created by {#on}.
|
|
63
|
+
def off(event, id) = raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
# Press a key down. Pair with {#keyboard_up}.
|
|
66
|
+
def keyboard_down(key) = raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
# Release a key.
|
|
69
|
+
def keyboard_up(key) = raise NotImplementedError
|
|
70
|
+
|
|
71
|
+
# Move the mouse pointer to viewport coordinates.
|
|
72
|
+
def mouse_move(x:, y:) = raise NotImplementedError # rubocop:disable Naming/MethodParameterName
|
|
73
|
+
|
|
74
|
+
# @return [Hash{String => Object}] all cookies for the page, keyed by
|
|
75
|
+
# cookie name, with each value responding to `to_h`.
|
|
76
|
+
def cookies_all = raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
# Set a cookie. Accepts `name:`, `value:`, `domain:`, `path:`, and any
|
|
79
|
+
# of `httponly:`, `secure:`, `expires:`.
|
|
80
|
+
def cookies_set(**) = raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
# Clear every cookie on the page.
|
|
83
|
+
def cookies_clear = raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
# Underlying CDP target id; used by the devtools handler. Backends that
|
|
86
|
+
# do not expose CDP should raise.
|
|
87
|
+
def target_id = raise NotImplementedError
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -29,6 +29,18 @@ module Browserctl
|
|
|
29
29
|
INVALID_STATE_NAME = "INVALID_STATE_NAME"
|
|
30
30
|
INVALID_DSL_USAGE = "INVALID_DSL_USAGE"
|
|
31
31
|
INVALID_FORMAT_VERSION = "INVALID_FORMAT_VERSION"
|
|
32
|
+
INVALID_ARGUMENT = "INVALID_ARGUMENT"
|
|
33
|
+
|
|
34
|
+
# Plugin family — introduced in v0.15 WS-2 PR 5 to isolate the daemon
|
|
35
|
+
# from misbehaving third-party commands registered via
|
|
36
|
+
# `Browserctl.register_command`. PLUGIN_FAILED is the catch-all when an
|
|
37
|
+
# uncaught exception escapes the plugin block; PLUGIN_TIMED_OUT is
|
|
38
|
+
# emitted when the per-plugin timeout (default 30s, configurable via
|
|
39
|
+
# `timeout:` on `register_command`, opt-out via `timeout: nil`) elapses
|
|
40
|
+
# before the block returns. Both codes carry the plugin name in the
|
|
41
|
+
# response payload's `context` so agents can branch on it.
|
|
42
|
+
PLUGIN_FAILED = "PLUGIN_FAILED"
|
|
43
|
+
PLUGIN_TIMED_OUT = "PLUGIN_TIMED_OUT"
|
|
32
44
|
|
|
33
45
|
GENERIC = "GENERIC"
|
|
34
46
|
|
|
@@ -46,6 +58,9 @@ module Browserctl
|
|
|
46
58
|
INVALID_STATE_NAME,
|
|
47
59
|
INVALID_DSL_USAGE,
|
|
48
60
|
INVALID_FORMAT_VERSION,
|
|
61
|
+
INVALID_ARGUMENT,
|
|
62
|
+
PLUGIN_FAILED,
|
|
63
|
+
PLUGIN_TIMED_OUT,
|
|
49
64
|
GENERIC
|
|
50
65
|
].freeze
|
|
51
66
|
|
|
@@ -36,6 +36,13 @@ module Browserctl
|
|
|
36
36
|
# entry in this table — drift-related raises fall through to GENERIC
|
|
37
37
|
# until that code is introduced.
|
|
38
38
|
#
|
|
39
|
+
# The plugin family (PLUGIN_FAILED, PLUGIN_TIMED_OUT) intentionally has
|
|
40
|
+
# no dedicated entry — plugins are Extension-zone surface (see
|
|
41
|
+
# api-stability.md), so their failures collapse to GENERIC (1) like the
|
|
42
|
+
# other Extension-adjacent codes (DOMAIN_NOT_ALLOWED, KEY_NOT_FOUND,
|
|
43
|
+
# SECRET_RESOLUTION_FAILED). Agents must branch on the `code` field for
|
|
44
|
+
# plugin failures, not on `$?`.
|
|
45
|
+
#
|
|
39
46
|
# The validation family (VALIDATION_FAILED parent plus INVALID_*
|
|
40
47
|
# specialisations) all map to exit code 8 — agents and scripts can
|
|
41
48
|
# branch on `$? == 8` for any caller-side validation failure without
|
|
@@ -50,7 +57,8 @@ module Browserctl
|
|
|
50
57
|
Codes::INVALID_SELECTOR_REF => VALIDATION_FAILED,
|
|
51
58
|
Codes::INVALID_STATE_NAME => VALIDATION_FAILED,
|
|
52
59
|
Codes::INVALID_DSL_USAGE => VALIDATION_FAILED,
|
|
53
|
-
Codes::INVALID_FORMAT_VERSION => VALIDATION_FAILED
|
|
60
|
+
Codes::INVALID_FORMAT_VERSION => VALIDATION_FAILED,
|
|
61
|
+
Codes::INVALID_ARGUMENT => VALIDATION_FAILED
|
|
54
62
|
}.freeze
|
|
55
63
|
|
|
56
64
|
# @param code [String, nil] a canonical code from {Browserctl::Error::Codes}
|
|
@@ -39,6 +39,13 @@ module Browserctl
|
|
|
39
39
|
"required blocks or arguments are missing.",
|
|
40
40
|
Codes::INVALID_FORMAT_VERSION =>
|
|
41
41
|
"Use a non-negative Integer for the format version header; see docs/reference/format-versions.md.",
|
|
42
|
+
Codes::INVALID_ARGUMENT =>
|
|
43
|
+
"Check the argument value against the documented contract; " \
|
|
44
|
+
"e.g. --scope must be one of cookies|localStorage|sessionStorage.",
|
|
45
|
+
Codes::PLUGIN_FAILED =>
|
|
46
|
+
"Check the plugin's logs; the daemon caught an uncaught exception from the plugin and is otherwise healthy.",
|
|
47
|
+
Codes::PLUGIN_TIMED_OUT =>
|
|
48
|
+
"Increase the plugin's `timeout:` on register_command, or pass `timeout: nil` to opt out (not recommended).",
|
|
42
49
|
Codes::GENERIC => DEFAULT
|
|
43
50
|
}.freeze
|
|
44
51
|
|
data/lib/browserctl/flow.rb
CHANGED
|
@@ -4,7 +4,7 @@ require_relative "callable_definition"
|
|
|
4
4
|
require_relative "errors"
|
|
5
5
|
|
|
6
6
|
module Browserctl
|
|
7
|
-
FlowConditionDef =
|
|
7
|
+
FlowConditionDef = Data.define(:kind, :label, :block)
|
|
8
8
|
|
|
9
9
|
# Back-compat aliases — flow_wrapper specs reference these directly.
|
|
10
10
|
FlowParamDef = CallableDefinition::ParamDef
|
|
@@ -13,7 +13,7 @@ require_relative "../../flow"
|
|
|
13
13
|
# the duck-typed (current_url, body) interface the detector expects.
|
|
14
14
|
module Browserctl
|
|
15
15
|
module Flows
|
|
16
|
-
PageDetectorAdapter =
|
|
16
|
+
PageDetectorAdapter = Data.define(:current_url, :body)
|
|
17
17
|
|
|
18
18
|
module CloudflareSolve
|
|
19
19
|
module_function
|
|
@@ -22,11 +22,11 @@ module Browserctl
|
|
|
22
22
|
# absolute path of the file being migrated; it is responsible for
|
|
23
23
|
# rewriting that file in place, advancing it from `from_version` to
|
|
24
24
|
# `to_version`.
|
|
25
|
-
Migration =
|
|
25
|
+
Migration = Data.define(:format, :from_version, :to_version, :upgrade)
|
|
26
26
|
|
|
27
27
|
# Result of {.run}. `applied` is the ordered list of {Migration} steps
|
|
28
28
|
# that ran. When the artifact was already at target, `applied` is empty.
|
|
29
|
-
Result =
|
|
29
|
+
Result = Data.define(:format, :from, :to, :applied)
|
|
30
30
|
|
|
31
31
|
FORMAT_EXTENSIONS = {
|
|
32
32
|
".bctl" => :bundle,
|