browserctl 0.10.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 +66 -0
- data/README.md +2 -1
- data/bin/browserctl +168 -78
- data/bin/browserd +8 -1
- data/lib/browserctl/client.rb +50 -6
- data/lib/browserctl/commands/cli_output.rb +36 -3
- data/lib/browserctl/commands/flow.rb +123 -0
- data/lib/browserctl/commands/migrate.rb +94 -0
- data/lib/browserctl/commands/state.rb +193 -0
- data/lib/browserctl/commands/trace.rb +187 -0
- data/lib/browserctl/commands/workflow.rb +62 -4
- data/lib/browserctl/constants.rb +4 -2
- data/lib/browserctl/crash_report.rb +96 -0
- data/lib/browserctl/detectors/auth_required.rb +128 -0
- data/lib/browserctl/detectors.rb +2 -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 +72 -12
- data/lib/browserctl/flow.rb +22 -1
- data/lib/browserctl/flow_registry.rb +66 -0
- data/lib/browserctl/flows/stdlib/basic_auth.rb +30 -0
- data/lib/browserctl/flows/stdlib/cloudflare_solve.rb +59 -0
- data/lib/browserctl/flows/stdlib/magic_link_email.rb +28 -0
- data/lib/browserctl/flows/stdlib/oauth_github.rb +28 -0
- data/lib/browserctl/flows/stdlib/oauth_google.rb +30 -0
- data/lib/browserctl/flows/stdlib/totp_2fa.rb +61 -0
- 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 +246 -28
- data/lib/browserctl/redactor.rb +58 -0
- data/lib/browserctl/replay/context.rb +40 -0
- data/lib/browserctl/replay/fingerprint_matcher.rb +86 -0
- data/lib/browserctl/replay/snapshot_diff.rb +51 -0
- data/lib/browserctl/replay/telemetry.rb +60 -0
- data/lib/browserctl/rubocop/cops/typed_error.rb +69 -0
- data/lib/browserctl/runner.rb +50 -10
- data/lib/browserctl/secret_resolver_registry.rb +23 -4
- data/lib/browserctl/server/command_dispatcher.rb +13 -1
- 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 +50 -5
- data/lib/browserctl/server/handlers/observation.rb +43 -2
- data/lib/browserctl/server/handlers/state.rb +149 -0
- data/lib/browserctl/server/page_session.rb +9 -7
- data/lib/browserctl/server/snapshot_builder.rb +21 -45
- data/lib/browserctl/session.rb +1 -1
- data/lib/browserctl/snapshot/annotator.rb +75 -0
- data/lib/browserctl/snapshot/extractor.rb +21 -0
- data/lib/browserctl/snapshot/fingerprint.rb +88 -0
- data/lib/browserctl/snapshot/ref.rb +70 -0
- data/lib/browserctl/snapshot/serializer.rb +17 -0
- data/lib/browserctl/state/bundle.rb +283 -0
- data/lib/browserctl/state/transport.rb +64 -0
- data/lib/browserctl/state/transports/file.rb +35 -0
- data/lib/browserctl/state/transports/one_password.rb +67 -0
- data/lib/browserctl/state/transports/s3.rb +42 -0
- data/lib/browserctl/state.rb +208 -0
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow/flow_wrapper.rb +81 -0
- data/lib/browserctl/workflow/promoter.rb +96 -0
- data/lib/browserctl/workflow/promotion_ledger.rb +72 -0
- data/lib/browserctl/workflow.rb +235 -16
- metadata +44 -7
data/lib/browserctl/workflow.rb
CHANGED
|
@@ -3,20 +3,80 @@
|
|
|
3
3
|
require "timeout"
|
|
4
4
|
require_relative "client"
|
|
5
5
|
require_relative "errors"
|
|
6
|
+
require_relative "flow_registry"
|
|
7
|
+
require_relative "replay/context"
|
|
8
|
+
require_relative "replay/fingerprint_matcher"
|
|
9
|
+
require_relative "replay/snapshot_diff"
|
|
6
10
|
require_relative "secret_resolvers"
|
|
7
11
|
require_relative "session"
|
|
8
12
|
|
|
9
13
|
module Browserctl
|
|
14
|
+
# Workflow-file format version. Workflows are Ruby files; the schema gate
|
|
15
|
+
# is a top-of-file comment header:
|
|
16
|
+
#
|
|
17
|
+
# # format_version: 1
|
|
18
|
+
#
|
|
19
|
+
# Unlike bundles and recordings, an unsupported or missing version on a
|
|
20
|
+
# workflow file is a *warning*, not a hard failure. Workflows are
|
|
21
|
+
# human-authored Ruby — the loader prefers to surface drift via stderr
|
|
22
|
+
# and let the file run, rather than block execution. See
|
|
23
|
+
# docs/reference/format-versions.md.
|
|
24
|
+
WORKFLOW_FORMAT_VERSION = 1
|
|
25
|
+
SUPPORTED_WORKFLOW_FORMAT_VERSIONS = [WORKFLOW_FORMAT_VERSION].freeze
|
|
26
|
+
|
|
27
|
+
# Matches a leading-line comment of the form `# format_version: <int>`.
|
|
28
|
+
# Tolerates leading whitespace inside the comment body and ignores the
|
|
29
|
+
# `# frozen_string_literal: true` magic comment that conventionally
|
|
30
|
+
# precedes it.
|
|
31
|
+
WORKFLOW_FORMAT_VERSION_HEADER = /^\s*#\s*format_version:\s*(\d+)\s*$/
|
|
32
|
+
|
|
33
|
+
# Parses the `# format_version: N` header from a workflow file's source.
|
|
34
|
+
# Scans only the contiguous leading comment block (and blank lines) so
|
|
35
|
+
# the header cannot be smuggled in mid-file. Returns the integer if
|
|
36
|
+
# present, or nil if the file has no version header.
|
|
37
|
+
def self.parse_workflow_format_version(source)
|
|
38
|
+
source.each_line do |line|
|
|
39
|
+
stripped = line.strip
|
|
40
|
+
next if stripped.empty?
|
|
41
|
+
break unless stripped.start_with?("#")
|
|
42
|
+
|
|
43
|
+
if (m = line.match(WORKFLOW_FORMAT_VERSION_HEADER))
|
|
44
|
+
return Integer(m[1])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reads a workflow file and warns to stderr when the `format_version:`
|
|
51
|
+
# header is missing or declares an unsupported version. Always returns
|
|
52
|
+
# the parsed integer (or nil) — never raises. Callers should still
|
|
53
|
+
# `load` the file regardless.
|
|
54
|
+
def self.verify_workflow_format_version!(path)
|
|
55
|
+
source = File.read(path)
|
|
56
|
+
version = parse_workflow_format_version(source)
|
|
57
|
+
|
|
58
|
+
if version.nil?
|
|
59
|
+
warn "[browserctl] workflow #{path} is missing a `# format_version: N` header " \
|
|
60
|
+
"(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway"
|
|
61
|
+
elsif !SUPPORTED_WORKFLOW_FORMAT_VERSIONS.include?(version)
|
|
62
|
+
warn "[browserctl] workflow #{path} format_version=#{version} is not supported " \
|
|
63
|
+
"(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
version
|
|
67
|
+
end
|
|
68
|
+
|
|
10
69
|
ParamDef = Struct.new(:name, :required, :secret, :default, :secret_ref, keyword_init: true)
|
|
11
70
|
StepResult = Struct.new(:name, :ok, :error, keyword_init: true)
|
|
12
71
|
StepDef = Struct.new(:label, :block, :retry_count, :timeout, keyword_init: true)
|
|
13
72
|
|
|
14
73
|
class WorkflowContext
|
|
15
|
-
attr_reader :client
|
|
74
|
+
attr_reader :client, :replay_context, :params
|
|
16
75
|
|
|
17
|
-
def initialize(params, client)
|
|
76
|
+
def initialize(params, client, replay_context: nil)
|
|
18
77
|
@params = params
|
|
19
78
|
@client = client
|
|
79
|
+
@replay_context = replay_context
|
|
20
80
|
end
|
|
21
81
|
|
|
22
82
|
def store(key, value)
|
|
@@ -44,7 +104,7 @@ module Browserctl
|
|
|
44
104
|
end
|
|
45
105
|
|
|
46
106
|
def page(name)
|
|
47
|
-
PageProxy.new(name.to_s, @client)
|
|
107
|
+
PageProxy.new(name.to_s, @client, replay_context: @replay_context)
|
|
48
108
|
end
|
|
49
109
|
|
|
50
110
|
def open_page(page_name, url: nil)
|
|
@@ -68,7 +128,39 @@ module Browserctl
|
|
|
68
128
|
res
|
|
69
129
|
end
|
|
70
130
|
|
|
131
|
+
# Persists the daemon's current cookies + storage as a .bctl bundle.
|
|
132
|
+
# Optional flow binding lets `load_state` auto-rotate when the bundle
|
|
133
|
+
# is detected as needing authentication.
|
|
134
|
+
def save_state(name, flow: nil, origins: nil, encrypt: false)
|
|
135
|
+
passphrase = encrypt ? ENV.fetch("BROWSERCTL_STATE_PASSPHRASE", nil) : nil
|
|
136
|
+
res = @client.state_save(name.to_s,
|
|
137
|
+
flow: flow&.to_s, origins: origins, passphrase: passphrase)
|
|
138
|
+
raise WorkflowError, res[:error] if res[:error]
|
|
139
|
+
|
|
140
|
+
res
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Restores a .bctl bundle. When the daemon detects AUTH_REQUIRED before
|
|
144
|
+
# applying (e.g. expired cookies in the payload), this rotates the bound
|
|
145
|
+
# flow and retries — no caller code change required.
|
|
146
|
+
#
|
|
147
|
+
# @param on_auth_required [Proc, nil] override the auto-rotate path. The
|
|
148
|
+
# block runs in the workflow context, in lieu of invoking the manifest's
|
|
149
|
+
# bound flow. Use this when the recovery procedure is bespoke.
|
|
150
|
+
def load_state(name, on_auth_required: nil)
|
|
151
|
+
res = @client.state_load(name.to_s)
|
|
152
|
+
return res unless auth_required_response?(res)
|
|
153
|
+
|
|
154
|
+
recover_auth_required_state(name.to_s, res, on_auth_required)
|
|
155
|
+
end
|
|
156
|
+
DEPRECATED_LOAD_SESSION_FALLBACK = <<~MSG
|
|
157
|
+
[browserctl] DEPRECATION: `load_session(name, fallback:, expired_if:)` is superseded by
|
|
158
|
+
`load_state(name)` with a flow-bound bundle (`save_state(name, flow: :name)`).
|
|
159
|
+
`load_session` will be removed in v0.12. See docs/concepts/state.md.
|
|
160
|
+
MSG
|
|
161
|
+
|
|
71
162
|
def load_session(session_name, fallback: nil, expired_if: nil)
|
|
163
|
+
warn DEPRECATED_LOAD_SESSION_FALLBACK if fallback || expired_if
|
|
72
164
|
validate_expired_if!(expired_if)
|
|
73
165
|
fallback_name = fallback&.to_s
|
|
74
166
|
res = @client.session_load(session_name)
|
|
@@ -94,16 +186,41 @@ module Browserctl
|
|
|
94
186
|
$stdin.gets.chomp
|
|
95
187
|
end
|
|
96
188
|
|
|
97
|
-
def invoke(
|
|
98
|
-
name =
|
|
189
|
+
def invoke(target_name, page: nil, **override_params)
|
|
190
|
+
name = target_name.to_s
|
|
99
191
|
guard_circular!(name)
|
|
100
|
-
|
|
192
|
+
|
|
193
|
+
flow = lookup_flow_target(name)
|
|
194
|
+
if flow
|
|
195
|
+
track_invoke(name) { run_invoked_flow(flow, page_name: page, **override_params) }
|
|
196
|
+
else
|
|
197
|
+
track_invoke(name) { run_nested(target_name, **override_params) }
|
|
198
|
+
end
|
|
101
199
|
end
|
|
102
200
|
|
|
103
201
|
def assert(condition, msg = "assertion failed")
|
|
104
202
|
raise WorkflowError, msg unless condition
|
|
105
203
|
end
|
|
106
204
|
|
|
205
|
+
# Snapshots the named page and compares its digest against `expected_digest`.
|
|
206
|
+
# Under `workflow run --check` (a replay context is attached), a mismatch is
|
|
207
|
+
# recorded as a drift event with reason "post-snapshot mismatch" and the
|
|
208
|
+
# step still passes. Outside --check, mismatch raises WorkflowError so the
|
|
209
|
+
# workflow fails fast.
|
|
210
|
+
def assert_snapshot_stable(page_name, expected_digest:)
|
|
211
|
+
res = @client.snapshot(page_name.to_s, format: "elements")
|
|
212
|
+
snapshot = res[:snapshot]
|
|
213
|
+
actual = Replay::SnapshotDiff.digest(snapshot)
|
|
214
|
+
return if actual == expected_digest
|
|
215
|
+
|
|
216
|
+
msg = "post-snapshot mismatch on :#{page_name} — expected #{expected_digest}, got #{actual}"
|
|
217
|
+
raise WorkflowError, msg unless @replay_context
|
|
218
|
+
|
|
219
|
+
@replay_context.record(command: :assert_snapshot_stable, selector: page_name.to_s,
|
|
220
|
+
matched_ref: nil, score: nil, reason: "post-snapshot mismatch")
|
|
221
|
+
warn "[browserctl replay] #{msg}"
|
|
222
|
+
end
|
|
223
|
+
|
|
107
224
|
def compose(*)
|
|
108
225
|
raise WorkflowError,
|
|
109
226
|
"`compose` must be called at the workflow definition level, not inside a step block. " \
|
|
@@ -112,6 +229,45 @@ module Browserctl
|
|
|
112
229
|
|
|
113
230
|
private
|
|
114
231
|
|
|
232
|
+
def auth_required_response?(res)
|
|
233
|
+
(res[:code] || res["code"]) == "AUTH_REQUIRED"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def recover_auth_required_state(name, initial_res, on_auth_required)
|
|
237
|
+
if on_auth_required
|
|
238
|
+
on_auth_required.call
|
|
239
|
+
else
|
|
240
|
+
flow_name = initial_res[:suggested_flow] || initial_res["suggested_flow"]
|
|
241
|
+
unless flow_name && !flow_name.to_s.empty?
|
|
242
|
+
raise WorkflowError,
|
|
243
|
+
"state '#{name}' needs auth but bundle has no bound flow — " \
|
|
244
|
+
"save with `save_state('#{name}', flow: :NAME)` or pass on_auth_required:"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Match the daemon's `state load` preflight: it auth-checks the first
|
|
248
|
+
# open page (insertion order). Passing that same name to the flow
|
|
249
|
+
# gives stdlib flows a `page` proxy to drive (oauth_github reads
|
|
250
|
+
# `page.url`, totp_2fa calls `page.fill`, etc.). Falls back to no
|
|
251
|
+
# page only when nothing is open — `state_save` would have errored
|
|
252
|
+
# earlier in that case, so this is a defence-in-depth nil.
|
|
253
|
+
invoke(flow_name, page: first_open_page)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
after_save = @client.state_save(name)
|
|
257
|
+
raise WorkflowError, after_save[:error] if after_save[:error]
|
|
258
|
+
|
|
259
|
+
retry_res = @client.state_load(name, skip_auth_check: true)
|
|
260
|
+
raise WorkflowError, retry_res[:error] if retry_res[:error]
|
|
261
|
+
|
|
262
|
+
retry_res.merge(rotated: true)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def first_open_page
|
|
266
|
+
res = @client.page_list
|
|
267
|
+
pages = res[:pages] || res["pages"] || []
|
|
268
|
+
pages.first
|
|
269
|
+
end
|
|
270
|
+
|
|
115
271
|
def validate_expired_if!(expired_if)
|
|
116
272
|
return unless expired_if
|
|
117
273
|
|
|
@@ -182,22 +338,43 @@ module Browserctl
|
|
|
182
338
|
def run_nested(workflow_name, **override_params)
|
|
183
339
|
Runner.new.run_workflow(workflow_name, **@params, **override_params)
|
|
184
340
|
end
|
|
341
|
+
|
|
342
|
+
def lookup_flow_target(name)
|
|
343
|
+
Browserctl.lookup_flow(name) || begin
|
|
344
|
+
FlowRegistry.resolve(name)
|
|
345
|
+
rescue ArgumentError
|
|
346
|
+
nil
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def run_invoked_flow(flow, page_name:, **params)
|
|
351
|
+
proxy = page_name ? page(page_name) : nil
|
|
352
|
+
flow.run(page: proxy, client: @client, **params)
|
|
353
|
+
end
|
|
185
354
|
end
|
|
186
355
|
|
|
187
356
|
class PageProxy
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
357
|
+
attr_accessor :replay_context
|
|
358
|
+
|
|
359
|
+
def initialize(name, client, replay_context: nil, matcher: nil)
|
|
360
|
+
@name = name
|
|
361
|
+
@client = client
|
|
362
|
+
@replay_context = replay_context
|
|
363
|
+
@matcher = matcher || Replay::FingerprintMatcher.new
|
|
191
364
|
end
|
|
192
365
|
|
|
193
366
|
def navigate(url) = unwrap @client.navigate(@name, url)
|
|
194
367
|
|
|
195
368
|
def fill(selector = nil, value = nil, ref: nil)
|
|
196
|
-
|
|
369
|
+
with_selector_fallback(:fill, selector, ref) do |sel, r|
|
|
370
|
+
@client.fill(@name, sel, value, ref: r)
|
|
371
|
+
end
|
|
197
372
|
end
|
|
198
373
|
|
|
199
374
|
def click(selector = nil, ref: nil)
|
|
200
|
-
|
|
375
|
+
with_selector_fallback(:click, selector, ref) do |sel, r|
|
|
376
|
+
@client.click(@name, sel, ref: r)
|
|
377
|
+
end
|
|
201
378
|
end
|
|
202
379
|
|
|
203
380
|
def snapshot(**) = unwrap @client.snapshot(@name, **)
|
|
@@ -219,15 +396,21 @@ module Browserctl
|
|
|
219
396
|
def press(key) = unwrap @client.press(@name, key)
|
|
220
397
|
|
|
221
398
|
def hover(selector = nil, ref: nil)
|
|
222
|
-
|
|
399
|
+
with_selector_fallback(:hover, selector, ref) do |sel, r|
|
|
400
|
+
@client.hover(@name, sel, ref: r)
|
|
401
|
+
end
|
|
223
402
|
end
|
|
224
403
|
|
|
225
404
|
def upload(selector = nil, path = nil, ref: nil)
|
|
226
|
-
|
|
405
|
+
with_selector_fallback(:upload, selector, ref) do |sel, r|
|
|
406
|
+
@client.upload(@name, sel, path, ref: r)
|
|
407
|
+
end
|
|
227
408
|
end
|
|
228
409
|
|
|
229
410
|
def select(selector = nil, value = nil, ref: nil)
|
|
230
|
-
|
|
411
|
+
with_selector_fallback(:select, selector, ref) do |sel, r|
|
|
412
|
+
@client.select(@name, sel, value, ref: r)
|
|
413
|
+
end
|
|
231
414
|
end
|
|
232
415
|
|
|
233
416
|
def dialog_accept(text: nil) = unwrap @client.dialog_accept(@name, text: text)
|
|
@@ -235,6 +418,42 @@ module Browserctl
|
|
|
235
418
|
|
|
236
419
|
private
|
|
237
420
|
|
|
421
|
+
# Issues the wrapped command. If the daemon returns selector_not_found
|
|
422
|
+
# and a replay context has a fingerprint for this selector, takes a
|
|
423
|
+
# fresh snapshot, asks the matcher for a candidate, and retries by ref.
|
|
424
|
+
def with_selector_fallback(cmd, selector, ref)
|
|
425
|
+
res = yield(selector, ref)
|
|
426
|
+
return unwrap(res) if !selector_not_found?(res) || ref || !@replay_context || !selector
|
|
427
|
+
|
|
428
|
+
fp = @replay_context.fingerprint_for(selector)
|
|
429
|
+
return unwrap(res) unless fp
|
|
430
|
+
|
|
431
|
+
match = @matcher.best(fp, snapshot_entries)
|
|
432
|
+
unless match
|
|
433
|
+
@replay_context.record(command: cmd, selector: selector, reason: "no candidate above threshold")
|
|
434
|
+
return unwrap(res)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
log_rematch(cmd, selector, match)
|
|
438
|
+
@replay_context.record(command: cmd, selector: selector,
|
|
439
|
+
matched_ref: match.candidate[:ref], score: match.score, reason: "rematch")
|
|
440
|
+
unwrap(yield(nil, match.candidate[:ref]))
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def snapshot_entries
|
|
444
|
+
res = @client.snapshot(@name, format: "elements")
|
|
445
|
+
Array(res[:snapshot])
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def selector_not_found?(res)
|
|
449
|
+
res.is_a?(Hash) && res[:code] == Browserctl::Error::Codes::SELECTOR_NOT_FOUND
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def log_rematch(cmd, selector, match)
|
|
453
|
+
warn "[browserctl replay] #{cmd} selector #{selector.inspect} not found — " \
|
|
454
|
+
"rematched to ref=#{match.candidate[:ref]} (score=#{format('%.2f', match.score)})"
|
|
455
|
+
end
|
|
456
|
+
|
|
238
457
|
def unwrap(res)
|
|
239
458
|
raise WorkflowError, res[:error] if res[:error]
|
|
240
459
|
|
|
@@ -273,8 +492,8 @@ module Browserctl
|
|
|
273
492
|
@steps.concat(source.steps)
|
|
274
493
|
end
|
|
275
494
|
|
|
276
|
-
def call(params, client)
|
|
277
|
-
ctx = WorkflowContext.new(resolve_params(params), client)
|
|
495
|
+
def call(params, client, replay_context: nil)
|
|
496
|
+
ctx = WorkflowContext.new(resolve_params(params), client, replay_context: replay_context)
|
|
278
497
|
execute_steps(ctx)
|
|
279
498
|
end
|
|
280
499
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browserctl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: ferrum
|
|
@@ -127,8 +126,8 @@ description: Named browser sessions, Ruby workflow DSL, and a token-efficient DO
|
|
|
127
126
|
email:
|
|
128
127
|
- patrick204nqh@gmail.com
|
|
129
128
|
executables:
|
|
130
|
-
- browserd
|
|
131
129
|
- browserctl
|
|
130
|
+
- browserd
|
|
132
131
|
extensions: []
|
|
133
132
|
extra_rdoc_files: []
|
|
134
133
|
files:
|
|
@@ -174,26 +173,50 @@ files:
|
|
|
174
173
|
- lib/browserctl/commands/daemon.rb
|
|
175
174
|
- lib/browserctl/commands/dialog.rb
|
|
176
175
|
- lib/browserctl/commands/fill.rb
|
|
176
|
+
- lib/browserctl/commands/flow.rb
|
|
177
177
|
- lib/browserctl/commands/init.rb
|
|
178
|
+
- lib/browserctl/commands/migrate.rb
|
|
178
179
|
- lib/browserctl/commands/page.rb
|
|
179
180
|
- lib/browserctl/commands/record.rb
|
|
180
181
|
- lib/browserctl/commands/resume.rb
|
|
181
182
|
- lib/browserctl/commands/screenshot.rb
|
|
182
183
|
- lib/browserctl/commands/session.rb
|
|
183
184
|
- lib/browserctl/commands/snapshot.rb
|
|
185
|
+
- lib/browserctl/commands/state.rb
|
|
184
186
|
- lib/browserctl/commands/storage.rb
|
|
187
|
+
- lib/browserctl/commands/trace.rb
|
|
185
188
|
- lib/browserctl/commands/workflow.rb
|
|
186
189
|
- lib/browserctl/constants.rb
|
|
190
|
+
- lib/browserctl/crash_report.rb
|
|
187
191
|
- lib/browserctl/detectors.rb
|
|
192
|
+
- lib/browserctl/detectors/auth_required.rb
|
|
188
193
|
- lib/browserctl/driver.rb
|
|
189
194
|
- lib/browserctl/driver/base.rb
|
|
190
195
|
- lib/browserctl/driver/cdp.rb
|
|
191
196
|
- lib/browserctl/driver/cdp_page.rb
|
|
197
|
+
- lib/browserctl/error/codes.rb
|
|
198
|
+
- lib/browserctl/error/exit_codes.rb
|
|
199
|
+
- lib/browserctl/error/suggested_actions.rb
|
|
192
200
|
- lib/browserctl/errors.rb
|
|
193
201
|
- lib/browserctl/flow.rb
|
|
202
|
+
- lib/browserctl/flow_registry.rb
|
|
203
|
+
- lib/browserctl/flows/stdlib/basic_auth.rb
|
|
204
|
+
- lib/browserctl/flows/stdlib/cloudflare_solve.rb
|
|
205
|
+
- lib/browserctl/flows/stdlib/magic_link_email.rb
|
|
206
|
+
- lib/browserctl/flows/stdlib/oauth_github.rb
|
|
207
|
+
- lib/browserctl/flows/stdlib/oauth_google.rb
|
|
208
|
+
- lib/browserctl/flows/stdlib/totp_2fa.rb
|
|
209
|
+
- lib/browserctl/format_version.rb
|
|
194
210
|
- lib/browserctl/logger.rb
|
|
211
|
+
- lib/browserctl/migrations.rb
|
|
195
212
|
- lib/browserctl/policy.rb
|
|
196
213
|
- lib/browserctl/recording.rb
|
|
214
|
+
- lib/browserctl/redactor.rb
|
|
215
|
+
- lib/browserctl/replay/context.rb
|
|
216
|
+
- lib/browserctl/replay/fingerprint_matcher.rb
|
|
217
|
+
- lib/browserctl/replay/snapshot_diff.rb
|
|
218
|
+
- lib/browserctl/replay/telemetry.rb
|
|
219
|
+
- lib/browserctl/rubocop/cops/typed_error.rb
|
|
197
220
|
- lib/browserctl/runner.rb
|
|
198
221
|
- lib/browserctl/secret_resolver_registry.rb
|
|
199
222
|
- lib/browserctl/secret_resolvers.rb
|
|
@@ -206,19 +229,35 @@ files:
|
|
|
206
229
|
- lib/browserctl/server/handlers/cookies.rb
|
|
207
230
|
- lib/browserctl/server/handlers/daemon_control.rb
|
|
208
231
|
- lib/browserctl/server/handlers/devtools.rb
|
|
232
|
+
- lib/browserctl/server/handlers/error_payload.rb
|
|
209
233
|
- lib/browserctl/server/handlers/hitl.rb
|
|
210
234
|
- lib/browserctl/server/handlers/interaction.rb
|
|
211
235
|
- lib/browserctl/server/handlers/navigation.rb
|
|
212
236
|
- lib/browserctl/server/handlers/observation.rb
|
|
213
237
|
- lib/browserctl/server/handlers/page_lifecycle.rb
|
|
214
238
|
- lib/browserctl/server/handlers/session.rb
|
|
239
|
+
- lib/browserctl/server/handlers/state.rb
|
|
215
240
|
- lib/browserctl/server/handlers/storage.rb
|
|
216
241
|
- lib/browserctl/server/idle_watcher.rb
|
|
217
242
|
- lib/browserctl/server/page_session.rb
|
|
218
243
|
- lib/browserctl/server/snapshot_builder.rb
|
|
219
244
|
- lib/browserctl/session.rb
|
|
245
|
+
- lib/browserctl/snapshot/annotator.rb
|
|
246
|
+
- lib/browserctl/snapshot/extractor.rb
|
|
247
|
+
- lib/browserctl/snapshot/fingerprint.rb
|
|
248
|
+
- lib/browserctl/snapshot/ref.rb
|
|
249
|
+
- lib/browserctl/snapshot/serializer.rb
|
|
250
|
+
- lib/browserctl/state.rb
|
|
251
|
+
- lib/browserctl/state/bundle.rb
|
|
252
|
+
- lib/browserctl/state/transport.rb
|
|
253
|
+
- lib/browserctl/state/transports/file.rb
|
|
254
|
+
- lib/browserctl/state/transports/one_password.rb
|
|
255
|
+
- lib/browserctl/state/transports/s3.rb
|
|
220
256
|
- lib/browserctl/version.rb
|
|
221
257
|
- lib/browserctl/workflow.rb
|
|
258
|
+
- lib/browserctl/workflow/flow_wrapper.rb
|
|
259
|
+
- lib/browserctl/workflow/promoter.rb
|
|
260
|
+
- lib/browserctl/workflow/promotion_ledger.rb
|
|
222
261
|
homepage: https://github.com/patrick204nqh/browserctl
|
|
223
262
|
licenses:
|
|
224
263
|
- MIT
|
|
@@ -229,7 +268,6 @@ metadata:
|
|
|
229
268
|
bug_tracker_uri: https://github.com/patrick204nqh/browserctl/issues
|
|
230
269
|
documentation_uri: https://github.com/patrick204nqh/browserctl/tree/main/docs
|
|
231
270
|
rubygems_mfa_required: 'true'
|
|
232
|
-
post_install_message:
|
|
233
271
|
rdoc_options: []
|
|
234
272
|
require_paths:
|
|
235
273
|
- lib
|
|
@@ -244,8 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
244
282
|
- !ruby/object:Gem::Version
|
|
245
283
|
version: '0'
|
|
246
284
|
requirements: []
|
|
247
|
-
rubygems_version: 3.
|
|
248
|
-
signing_key:
|
|
285
|
+
rubygems_version: 3.6.9
|
|
249
286
|
specification_version: 4
|
|
250
287
|
summary: Persistent browser automation daemon and CLI for AI agents and developer
|
|
251
288
|
workflows
|