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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b5b21accfeb82afa82dc2c12d51ef7da31ed8ab9db073d2b576df0385e5adb6
4
- data.tar.gz: d9cd601ce2d63b5c3f2c8d539d14dc684dcf3753a7f95002b9c165791ff2f584
3
+ metadata.gz: ac5d5a587f5d9862ef7bd7839f7e7e817acd742ed0a1c6d6e33913eac1be1444
4
+ data.tar.gz: f28af579b05237429959a444c0369550bcc848c04644bf172b95a4927657b40c
5
5
  SHA512:
6
- metadata.gz: 5bc9c337336ac4fb23da1966a9b51e3dffcef8b23ea00a2990310a0aae5978ccf423416118ebe61b83c92ca84f38846ca16ca909a376e2e3e8f13eb97e82b045
7
- data.tar.gz: 13b8b5fcbc17e328210488ce8e50ead2ea8576419dc5d493c06285d8e191a4be7599a04a58b651f501febb351704609be6dc06c0c8c00c827a7c796667072943
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] Open or focus a named page
22
- close <page> Close a named page
23
- pages List open pages
24
- goto <page> <url> Navigate a page
25
- fill <page> <selector> <value> Fill an input
26
- click <page> <selector> Click an element
27
- shot <page> [--out PATH] [--full] Take a screenshot
28
- snap <page> [--format ai|html] Snapshot DOM (default: ai)
29
- url <page> Print current URL
30
- eval <page> <expression> Evaluate JS expression
31
- watch <page> <selector> [--timeout N] Wait for a selector to appear
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> Start recording browser commands
35
- record stop [--out path] Stop recording and save workflow
36
- record status Show active recording name
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 List available workflows
41
- describe <name> Describe a workflow
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 Check if browserd is alive
45
- shutdown Stop browserd
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" then Browserctl::Commands::OpenPage.run(client, args)
97
- when "close" then puts client.close_page(args[0]).to_json
98
- when "pages" then puts client.list_pages.to_json
99
- when "goto" then puts client.goto(args[0], args[1]).to_json
100
- when "fill" then Browserctl::Commands::Fill.run(client, args)
101
- when "click" then Browserctl::Commands::Click.run(client, args)
102
- when "shot" then Browserctl::Commands::Screenshot.run(client, args)
103
- when "snap" then Browserctl::Commands::Snapshot.run(client, args)
104
- when "url" then puts client.url(args[0]).to_json
105
- when "eval" then puts client.evaluate(args[0], args[1]).to_json
106
- when "watch" then Browserctl::Commands::Watch.run(client, args)
107
- when "ping" then puts client.ping.to_json
108
- when "shutdown" then puts client.shutdown.to_json
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
- opt :headed, "Run with a visible browser window", default: false
13
- opt :log_level, "Log verbosity: debug, info, warn, error", default: "info", type: :string
14
- opt :name, "Daemon instance name for multi-agent use", default: nil, type: :string
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
@@ -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
- # Convenience wrappers matching CLI command vocabulary
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)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browserctl
4
+ module Commands
5
+ module CliOutput
6
+ def print_result(res)
7
+ if res.is_a?(Hash) && res[:error]
8
+ warn "Error: #{res[:error]}"
9
+ exit 1
10
+ end
11
+ puts res.to_json
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,21 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "flag_extractor"
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
- ref = FlagExtractor.extract_opt(args, "--ref")
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
- puts client.click(name, ref: ref).to_json
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
- puts client.click(name, selector).to_json
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
- require_relative "flag_extractor"
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 = FlagExtractor.extract_opt(args, "--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
- if ref
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 = args.shift
32
+ value = value_opt || args.shift
19
33
  abort "usage: browserctl fill <page> <selector> <value>" unless name && selector && value
20
- puts client.fill(name, selector, value).to_json
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
- require_relative "flag_extractor"
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
- name = args.shift or abort "usage: browserctl open <name> [--url URL]"
10
- url = FlagExtractor.extract_opt(args, "--url")
11
- res = client.open_page(name, url: url)
12
- puts res.to_json
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