browserctl 0.3.1 → 0.5.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 +47 -0
- data/README.md +120 -214
- data/bin/browserctl +35 -13
- data/bin/browserd +7 -1
- data/bin/setup +7 -3
- data/examples/cloudflare_hitl.rb +1 -1
- data/examples/smoke/params_file.rb +35 -0
- data/examples/smoke/store_fetch.rb +39 -0
- data/examples/the_internet/add_remove_elements.rb +3 -3
- data/examples/the_internet/checkboxes.rb +3 -3
- data/examples/the_internet/dropdown.rb +3 -3
- data/examples/the_internet/dynamic_loading.rb +3 -3
- data/examples/the_internet/login.rb +5 -5
- data/lib/browserctl/client.rb +38 -2
- data/lib/browserctl/commands/export_cookies.rb +18 -0
- data/lib/browserctl/commands/import_cookies.rb +23 -0
- data/lib/browserctl/commands/init.rb +11 -0
- data/lib/browserctl/commands/{pause_resume.rb → pause.rb} +2 -12
- data/lib/browserctl/commands/record.rb +2 -0
- data/lib/browserctl/commands/resume.rb +21 -0
- data/lib/browserctl/commands/snapshot.rb +5 -5
- data/lib/browserctl/commands/status.rb +30 -0
- data/lib/browserctl/constants.rb +9 -2
- data/lib/browserctl/detectors.rb +23 -0
- data/lib/browserctl/errors.rb +25 -0
- data/lib/browserctl/logger.rb +40 -5
- data/lib/browserctl/policy.rb +36 -0
- data/lib/browserctl/recording.rb +81 -15
- data/lib/browserctl/runner.rb +23 -4
- data/lib/browserctl/server/command_dispatcher.rb +31 -234
- data/lib/browserctl/server/handlers/cookies.rb +57 -0
- data/lib/browserctl/server/handlers/daemon_control.rb +29 -0
- data/lib/browserctl/server/handlers/devtools.rb +22 -0
- data/lib/browserctl/server/handlers/hitl.rb +30 -0
- data/lib/browserctl/server/handlers/navigation.rb +72 -0
- data/lib/browserctl/server/handlers/observation.rb +113 -0
- data/lib/browserctl/server/handlers/page_lifecycle.rb +29 -0
- data/lib/browserctl/server.rb +18 -2
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow.rb +41 -3
- data/lib/browserctl.rb +12 -2
- metadata +48 -4
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browserctl
|
|
4
|
+
class CommandDispatcher
|
|
5
|
+
module Handlers
|
|
6
|
+
module Navigation
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def cmd_goto(req)
|
|
10
|
+
unless Policy.allowed_navigation?(req[:url].to_s)
|
|
11
|
+
return { error: "navigation to '#{req[:url]}' blocked by domain policy", code: "domain_not_allowed" }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
with_page(req[:name]) do |session|
|
|
15
|
+
session.page.go_to(req[:url])
|
|
16
|
+
{ ok: true, url: session.page.current_url, challenge: Detectors.cloudflare?(session.page) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cmd_evaluate(req)
|
|
21
|
+
with_page(req[:name]) { |session| { ok: true, result: session.page.evaluate(req[:expression]) } }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cmd_fill(req)
|
|
25
|
+
with_page(req[:name]) do |session|
|
|
26
|
+
sel = resolve_selector_from(session, req)
|
|
27
|
+
return sel if sel.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
type_into(session.page, sel, req[:value])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cmd_click(req)
|
|
34
|
+
with_page(req[:name]) do |session|
|
|
35
|
+
sel = resolve_selector_from(session, req)
|
|
36
|
+
return sel if sel.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
click_element(session.page, sel)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cmd_url(req)
|
|
43
|
+
with_page(req[:name]) { |session| { ok: true, url: session.page.current_url } }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def type_into(page, selector, value)
|
|
47
|
+
el = page.at_css(selector)
|
|
48
|
+
return { error: "selector not found: #{selector}" } unless el
|
|
49
|
+
|
|
50
|
+
el.focus
|
|
51
|
+
el.type(value)
|
|
52
|
+
{ ok: true }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def click_element(page, selector)
|
|
56
|
+
el = page.at_css(selector)
|
|
57
|
+
return { error: "selector not found: #{selector}" } unless el
|
|
58
|
+
|
|
59
|
+
el.click
|
|
60
|
+
{ ok: true }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolve_selector_from(session, req)
|
|
64
|
+
return req[:selector] if req[:selector]
|
|
65
|
+
return { error: "selector or ref required" } unless req[:ref]
|
|
66
|
+
|
|
67
|
+
session.ref_registry[req[:ref]] || { error: "ref '#{req[:ref]}' not found — run snap first" }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Browserctl
|
|
6
|
+
class CommandDispatcher
|
|
7
|
+
module Handlers
|
|
8
|
+
module Observation
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def cmd_snapshot(req)
|
|
12
|
+
with_page(req[:name]) { |session| take_snapshot(session, req[:format], req[:diff]) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def take_snapshot(session, format, diff)
|
|
16
|
+
nonce = SecureRandom.hex(8)
|
|
17
|
+
challenge = Detectors.cloudflare?(session.page)
|
|
18
|
+
|
|
19
|
+
return { ok: true, html: session.page.body, challenge: challenge, nonce: nonce } unless format == "elements"
|
|
20
|
+
|
|
21
|
+
snapshot = @snapshot_builder.call(session.page)
|
|
22
|
+
registry = snapshot.to_h { |el| [el[:ref], el[:selector]] }
|
|
23
|
+
|
|
24
|
+
prev = session.prev_snapshot
|
|
25
|
+
session.ref_registry = registry
|
|
26
|
+
session.prev_snapshot = snapshot
|
|
27
|
+
result = diff && prev ? compute_diff(prev, snapshot) : snapshot
|
|
28
|
+
|
|
29
|
+
{ ok: true, snapshot: result, challenge: challenge, nonce: nonce }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def compute_diff(prev, current)
|
|
33
|
+
prev_by_sel = prev.to_h { |el| [el[:selector], el] }
|
|
34
|
+
current.reject do |el|
|
|
35
|
+
old = prev_by_sel[el[:selector]]
|
|
36
|
+
old && old.slice(:text, :attrs) == el.slice(:text, :attrs)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cmd_screenshot(req)
|
|
41
|
+
with_page(req[:name]) do |session|
|
|
42
|
+
path = safe_screenshot_path(req[:path], req[:name])
|
|
43
|
+
return path if path.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
46
|
+
session.page.screenshot(path: path, full: req.fetch(:full, false))
|
|
47
|
+
{ ok: true, path: path }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def safe_screenshot_path(requested, page_name)
|
|
52
|
+
return default_screenshot_path(page_name) unless requested
|
|
53
|
+
|
|
54
|
+
expanded = File.expand_path(requested)
|
|
55
|
+
return { error: "invalid extension — use .png, .jpg, or .jpeg" } unless valid_screenshot_ext?(expanded)
|
|
56
|
+
return { error: "path outside allowed directory (#{SCREENSHOT_DIR} or project directory)" } \
|
|
57
|
+
unless within_screenshot_roots?(expanded)
|
|
58
|
+
|
|
59
|
+
File.join(resolve_dir(File.dirname(expanded)), File.basename(expanded))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def valid_screenshot_ext?(path)
|
|
63
|
+
SCREENSHOT_EXTS.include?(File.extname(path).downcase)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def within_screenshot_roots?(path)
|
|
67
|
+
dir = resolve_dir(File.dirname(path))
|
|
68
|
+
SCREENSHOT_ROOTS.any? do |root|
|
|
69
|
+
real_root = resolve_dir(root)
|
|
70
|
+
dir.start_with?("#{real_root}/") || dir == real_root
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def resolve_dir(dir)
|
|
75
|
+
File.realpath(dir)
|
|
76
|
+
rescue Errno::ENOENT
|
|
77
|
+
dir
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def default_screenshot_path(page_name)
|
|
81
|
+
name_safe = page_name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
82
|
+
File.join(SCREENSHOT_DIR, "browserctl_shot_#{name_safe}_#{Time.now.to_i}.png")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def cmd_wait_for(req)
|
|
86
|
+
with_page(req[:name]) do |session|
|
|
87
|
+
wait_for_selector(session.page, req[:selector], req.fetch(:timeout, 10).to_f)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def cmd_watch(req)
|
|
92
|
+
with_page(req[:name]) do |session|
|
|
93
|
+
result = wait_for_selector(session.page, req[:selector], req.fetch(:timeout, 30).to_f)
|
|
94
|
+
result[:error] ? result : { ok: true, selector: req[:selector] }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def wait_for_selector(page, selector, timeout)
|
|
99
|
+
deadline = Time.now + timeout
|
|
100
|
+
loop do
|
|
101
|
+
found = page.at_css(selector)
|
|
102
|
+
break { ok: true } if found
|
|
103
|
+
if Time.now >= deadline
|
|
104
|
+
break { error: "wait_for timeout: selector '#{selector}' not found after #{timeout}s" }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sleep 0.2
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browserctl
|
|
4
|
+
class CommandDispatcher
|
|
5
|
+
module Handlers
|
|
6
|
+
module PageLifecycle
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def cmd_open_page(req)
|
|
10
|
+
session = @global_mutex.synchronize do
|
|
11
|
+
@pages[req[:name]] ||= PageSession.new(@browser.create_page)
|
|
12
|
+
end
|
|
13
|
+
session.page.go_to(req[:url]) if req[:url]
|
|
14
|
+
{ ok: true, name: req[:name] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cmd_close_page(req)
|
|
18
|
+
session = @global_mutex.synchronize { @pages.delete(req[:name]) }
|
|
19
|
+
session&.page&.close
|
|
20
|
+
{ ok: true }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def cmd_list_pages(_req)
|
|
24
|
+
{ pages: @global_mutex.synchronize { @pages.keys } }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/browserctl/server.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Browserctl
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def run
|
|
24
|
+
guard_already_running!
|
|
24
25
|
write_pid
|
|
25
26
|
server, idle = setup_server
|
|
26
27
|
serve(server)
|
|
@@ -61,12 +62,27 @@ module Browserctl
|
|
|
61
62
|
|
|
62
63
|
def setup_socket
|
|
63
64
|
FileUtils.rm_f(@socket_path)
|
|
65
|
+
old_umask = File.umask(0o177)
|
|
64
66
|
server = UNIXServer.new(@socket_path)
|
|
65
|
-
File.
|
|
66
|
-
Browserctl.logger.info "listening on #{@socket_path}"
|
|
67
|
+
File.umask(old_umask)
|
|
68
|
+
Browserctl.logger.info "daemon ready — listening on #{@socket_path}"
|
|
67
69
|
server
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
def guard_already_running!
|
|
73
|
+
return unless File.exist?(@pid_path)
|
|
74
|
+
|
|
75
|
+
pid = File.read(@pid_path).strip.to_i
|
|
76
|
+
return unless pid.positive?
|
|
77
|
+
|
|
78
|
+
Process.kill(0, pid)
|
|
79
|
+
abort "browserd already running (PID #{pid}). Use 'browserctl shutdown' first."
|
|
80
|
+
rescue Errno::ESRCH
|
|
81
|
+
# Dead process — stale PID file, safe to continue
|
|
82
|
+
rescue Errno::EPERM
|
|
83
|
+
abort "browserd (PID #{pid}) is running as a different user. Remove #{@pid_path} manually if stale."
|
|
84
|
+
end
|
|
85
|
+
|
|
70
86
|
def serve(server)
|
|
71
87
|
loop do
|
|
72
88
|
client = server.accept
|
data/lib/browserctl/version.rb
CHANGED
data/lib/browserctl/workflow.rb
CHANGED
|
@@ -18,6 +18,20 @@ module Browserctl
|
|
|
18
18
|
@client = client
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def store(key, value)
|
|
22
|
+
res = @client.store(key.to_s, value)
|
|
23
|
+
raise WorkflowError, res[:error] if res[:error]
|
|
24
|
+
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fetch(key)
|
|
29
|
+
res = @client.fetch(key.to_s)
|
|
30
|
+
raise WorkflowError, res[:error] if res[:error]
|
|
31
|
+
|
|
32
|
+
res[:value]
|
|
33
|
+
end
|
|
34
|
+
|
|
21
35
|
def method_missing(name, *args)
|
|
22
36
|
return @params[name] if @params.key?(name)
|
|
23
37
|
|
|
@@ -32,6 +46,20 @@ module Browserctl
|
|
|
32
46
|
PageProxy.new(name.to_s, @client)
|
|
33
47
|
end
|
|
34
48
|
|
|
49
|
+
def open_page(page_name, url: nil)
|
|
50
|
+
res = @client.open_page(page_name.to_s, url: url)
|
|
51
|
+
raise WorkflowError, res[:error] if res[:error]
|
|
52
|
+
|
|
53
|
+
res
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def close_page(page_name)
|
|
57
|
+
res = @client.close_page(page_name.to_s)
|
|
58
|
+
raise WorkflowError, res[:error] if res[:error]
|
|
59
|
+
|
|
60
|
+
res
|
|
61
|
+
end
|
|
62
|
+
|
|
35
63
|
def invoke(workflow_name, **override_params)
|
|
36
64
|
name = workflow_name.to_s
|
|
37
65
|
guard_circular!(name)
|
|
@@ -77,6 +105,7 @@ module Browserctl
|
|
|
77
105
|
def click(sel) = unwrap @client.click(@name, sel)
|
|
78
106
|
def snapshot(**) = unwrap @client.snapshot(@name, **)
|
|
79
107
|
def screenshot(**) = unwrap @client.screenshot(@name, **)
|
|
108
|
+
def watch(sel, timeout: 30) = unwrap @client.watch(@name, sel, timeout: timeout)
|
|
80
109
|
def wait_for(sel, timeout: 10) = unwrap @client.wait_for(@name, sel, timeout: timeout)
|
|
81
110
|
def url = @client.url(@name)[:url]
|
|
82
111
|
def evaluate(expr) = @client.evaluate(@name, expr)[:result]
|
|
@@ -113,7 +142,7 @@ module Browserctl
|
|
|
113
142
|
end
|
|
114
143
|
|
|
115
144
|
def compose(workflow_name)
|
|
116
|
-
source =
|
|
145
|
+
source = Browserctl.lookup_workflow(workflow_name.to_s)
|
|
117
146
|
raise WorkflowError, "workflow '#{workflow_name}' not found for composition" unless source
|
|
118
147
|
|
|
119
148
|
@steps.concat(source.steps)
|
|
@@ -163,11 +192,20 @@ module Browserctl
|
|
|
163
192
|
end
|
|
164
193
|
end
|
|
165
194
|
|
|
166
|
-
|
|
195
|
+
@registry_mutex = Mutex.new
|
|
196
|
+
@registry = {}
|
|
167
197
|
|
|
168
198
|
def self.workflow(name, &)
|
|
169
199
|
defn = WorkflowDefinition.new(name.to_s)
|
|
170
200
|
defn.instance_exec(&)
|
|
171
|
-
|
|
201
|
+
@registry_mutex.synchronize { @registry[name.to_s] = defn }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def self.lookup_workflow(name)
|
|
205
|
+
@registry_mutex.synchronize { @registry[name.to_s] }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.registry_snapshot
|
|
209
|
+
@registry_mutex.synchronize { @registry.dup }
|
|
172
210
|
end
|
|
173
211
|
end
|
data/lib/browserctl.rb
CHANGED
|
@@ -2,14 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "browserctl/version"
|
|
4
4
|
require_relative "browserctl/constants"
|
|
5
|
+
require_relative "browserctl/errors"
|
|
5
6
|
require_relative "browserctl/workflow"
|
|
6
7
|
require_relative "browserctl/runner"
|
|
7
8
|
require_relative "browserctl/client"
|
|
8
9
|
|
|
9
10
|
module Browserctl
|
|
10
|
-
|
|
11
|
+
@plugin_commands_mutex = Mutex.new
|
|
12
|
+
@plugin_commands = {}
|
|
11
13
|
|
|
12
14
|
def self.register_command(name, &block)
|
|
13
|
-
|
|
15
|
+
@plugin_commands_mutex.synchronize { @plugin_commands[name.to_s] = block }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.lookup_plugin_command(name)
|
|
19
|
+
@plugin_commands_mutex.synchronize { @plugin_commands[name.to_s] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.plugin_commands_snapshot
|
|
23
|
+
@plugin_commands_mutex.synchronize { @plugin_commands.dup }
|
|
14
24
|
end
|
|
15
25
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browserctl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ferrum
|
|
@@ -52,6 +52,34 @@ dependencies:
|
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '3.1'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: lefthook
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.11'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.11'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '13.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '13.0'
|
|
55
83
|
- !ruby/object:Gem::Dependency
|
|
56
84
|
name: rspec
|
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -111,6 +139,8 @@ files:
|
|
|
111
139
|
- bin/browserd
|
|
112
140
|
- bin/setup
|
|
113
141
|
- examples/cloudflare_hitl.rb
|
|
142
|
+
- examples/smoke/params_file.rb
|
|
143
|
+
- examples/smoke/store_fetch.rb
|
|
114
144
|
- examples/the_internet/add_remove_elements.rb
|
|
115
145
|
- examples/the_internet/checkboxes.rb
|
|
116
146
|
- examples/the_internet/dropdown.rb
|
|
@@ -120,21 +150,35 @@ files:
|
|
|
120
150
|
- lib/browserctl/client.rb
|
|
121
151
|
- lib/browserctl/commands/cli_output.rb
|
|
122
152
|
- lib/browserctl/commands/click.rb
|
|
153
|
+
- lib/browserctl/commands/export_cookies.rb
|
|
123
154
|
- lib/browserctl/commands/fill.rb
|
|
155
|
+
- lib/browserctl/commands/import_cookies.rb
|
|
124
156
|
- lib/browserctl/commands/init.rb
|
|
125
157
|
- lib/browserctl/commands/inspect.rb
|
|
126
158
|
- lib/browserctl/commands/open_page.rb
|
|
127
|
-
- lib/browserctl/commands/
|
|
159
|
+
- lib/browserctl/commands/pause.rb
|
|
128
160
|
- lib/browserctl/commands/record.rb
|
|
161
|
+
- lib/browserctl/commands/resume.rb
|
|
129
162
|
- lib/browserctl/commands/screenshot.rb
|
|
130
163
|
- lib/browserctl/commands/snapshot.rb
|
|
164
|
+
- lib/browserctl/commands/status.rb
|
|
131
165
|
- lib/browserctl/commands/watch.rb
|
|
132
166
|
- lib/browserctl/constants.rb
|
|
167
|
+
- lib/browserctl/detectors.rb
|
|
168
|
+
- lib/browserctl/errors.rb
|
|
133
169
|
- lib/browserctl/logger.rb
|
|
170
|
+
- lib/browserctl/policy.rb
|
|
134
171
|
- lib/browserctl/recording.rb
|
|
135
172
|
- lib/browserctl/runner.rb
|
|
136
173
|
- lib/browserctl/server.rb
|
|
137
174
|
- lib/browserctl/server/command_dispatcher.rb
|
|
175
|
+
- lib/browserctl/server/handlers/cookies.rb
|
|
176
|
+
- lib/browserctl/server/handlers/daemon_control.rb
|
|
177
|
+
- lib/browserctl/server/handlers/devtools.rb
|
|
178
|
+
- lib/browserctl/server/handlers/hitl.rb
|
|
179
|
+
- lib/browserctl/server/handlers/navigation.rb
|
|
180
|
+
- lib/browserctl/server/handlers/observation.rb
|
|
181
|
+
- lib/browserctl/server/handlers/page_lifecycle.rb
|
|
138
182
|
- lib/browserctl/server/idle_watcher.rb
|
|
139
183
|
- lib/browserctl/server/page_session.rb
|
|
140
184
|
- lib/browserctl/server/snapshot_builder.rb
|
|
@@ -157,7 +201,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
157
201
|
requirements:
|
|
158
202
|
- - ">="
|
|
159
203
|
- !ruby/object:Gem::Version
|
|
160
|
-
version: '3.
|
|
204
|
+
version: '3.3'
|
|
161
205
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
206
|
requirements:
|
|
163
207
|
- - ">="
|