browserctl 0.2.0 → 0.3.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 +39 -0
- data/README.md +7 -1
- data/bin/browserctl +73 -32
- data/bin/browserd +5 -3
- data/examples/cloudflare_hitl.rb +65 -0
- data/lib/browserctl/client.rb +99 -2
- data/lib/browserctl/commands/cli_output.rb +15 -0
- data/lib/browserctl/commands/click.rb +13 -7
- data/lib/browserctl/commands/fill.rb +23 -9
- data/lib/browserctl/commands/init.rb +29 -0
- data/lib/browserctl/commands/inspect.rb +21 -0
- data/lib/browserctl/commands/open_page.rb +10 -5
- data/lib/browserctl/commands/pause_resume.rb +32 -0
- data/lib/browserctl/commands/record.rb +13 -18
- data/lib/browserctl/commands/screenshot.rb +10 -4
- data/lib/browserctl/commands/snapshot.rb +19 -8
- data/lib/browserctl/commands/watch.rb +13 -3
- data/lib/browserctl/recording.rb +3 -1
- data/lib/browserctl/runner.rb +19 -0
- data/lib/browserctl/server/command_dispatcher.rb +171 -53
- data/lib/browserctl/server/page_session.rb +21 -0
- data/lib/browserctl/server.rb +3 -3
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow.rb +7 -0
- data/lib/browserctl.rb +8 -0
- metadata +21 -2
- data/lib/browserctl/commands/flag_extractor.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac5d5a587f5d9862ef7bd7839f7e7e817acd742ed0a1c6d6e33913eac1be1444
|
|
4
|
+
data.tar.gz: f28af579b05237429959a444c0369550bcc848c04644bf172b95a4927657b40c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eca2babe456ed7a818ab908bbb9d0ec5fecb0a0474cdfe5335dbc5112667b8bf2e031a5a278dd8369b2009b284def98839ea26f712d88d94c2b89c061b5718f7
|
|
7
|
+
data.tar.gz: da2d6b4eff687f5257e178a00a96b3ec9b93256fc191b386848328874bd5070cb2631a594b039550a8e3ec6ff9f00fbbfbd088d9ee594cc1d04166c6074f0c60
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0](https://github.com/patrick204nqh/browserctl/compare/v0.2.2...v0.3.0) (2026-04-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add cookies, set_cookie, clear_cookies commands for CF clearance replay ([dd38f2d](https://github.com/patrick204nqh/browserctl/commit/dd38f2d587d12f789072326310a1f490e7f993b7))
|
|
14
|
+
* add cookies, set_cookie, clear_cookies commands for CF clearance replay ([c773fe2](https://github.com/patrick204nqh/browserctl/commit/c773fe25e73660cbe5c7e0e3d698fbf7709df51d))
|
|
15
|
+
* browserctl v0.3 — HITL, Cloudflare detection, init, compose, plugins, inspect, RBS, YARD ([ec24c85](https://github.com/patrick204nqh/browserctl/commit/ec24c85a7772b508c477ef4413b0b4a64be8dfa2))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* rubocop offenses — bump ClassLength max to 240, fix hash alignment ([52669fd](https://github.com/patrick204nqh/browserctl/commit/52669fd21051e1fef581d1c6acd3189d7d805dfb))
|
|
21
|
+
* rubocop offenses — move CLOUDFLARE_SIGNALS above private, autocorrect style ([0ad3d38](https://github.com/patrick204nqh/browserctl/commit/0ad3d38127671284c935809e189478c7160294f6))
|
|
22
|
+
|
|
23
|
+
## [0.2.2](https://github.com/patrick204nqh/browserctl/compare/v0.2.1...v0.2.2) (2026-04-20)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* never record plaintext fill values in session recordings ([a0ea9a1](https://github.com/patrick204nqh/browserctl/commit/a0ea9a15f4dbdc29a0a6aaa8186fa313e601c691))
|
|
29
|
+
* rescue Timeout::Error in quietly; remove duplicate [@last](https://github.com/last)_used update ([d0b1430](https://github.com/patrick204nqh/browserctl/commit/d0b143051a4a6adcdc80620388390bd649ee0bce))
|
|
30
|
+
* restrict screenshot path to ~/.browserctl/screenshots ([0a1303f](https://github.com/patrick204nqh/browserctl/commit/0a1303f95b1df3135a81c98e531ac31c99e2e275))
|
|
31
|
+
* rubocop offenses — constant scope and hash alignment ([cc11bc5](https://github.com/patrick204nqh/browserctl/commit/cc11bc546746c7f341573ffff43b02fdc94578d8))
|
|
32
|
+
* security patches and PageSession architecture refactor (review fixes) ([a01d0f7](https://github.com/patrick204nqh/browserctl/commit/a01d0f7bd3f58b51d21fc534a67dda25c15b029b))
|
|
33
|
+
* trigger release on tag push; revert release-please to default token ([011d4dc](https://github.com/patrick204nqh/browserctl/commit/011d4dcddd872f0ed88548ee9c2e8b19d64ad5c2))
|
|
34
|
+
* update CI workflows to use actions/checkout@v4 and set timeout for jobs ([15ab5d4](https://github.com/patrick204nqh/browserctl/commit/15ab5d471952ff9759eff7ff77ce2df30eea5e1f))
|
|
35
|
+
* validate workflow name to prevent path traversal ([1d0f221](https://github.com/patrick204nqh/browserctl/commit/1d0f221b35a855c1991bee5177b7e742ecb16e86))
|
|
36
|
+
|
|
37
|
+
## [0.2.1](https://github.com/patrick204nqh/browserctl/compare/v0.2.0...v0.2.1) (2026-04-20)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
|
|
42
|
+
* align CLI with standard conventions ([215c2af](https://github.com/patrick204nqh/browserctl/commit/215c2af3a4c4d27397476932147f3fda39e4039c))
|
|
43
|
+
* remove flag_extractor spec after deleting the module ([cd80045](https://github.com/patrick204nqh/browserctl/commit/cd8004515fcbae68248d7133e10d5c0223be10e1))
|
|
44
|
+
* use GH_PAT for release-please to trigger release workflow ([da83358](https://github.com/patrick204nqh/browserctl/commit/da83358e8ff19e8a30c23b9f5d744cb11ffbc7c6))
|
|
45
|
+
* use PAT for release-please to trigger release workflow ([4a9ea99](https://github.com/patrick204nqh/browserctl/commit/4a9ea99f418cbf719a16e2a971672175e0292221))
|
|
46
|
+
|
|
8
47
|
## [0.2.0](https://github.com/patrick204nqh/browserctl/compare/v0.1.1...v0.2.0) (2026-04-20)
|
|
9
48
|
|
|
10
49
|
|
data/README.md
CHANGED
|
@@ -138,6 +138,12 @@ browserctl shutdown
|
|
|
138
138
|
| `shot <page> [--out PATH] [--full]` | Take a screenshot |
|
|
139
139
|
| `url <page>` | Print current URL |
|
|
140
140
|
| `eval <page> <expression>` | Evaluate a JS expression |
|
|
141
|
+
| `pause <page>` | Pause automation — browser stays live for manual interaction |
|
|
142
|
+
| `resume <page>` | Resume automation after manual action |
|
|
143
|
+
| `inspect <page>` | Open Chrome DevTools for a named page |
|
|
144
|
+
| `cookies <page>` | List all cookies as JSON |
|
|
145
|
+
| `set_cookie <page> <name> <value> <domain>` | Set a cookie (path defaults to `/`) |
|
|
146
|
+
| `clear_cookies <page>` | Clear all cookies for a page |
|
|
141
147
|
| `record start <name>` | Begin recording commands as a replayable workflow |
|
|
142
148
|
| `record stop [--out path]` | End recording; saves to `.browserctl/workflows/` or custom path |
|
|
143
149
|
| `record status` | Show whether a recording is active |
|
|
@@ -262,7 +268,7 @@ browserctl run smoke_login --email me@example.com --password s3cr3t
|
|
|
262
268
|
|
|
263
269
|
### PageProxy methods
|
|
264
270
|
|
|
265
|
-
`goto(url)` · `fill(selector, value)` · `click(selector)` · `snapshot(**opts)` · `screenshot(**opts)` · `wait_for(selector, timeout: 10)` · `url` · `evaluate(expression)`
|
|
271
|
+
`goto(url)` · `fill(selector, value)` · `click(selector)` · `snapshot(**opts)` · `screenshot(**opts)` · `wait_for(selector, timeout: 10)` · `url` · `evaluate(expression)` · `pause` · `resume` · `inspect_page` · `cookies` · `set_cookie(name, value, domain, path: "/")` · `clear_cookies`
|
|
266
272
|
|
|
267
273
|
---
|
|
268
274
|
|
data/bin/browserctl
CHANGED
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
5
5
|
|
|
6
|
+
require "browserctl/version"
|
|
7
|
+
|
|
8
|
+
if ARGV.intersect?(%w[--version -v])
|
|
9
|
+
puts "browserctl #{Browserctl::VERSION}"
|
|
10
|
+
exit 0
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
require "json"
|
|
14
|
+
require "optimist"
|
|
7
15
|
require "browserctl"
|
|
16
|
+
require "browserctl/commands/cli_output"
|
|
8
17
|
require "browserctl/commands/open_page"
|
|
9
18
|
require "browserctl/commands/fill"
|
|
10
19
|
require "browserctl/commands/click"
|
|
@@ -12,43 +21,68 @@ require "browserctl/commands/snapshot"
|
|
|
12
21
|
require "browserctl/commands/screenshot"
|
|
13
22
|
require "browserctl/commands/watch"
|
|
14
23
|
require "browserctl/commands/record"
|
|
24
|
+
require "browserctl/commands/pause_resume"
|
|
25
|
+
require "browserctl/commands/init"
|
|
26
|
+
require "browserctl/commands/inspect"
|
|
27
|
+
|
|
28
|
+
def print_result(res)
|
|
29
|
+
if res.is_a?(Hash) && res[:error]
|
|
30
|
+
warn "Error: #{res[:error]}"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
puts res.to_json
|
|
34
|
+
end
|
|
15
35
|
|
|
36
|
+
# rubocop:disable Metrics/MethodLength
|
|
16
37
|
def usage
|
|
17
38
|
puts <<~USAGE
|
|
18
39
|
Usage: browserctl <command> [args]
|
|
19
40
|
|
|
41
|
+
Setup:
|
|
42
|
+
init Scaffold .browserctl/ in this project
|
|
43
|
+
|
|
20
44
|
Browser commands (require browserd running):
|
|
21
|
-
open <page> [--url URL]
|
|
22
|
-
close <page>
|
|
23
|
-
pages
|
|
24
|
-
goto <page> <url>
|
|
25
|
-
fill <page> <selector> <value>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
open <page> [--url URL] Open or focus a named page
|
|
46
|
+
close <page> Close a named page
|
|
47
|
+
pages List open pages
|
|
48
|
+
goto <page> <url> Navigate a page
|
|
49
|
+
fill <page> <selector> <value> Fill an input
|
|
50
|
+
<page> --ref <ref> --value <value> Fill via snapshot ref
|
|
51
|
+
click <page> <selector> Click an element
|
|
52
|
+
<page> --ref <ref> Click via snapshot ref
|
|
53
|
+
shot <page> [--out PATH] [--full] Take a screenshot
|
|
54
|
+
snap <page> [--format ai|html] [--diff] Snapshot DOM (default: ai)
|
|
55
|
+
url <page> Print current URL
|
|
56
|
+
eval <page> <expression> Evaluate JS expression
|
|
57
|
+
watch <page> <selector> [--timeout N] Wait for a selector to appear
|
|
58
|
+
pause <page> Pause automation — browser stays live
|
|
59
|
+
resume <page> Resume automation after manual action
|
|
60
|
+
inspect <page> Open Chrome DevTools for a named page
|
|
61
|
+
cookies <page> List all cookies as JSON
|
|
62
|
+
set_cookie <page> <name> <value> <domain> Set a cookie (path defaults to /)
|
|
63
|
+
clear_cookies <page> Clear all cookies for a page
|
|
32
64
|
|
|
33
65
|
Recording commands:
|
|
34
|
-
record start <name>
|
|
35
|
-
record stop
|
|
36
|
-
record status
|
|
66
|
+
record start <name> Start recording browser commands
|
|
67
|
+
record stop [--out PATH] Stop recording and save workflow
|
|
68
|
+
record status Show active recording name
|
|
37
69
|
|
|
38
70
|
Workflow commands:
|
|
39
|
-
run <name|file> [--key value] Run a workflow
|
|
40
|
-
workflows
|
|
41
|
-
describe <name>
|
|
71
|
+
run <name|file> [--key value ...] Run a workflow
|
|
72
|
+
workflows List available workflows
|
|
73
|
+
describe <name> Describe a workflow
|
|
42
74
|
|
|
43
75
|
Daemon commands:
|
|
44
|
-
ping
|
|
45
|
-
shutdown
|
|
76
|
+
ping Check if browserd is alive
|
|
77
|
+
shutdown Stop browserd
|
|
46
78
|
|
|
47
79
|
Options:
|
|
48
80
|
--daemon <name> Connect to a named daemon instance
|
|
81
|
+
--version, -v Print version and exit
|
|
49
82
|
USAGE
|
|
50
83
|
exit 0
|
|
51
84
|
end
|
|
85
|
+
# rubocop:enable Metrics/MethodLength
|
|
52
86
|
|
|
53
87
|
daemon_idx = ARGV.index("--daemon")
|
|
54
88
|
daemon_name = if daemon_idx
|
|
@@ -88,24 +122,31 @@ when "describe"
|
|
|
88
122
|
puts JSON.pretty_generate(runner.describe_workflow(name))
|
|
89
123
|
|
|
90
124
|
when "record" then Browserctl::Commands::Record.run(args)
|
|
125
|
+
when "init" then Browserctl::Commands::Init.run(args)
|
|
91
126
|
|
|
92
127
|
else
|
|
93
128
|
client = Browserctl::Client.new(Browserctl.socket_path(daemon_name))
|
|
94
129
|
|
|
95
130
|
case cmd
|
|
96
|
-
when "open"
|
|
97
|
-
when "close"
|
|
98
|
-
when "pages"
|
|
99
|
-
when "goto"
|
|
100
|
-
when "fill"
|
|
101
|
-
when "click"
|
|
102
|
-
when "shot"
|
|
103
|
-
when "snap"
|
|
104
|
-
when "url"
|
|
105
|
-
when "eval"
|
|
106
|
-
when "watch"
|
|
107
|
-
when "
|
|
108
|
-
when "
|
|
131
|
+
when "open" then Browserctl::Commands::OpenPage.run(client, args)
|
|
132
|
+
when "close" then print_result(client.close_page(args[0]))
|
|
133
|
+
when "pages" then print_result(client.list_pages)
|
|
134
|
+
when "goto" then print_result(client.goto(args[0], args[1]))
|
|
135
|
+
when "fill" then Browserctl::Commands::Fill.run(client, args)
|
|
136
|
+
when "click" then Browserctl::Commands::Click.run(client, args)
|
|
137
|
+
when "shot" then Browserctl::Commands::Screenshot.run(client, args)
|
|
138
|
+
when "snap" then Browserctl::Commands::Snapshot.run(client, args)
|
|
139
|
+
when "url" then print_result(client.url(args[0]))
|
|
140
|
+
when "eval" then print_result(client.evaluate(args[0], args[1]))
|
|
141
|
+
when "watch" then Browserctl::Commands::Watch.run(client, args)
|
|
142
|
+
when "pause" then Browserctl::Commands::PauseResume.pause(client, args)
|
|
143
|
+
when "resume" then Browserctl::Commands::PauseResume.resume(client, args)
|
|
144
|
+
when "inspect" then Browserctl::Commands::Inspect.run(client, args)
|
|
145
|
+
when "cookies" then print_result(client.cookies(args[0]))
|
|
146
|
+
when "set_cookie" then print_result(client.set_cookie(args[0], args[1], args[2], args[3]))
|
|
147
|
+
when "clear_cookies" then print_result(client.clear_cookies(args[0]))
|
|
148
|
+
when "ping" then print_result(client.ping)
|
|
149
|
+
when "shutdown" then print_result(client.shutdown)
|
|
109
150
|
else
|
|
110
151
|
abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
|
|
111
152
|
end
|
data/bin/browserd
CHANGED
|
@@ -7,11 +7,13 @@ require "optimist"
|
|
|
7
7
|
require "nokogiri"
|
|
8
8
|
require "browserctl/logger"
|
|
9
9
|
require "browserctl/server"
|
|
10
|
+
require "browserctl/version"
|
|
10
11
|
|
|
11
12
|
opts = Optimist.options do
|
|
12
|
-
|
|
13
|
-
opt :
|
|
14
|
-
opt :
|
|
13
|
+
version "browserd #{Browserctl::VERSION}"
|
|
14
|
+
opt :headed, "Run with a visible browser window", default: false, short: "-H"
|
|
15
|
+
opt :log_level, "Log verbosity: debug, info, warn, error", default: "info", short: "-l", type: :string
|
|
16
|
+
opt :name, "Daemon instance name for multi-agent use", default: nil, short: "-n", type: :string
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
Browserctl.logger = Browserctl.build_logger(opts[:log_level])
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Demonstrates the Cloudflare HITL (Human-in-the-Loop) pattern.
|
|
4
|
+
#
|
|
5
|
+
# When an agent navigates to a Cloudflare-protected page and hits a bot challenge,
|
|
6
|
+
# it cannot interact with the real page content. This workflow detects the challenge,
|
|
7
|
+
# pauses automation so a human can solve it in the live browser, then resumes.
|
|
8
|
+
#
|
|
9
|
+
# Run:
|
|
10
|
+
# browserctl run examples/cloudflare_hitl.rb --url https://example.com
|
|
11
|
+
#
|
|
12
|
+
# Note: modern Cloudflare often passes a real headed Chrome without challenge.
|
|
13
|
+
# The pause/resume branch fires only when the challenge page is actually served
|
|
14
|
+
# (typically against headless or CDP-fingerprinted browsers, or stricter zones).
|
|
15
|
+
# When paused, the terminal prints instructions. Solve the challenge in the open
|
|
16
|
+
# browser window, then run: browserctl resume main
|
|
17
|
+
|
|
18
|
+
Browserctl.workflow "cloudflare_hitl" do
|
|
19
|
+
desc "Navigate a Cloudflare-protected URL with human-assisted challenge bypass"
|
|
20
|
+
|
|
21
|
+
param :url, required: true
|
|
22
|
+
param :selector, default: "body"
|
|
23
|
+
|
|
24
|
+
step "open page" do
|
|
25
|
+
client.open_page("main")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
step "navigate to target URL" do
|
|
29
|
+
res = client.goto("main", url)
|
|
30
|
+
|
|
31
|
+
if res[:challenge]
|
|
32
|
+
$stdout.puts ""
|
|
33
|
+
$stdout.puts " ⚠ Cloudflare challenge detected on #{url}"
|
|
34
|
+
$stdout.puts " → Pausing automation. Solve the challenge in the browser, then run:"
|
|
35
|
+
$stdout.puts " browserctl resume main"
|
|
36
|
+
$stdout.puts ""
|
|
37
|
+
|
|
38
|
+
client.pause("main")
|
|
39
|
+
|
|
40
|
+
# Block here until the human calls `browserctl resume main`.
|
|
41
|
+
# The pause command marks the page; resume unblocks and signals the CV.
|
|
42
|
+
# In a workflow context, we poll the server until the page is unpaused
|
|
43
|
+
# by attempting a lightweight snapshot — once it succeeds, we're through.
|
|
44
|
+
loop do
|
|
45
|
+
snap = client.snapshot("main", format: "html")
|
|
46
|
+
break unless snap[:challenge]
|
|
47
|
+
|
|
48
|
+
$stdout.puts " … still on challenge page, waiting 3s"
|
|
49
|
+
sleep 3
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
$stdout.puts " ✓ Challenge cleared — resuming automation"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
step "wait for content and snapshot" do
|
|
57
|
+
page(:main).wait_for(selector, timeout: 15)
|
|
58
|
+
result = page(:main).snapshot(format: "ai")
|
|
59
|
+
$stdout.puts " Snapshot: #{result[:snapshot]&.length || 0} elements captured"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
step "close page" do
|
|
63
|
+
client.close_page("main")
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/browserctl/client.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "constants"
|
|
|
6
6
|
require_relative "recording"
|
|
7
7
|
|
|
8
8
|
module Browserctl
|
|
9
|
+
# Thin IPC client that wraps each browserd command as a Ruby method call.
|
|
9
10
|
class Client
|
|
10
11
|
def initialize(socket_path = Browserctl.socket_path)
|
|
11
12
|
@socket_path = socket_path
|
|
@@ -19,42 +20,138 @@ module Browserctl
|
|
|
19
20
|
raise "browserd is not running — start it with: browserd"
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
#
|
|
23
|
-
|
|
23
|
+
# Opens or focuses a named browser page.
|
|
24
|
+
# @param name [String] logical page name
|
|
25
|
+
# @param url [String, nil] optional URL to navigate to after opening
|
|
26
|
+
# @return [Hash] `{ ok: true, name: }` or `{ error: }`
|
|
24
27
|
def open_page(name, url: nil) = call("open_page", name: name, url: url)
|
|
28
|
+
|
|
29
|
+
# Closes a named page and removes it from the session.
|
|
30
|
+
# @param name [String] logical page name
|
|
31
|
+
# @return [Hash] `{ ok: true }` or `{ error: }`
|
|
25
32
|
def close_page(name) = call("close_page", name: name)
|
|
33
|
+
|
|
34
|
+
# Lists all open page names.
|
|
35
|
+
# @return [Hash] `{ pages: [String] }`
|
|
26
36
|
def list_pages = call("list_pages")
|
|
37
|
+
|
|
38
|
+
# Navigates a page to a URL. Returns `challenge: true` when Cloudflare is detected.
|
|
39
|
+
# @param name [String] logical page name
|
|
40
|
+
# @param url [String] destination URL
|
|
41
|
+
# @return [Hash] `{ ok: true, url:, challenge: }` or `{ error: }`
|
|
27
42
|
def goto(name, url) = call("goto", name: name, url: url)
|
|
28
43
|
|
|
44
|
+
# Clicks an element identified by CSS selector or snapshot ref.
|
|
45
|
+
# @param name [String] logical page name
|
|
46
|
+
# @param selector [String, nil] CSS selector
|
|
47
|
+
# @param ref [String, nil] snapshot ref (e.g. "e3")
|
|
48
|
+
# @return [Hash] `{ ok: true }` or `{ error: }`
|
|
29
49
|
def click(name, selector = nil, ref: nil)
|
|
30
50
|
raise ArgumentError, "click: provide selector or ref:" unless selector || ref
|
|
31
51
|
|
|
32
52
|
call("click", name: name, selector: selector, ref: ref)
|
|
33
53
|
end
|
|
34
54
|
|
|
55
|
+
# Fills an input element with a value.
|
|
56
|
+
# @param name [String] logical page name
|
|
57
|
+
# @param selector [String, nil] CSS selector
|
|
58
|
+
# @param value [String, nil] text to type
|
|
59
|
+
# @param ref [String, nil] snapshot ref
|
|
60
|
+
# @return [Hash] `{ ok: true }` or `{ error: }`
|
|
35
61
|
def fill(name, selector = nil, value = nil, ref: nil)
|
|
36
62
|
raise ArgumentError, "fill: provide selector or ref:" unless selector || ref
|
|
37
63
|
|
|
38
64
|
call("fill", name: name, selector: selector, ref: ref, value: value)
|
|
39
65
|
end
|
|
40
66
|
|
|
67
|
+
# Takes a screenshot of a named page.
|
|
68
|
+
# @param name [String] logical page name
|
|
69
|
+
# @param path [String, nil] output path (default: ~/.browserctl/screenshots/)
|
|
70
|
+
# @param full [Boolean] capture full page (default: false)
|
|
71
|
+
# @return [Hash] `{ ok: true, path: }` or `{ error: }`
|
|
41
72
|
def screenshot(name, path: nil, full: false) = call("screenshot", name: name, path: path, full: full)
|
|
42
73
|
|
|
74
|
+
# Takes a DOM snapshot. Returns `challenge: true` when Cloudflare is detected.
|
|
75
|
+
# @param name [String] logical page name
|
|
76
|
+
# @param format [String] "ai" (token-efficient JSON) or "html" (raw HTML)
|
|
77
|
+
# @param diff [Boolean] return only elements changed since last snapshot
|
|
78
|
+
# @return [Hash] `{ ok: true, snapshot:, challenge: }` or `{ ok: true, html:, challenge: }` or `{ error: }`
|
|
43
79
|
def snapshot(name, format: "ai", diff: false)
|
|
44
80
|
call("snapshot", name: name, format: format, diff: diff)
|
|
45
81
|
end
|
|
46
82
|
|
|
83
|
+
# Waits for a CSS selector to appear (short timeout).
|
|
84
|
+
# @param name [String] logical page name
|
|
85
|
+
# @param selector [String] CSS selector to wait for
|
|
86
|
+
# @param timeout [Numeric] seconds before giving up (default: 10)
|
|
87
|
+
# @return [Hash] `{ ok: true }` or `{ error: }`
|
|
47
88
|
def wait_for(name, selector, timeout: 10) = call("wait_for", name: name, selector: selector, timeout: timeout)
|
|
48
89
|
|
|
90
|
+
# Polls for a CSS selector with a longer timeout (suitable for async operations).
|
|
91
|
+
# @param name [String] logical page name
|
|
92
|
+
# @param selector [String] CSS selector to poll for
|
|
93
|
+
# @param timeout [Numeric] seconds before giving up (default: 30)
|
|
94
|
+
# @return [Hash] `{ ok: true, selector: }` or `{ error: }`
|
|
49
95
|
def watch(name, selector, timeout: 30)
|
|
50
96
|
call("watch", name: name, selector: selector, timeout: timeout)
|
|
51
97
|
end
|
|
52
98
|
|
|
99
|
+
# Returns the current URL of a named page.
|
|
100
|
+
# @param name [String] logical page name
|
|
101
|
+
# @return [Hash] `{ ok: true, url: }` or `{ error: }`
|
|
53
102
|
def url(name) = call("url", name: name)
|
|
103
|
+
|
|
104
|
+
# Evaluates a JavaScript expression and returns the result.
|
|
105
|
+
# @param name [String] logical page name
|
|
106
|
+
# @param expression [String] JavaScript expression
|
|
107
|
+
# @return [Hash] `{ ok: true, result: }` or `{ error: }`
|
|
54
108
|
def evaluate(name, expression) = call("evaluate", name: name, expression: expression)
|
|
109
|
+
|
|
110
|
+
# Checks if browserd is alive.
|
|
111
|
+
# @return [Hash] `{ ok: true, pid: }` or raises if daemon is not running
|
|
55
112
|
def ping = call("ping")
|
|
113
|
+
|
|
114
|
+
# Shuts down browserd gracefully.
|
|
115
|
+
# @return [Hash] `{ ok: true }`
|
|
56
116
|
def shutdown = call("shutdown")
|
|
57
117
|
|
|
118
|
+
# Pauses automation on a page so a human can interact directly.
|
|
119
|
+
# @param name [String] logical page name
|
|
120
|
+
# @return [Hash] `{ ok: true, paused: true }` or `{ error: }`
|
|
121
|
+
def pause(name) = call("pause", name: name)
|
|
122
|
+
|
|
123
|
+
# Resumes automation on a paused page.
|
|
124
|
+
# @param name [String] logical page name
|
|
125
|
+
# @return [Hash] `{ ok: true, paused: false }` or `{ error: }`
|
|
126
|
+
def resume(name) = call("resume", name: name)
|
|
127
|
+
|
|
128
|
+
# Returns the Chrome DevTools URL for a named page.
|
|
129
|
+
# @param name [String] logical page name
|
|
130
|
+
# @return [Hash] `{ ok: true, devtools_url: }` or `{ error: }`
|
|
131
|
+
def inspect_page(name) = call("inspect", name: name)
|
|
132
|
+
|
|
133
|
+
# Returns all cookies for a named page.
|
|
134
|
+
# @param name [String] logical page name
|
|
135
|
+
# @return [Hash] `{ ok: true, cookies: [Hash] }` or `{ error: }`
|
|
136
|
+
def cookies(name) = call("cookies", name: name)
|
|
137
|
+
|
|
138
|
+
# Sets a cookie on a named page.
|
|
139
|
+
# @param name [String] logical page name
|
|
140
|
+
# @param cookie_name [String] cookie name (e.g. "cf_clearance")
|
|
141
|
+
# @param value [String] cookie value
|
|
142
|
+
# @param domain [String] cookie domain (e.g. ".example.com")
|
|
143
|
+
# @param path [String] cookie path (default: "/")
|
|
144
|
+
# @return [Hash] `{ ok: true }` or `{ error: }`
|
|
145
|
+
def set_cookie(name, cookie_name, value, domain, path: "/")
|
|
146
|
+
call("set_cookie", name: name, cookie_name: cookie_name,
|
|
147
|
+
value: value, domain: domain, path: path)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Clears all cookies for a named page.
|
|
151
|
+
# @param name [String] logical page name
|
|
152
|
+
# @return [Hash] `{ ok: true }` or `{ error: }`
|
|
153
|
+
def clear_cookies(name) = call("clear_cookies", name: name)
|
|
154
|
+
|
|
58
155
|
private
|
|
59
156
|
|
|
60
157
|
def communicate(payload)
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "optimist"
|
|
4
|
+
require_relative "cli_output"
|
|
4
5
|
|
|
5
6
|
module Browserctl
|
|
6
7
|
module Commands
|
|
7
8
|
class Click
|
|
9
|
+
extend CliOutput
|
|
10
|
+
|
|
8
11
|
def self.run(client, args)
|
|
12
|
+
opts = Optimist.options(args) do
|
|
13
|
+
banner "Usage: browserctl click <page> <selector>\n " \
|
|
14
|
+
"browserctl click <page> --ref <ref>"
|
|
15
|
+
opt :ref, "Snapshot ref to click", type: :string, short: "-r"
|
|
16
|
+
end
|
|
9
17
|
name = args.shift
|
|
10
|
-
|
|
11
|
-
selector = args.shift unless ref
|
|
12
|
-
|
|
13
|
-
if ref
|
|
18
|
+
if opts[:ref]
|
|
14
19
|
abort "usage: browserctl click <page> --ref <ref>" unless name
|
|
15
|
-
|
|
20
|
+
print_result(client.click(name, ref: opts[:ref]))
|
|
16
21
|
else
|
|
22
|
+
selector = args.shift
|
|
17
23
|
abort "usage: browserctl click <page> <selector>" unless name && selector
|
|
18
|
-
|
|
24
|
+
print_result(client.click(name, selector))
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "optimist"
|
|
4
|
+
require_relative "cli_output"
|
|
4
5
|
|
|
5
6
|
module Browserctl
|
|
6
7
|
module Commands
|
|
7
8
|
class Fill
|
|
9
|
+
extend CliOutput
|
|
10
|
+
|
|
8
11
|
def self.run(client, args)
|
|
12
|
+
opts = Optimist.options(args) do
|
|
13
|
+
banner "Usage: browserctl fill <page> <selector> <value>\n " \
|
|
14
|
+
"browserctl fill <page> --ref <ref> --value <value>"
|
|
15
|
+
opt :ref, "Snapshot ref to fill", type: :string, short: "-r"
|
|
16
|
+
opt :value, "Value to fill", type: :string, short: "-V"
|
|
17
|
+
end
|
|
9
18
|
name = args.shift
|
|
10
|
-
ref
|
|
19
|
+
opts[:ref] ? fill_by_ref(client, name, opts) : fill_by_selector(client, name, args, opts[:value])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def fill_by_ref(client, name, opts)
|
|
26
|
+
abort "usage: browserctl fill <page> --ref <ref> --value <value>" unless name && opts[:value]
|
|
27
|
+
print_result(client.fill(name, nil, opts[:value], ref: opts[:ref]))
|
|
28
|
+
end
|
|
11
29
|
|
|
12
|
-
|
|
13
|
-
value = FlagExtractor.extract_opt(args, "--value")
|
|
14
|
-
abort "usage: browserctl fill <page> --ref <ref> --value <value>" unless name && value
|
|
15
|
-
puts client.fill(name, nil, value, ref: ref).to_json
|
|
16
|
-
else
|
|
30
|
+
def fill_by_selector(client, name, args, value_opt)
|
|
17
31
|
selector = args.shift
|
|
18
|
-
value
|
|
32
|
+
value = value_opt || args.shift
|
|
19
33
|
abort "usage: browserctl fill <page> <selector> <value>" unless name && selector && value
|
|
20
|
-
|
|
34
|
+
print_result(client.fill(name, selector, value))
|
|
21
35
|
end
|
|
22
36
|
end
|
|
23
37
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Browserctl
|
|
6
|
+
module Commands
|
|
7
|
+
class Init
|
|
8
|
+
CONFIG_TEMPLATE = <<~YAML
|
|
9
|
+
# browserctl project configuration
|
|
10
|
+
# Uncomment and edit to customise.
|
|
11
|
+
|
|
12
|
+
# daemon: default # named daemon instance (see browserd --name)
|
|
13
|
+
# workflows_dir: .browserctl/workflows
|
|
14
|
+
YAML
|
|
15
|
+
|
|
16
|
+
def self.run(_args)
|
|
17
|
+
FileUtils.mkdir_p(".browserctl/workflows")
|
|
18
|
+
FileUtils.touch(".browserctl/workflows/.keep")
|
|
19
|
+
|
|
20
|
+
config_path = ".browserctl/config.yml"
|
|
21
|
+
File.write(config_path, CONFIG_TEMPLATE) unless File.exist?(config_path)
|
|
22
|
+
|
|
23
|
+
puts "Initialised browserctl project:"
|
|
24
|
+
puts " .browserctl/workflows/ (place workflow .rb files here)"
|
|
25
|
+
puts " .browserctl/config.yml (project settings)"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browserctl
|
|
4
|
+
module Commands
|
|
5
|
+
class Inspect
|
|
6
|
+
def self.run(client, args)
|
|
7
|
+
name = args.shift or abort "usage: browserctl inspect <page>"
|
|
8
|
+
res = client.inspect_page(name)
|
|
9
|
+
if res[:error]
|
|
10
|
+
warn "Error: #{res[:error]}"
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
13
|
+
url = res[:devtools_url]
|
|
14
|
+
puts "Opening DevTools for '#{name}':"
|
|
15
|
+
puts " #{url}"
|
|
16
|
+
opener = RUBY_PLATFORM =~ /darwin/ ? "open" : "xdg-open"
|
|
17
|
+
system(opener, url)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "optimist"
|
|
4
|
+
require_relative "cli_output"
|
|
4
5
|
|
|
5
6
|
module Browserctl
|
|
6
7
|
module Commands
|
|
7
8
|
class OpenPage
|
|
9
|
+
extend CliOutput
|
|
10
|
+
|
|
8
11
|
def self.run(client, args)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
opts = Optimist.options(args) do
|
|
13
|
+
banner "Usage: browserctl open <page> [--url URL]"
|
|
14
|
+
opt :url, "URL to navigate to", type: :string, short: "-u"
|
|
15
|
+
end
|
|
16
|
+
name = args.shift or abort "usage: browserctl open <page> [--url URL]"
|
|
17
|
+
print_result(client.open_page(name, url: opts[:url]))
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|