browserctl 0.2.0 → 0.2.1

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: c12ad0cc3d34e7163bab1d678d65b320e946e0fa16fe7bd3e9f5881855d4801f
4
+ data.tar.gz: 84d80f56270dfc7d252d7e63279f7c7c54123067837af457f5e92e82b2f0384e
5
5
  SHA512:
6
- metadata.gz: 5bc9c337336ac4fb23da1966a9b51e3dffcef8b23ea00a2990310a0aae5978ccf423416118ebe61b83c92ca84f38846ca16ca909a376e2e3e8f13eb97e82b045
7
- data.tar.gz: 13b8b5fcbc17e328210488ce8e50ead2ea8576419dc5d493c06285d8e191a4be7599a04a58b651f501febb351704609be6dc06c0c8c00c827a7c796667072943
6
+ metadata.gz: c95dbbbe2a573635421fe90cd65d2ccc41baf968c32bb65edcea52030e1b0080fd2af0f50ea9d85bc827ff4f537e2c5b4f3a7631c8242380a007df432e820906
7
+ data.tar.gz: 1da50ab443d365e4a4b9ce145671e266fdfb0d83432b7b2313d87233b58b425252d2d9a365b0f8b3c31040961b90f684fe9299867c4cccfcd9600ea88cb93cc1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ 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.2.1](https://github.com/patrick204nqh/browserctl/compare/v0.2.0...v0.2.1) (2026-04-20)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * align CLI with standard conventions ([215c2af](https://github.com/patrick204nqh/browserctl/commit/215c2af3a4c4d27397476932147f3fda39e4039c))
14
+ * remove flag_extractor spec after deleting the module ([cd80045](https://github.com/patrick204nqh/browserctl/commit/cd8004515fcbae68248d7133e10d5c0223be10e1))
15
+ * use GH_PAT for release-please to trigger release workflow ([da83358](https://github.com/patrick204nqh/browserctl/commit/da83358e8ff19e8a30c23b9f5d744cb11ffbc7c6))
16
+ * use PAT for release-please to trigger release workflow ([4a9ea99](https://github.com/patrick204nqh/browserctl/commit/4a9ea99f418cbf719a16e2a971672175e0292221))
17
+
8
18
  ## [0.2.0](https://github.com/patrick204nqh/browserctl/compare/v0.1.1...v0.2.0) (2026-04-20)
9
19
 
10
20
 
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"
@@ -13,42 +22,55 @@ require "browserctl/commands/screenshot"
13
22
  require "browserctl/commands/watch"
14
23
  require "browserctl/commands/record"
15
24
 
25
+ def print_result(res)
26
+ if res.is_a?(Hash) && res[:error]
27
+ warn "Error: #{res[:error]}"
28
+ exit 1
29
+ end
30
+ puts res.to_json
31
+ end
32
+
33
+ # rubocop:disable Metrics/MethodLength
16
34
  def usage
17
35
  puts <<~USAGE
18
36
  Usage: browserctl <command> [args]
19
37
 
20
38
  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
39
+ open <page> [--url URL] Open or focus a named page
40
+ close <page> Close a named page
41
+ pages List open pages
42
+ goto <page> <url> Navigate a page
43
+ fill <page> <selector> <value> Fill an input
44
+ <page> --ref <ref> --value <value> Fill via snapshot ref
45
+ click <page> <selector> Click an element
46
+ <page> --ref <ref> Click via snapshot ref
47
+ shot <page> [--out PATH] [--full] Take a screenshot
48
+ snap <page> [--format ai|html] [--diff] Snapshot DOM (default: ai)
49
+ url <page> Print current URL
50
+ eval <page> <expression> Evaluate JS expression
51
+ watch <page> <selector> [--timeout N] Wait for a selector to appear
32
52
 
33
53
  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
54
+ record start <name> Start recording browser commands
55
+ record stop [--out PATH] Stop recording and save workflow
56
+ record status Show active recording name
37
57
 
38
58
  Workflow commands:
39
- run <name|file> [--key value] Run a workflow
40
- workflows List available workflows
41
- describe <name> Describe a workflow
59
+ run <name|file> [--key value ...] Run a workflow
60
+ workflows List available workflows
61
+ describe <name> Describe a workflow
42
62
 
43
63
  Daemon commands:
44
- ping Check if browserd is alive
45
- shutdown Stop browserd
64
+ ping Check if browserd is alive
65
+ shutdown Stop browserd
46
66
 
47
67
  Options:
48
68
  --daemon <name> Connect to a named daemon instance
69
+ --version, -v Print version and exit
49
70
  USAGE
50
71
  exit 0
51
72
  end
73
+ # rubocop:enable Metrics/MethodLength
52
74
 
53
75
  daemon_idx = ARGV.index("--daemon")
54
76
  daemon_name = if daemon_idx
@@ -93,19 +115,19 @@ else
93
115
  client = Browserctl::Client.new(Browserctl.socket_path(daemon_name))
94
116
 
95
117
  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
118
+ when "open" then Browserctl::Commands::OpenPage.run(client, args)
119
+ when "close" then print_result(client.close_page(args[0]))
120
+ when "pages" then print_result(client.list_pages)
121
+ when "goto" then print_result(client.goto(args[0], args[1]))
122
+ when "fill" then Browserctl::Commands::Fill.run(client, args)
123
+ when "click" then Browserctl::Commands::Click.run(client, args)
124
+ when "shot" then Browserctl::Commands::Screenshot.run(client, args)
125
+ when "snap" then Browserctl::Commands::Snapshot.run(client, args)
126
+ when "url" then print_result(client.url(args[0]))
127
+ when "eval" then print_result(client.evaluate(args[0], args[1]))
128
+ when "watch" then Browserctl::Commands::Watch.run(client, args)
129
+ when "ping" then print_result(client.ping)
130
+ when "shutdown" then print_result(client.shutdown)
109
131
  else
110
132
  abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
111
133
  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,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
@@ -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
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "optimist"
4
5
  require "browserctl/recording"
5
6
 
6
7
  module Browserctl
7
8
  module Commands
8
9
  class Record
9
- USAGE = "usage: browserctl record start <name> | stop [--out path] | status"
10
+ USAGE = "Usage: browserctl record start <name> | stop [--out PATH] | status"
10
11
 
11
12
  def self.run(args)
12
13
  subcmd = args.shift
@@ -14,7 +15,8 @@ module Browserctl
14
15
  when "start" then run_start(args)
15
16
  when "stop" then run_stop(args)
16
17
  when "status" then run_status
17
- else abort USAGE
18
+ else
19
+ abort "#{USAGE}\nRun 'browserctl record <subcommand> --help' for details."
18
20
  end
19
21
  end
20
22
 
@@ -22,6 +24,7 @@ module Browserctl
22
24
  private
23
25
 
24
26
  def run_start(args)
27
+ Optimist.options(args) { banner "Usage: browserctl record start <name>" }
25
28
  name = args.shift or abort "usage: browserctl record start <name>"
26
29
  Recording.start(name)
27
30
  puts "Recording started: #{name}"
@@ -29,23 +32,15 @@ module Browserctl
29
32
  end
30
33
 
31
34
  def run_stop(args)
32
- idx = args.index("--out")
33
- out = if idx
34
- args.delete_at(idx)
35
- args.delete_at(idx)
36
- end
37
- name = Recording.stop
38
- if out
39
- FileUtils.mkdir_p(File.dirname(out))
40
- Recording.generate_workflow(name, output_path: out)
41
- puts "Workflow saved: #{out}"
42
- else
43
- dest_dir = ".browserctl/workflows"
44
- dest_file = File.join(dest_dir, "#{name}.rb")
45
- FileUtils.mkdir_p(dest_dir)
46
- Recording.generate_workflow(name, output_path: dest_file)
47
- puts "Workflow saved: #{dest_file}"
35
+ opts = Optimist.options(args) do
36
+ banner "Usage: browserctl record stop [--out PATH]"
37
+ opt :out, "Output path for workflow file", type: :string, short: "-o"
48
38
  end
39
+ name = Recording.stop
40
+ out = opts[:out] || File.join(".browserctl/workflows", "#{name}.rb")
41
+ FileUtils.mkdir_p(File.dirname(out))
42
+ Recording.generate_workflow(name, output_path: out)
43
+ puts "Workflow saved: #{out}"
49
44
  puts "Run with: browserctl run #{name}"
50
45
  end
51
46
 
@@ -1,15 +1,21 @@
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 Screenshot
9
+ extend CliOutput
10
+
8
11
  def self.run(client, args)
12
+ opts = Optimist.options(args) do
13
+ banner "Usage: browserctl shot <page> [--out PATH] [--full]"
14
+ opt :out, "Output file path", type: :string, short: "-o"
15
+ opt :full, "Capture full page", default: false, short: "-f"
16
+ end
9
17
  name = args.shift or abort "usage: browserctl shot <page> [--out PATH] [--full]"
10
- path = FlagExtractor.extract_opt(args, "--out")
11
- full = args.delete("--full") ? true : false
12
- puts client.screenshot(name, path: path, full: full).to_json
18
+ print_result(client.screenshot(name, path: opts[:out], full: opts[:full]))
13
19
  end
14
20
  end
15
21
  end
@@ -1,25 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require_relative "flag_extractor"
4
+ require "optimist"
5
5
 
6
6
  module Browserctl
7
7
  module Commands
8
8
  class Snapshot
9
+ VALID_FORMATS = %w[ai html].freeze
10
+
9
11
  def self.run(client, args)
10
- name = args.shift or abort "usage: browserctl snap <page> [--format ai|html] [--diff]"
11
- format = FlagExtractor.extract_opt(args, "--format") || "ai"
12
- diff = FlagExtractor.extract_flag?(args, "--diff")
13
- res = client.snapshot(name, format: format, diff: diff)
14
- output_snapshot(res, format)
12
+ opts = Optimist.options(args) do
13
+ banner "Usage: browserctl snap <page> [--format ai|html] [--diff]"
14
+ opt :format, "Output format: ai or html", default: "ai", short: "-f"
15
+ opt :diff, "Return only changed elements", default: false, short: "-d"
16
+ end
17
+ name = args.shift or abort "usage: browserctl snap <page> [--format ai|html] [--diff]"
18
+ unless VALID_FORMATS.include?(opts[:format])
19
+ warn "Error: --format must be one of: #{VALID_FORMATS.join(', ')}"
20
+ exit 1
21
+ end
22
+ res = client.snapshot(name, format: opts[:format], diff: opts[:diff])
23
+ output_snapshot(res, opts[:format])
15
24
  end
16
25
 
17
26
  class << self
18
27
  private
19
28
 
20
29
  def output_snapshot(res, format)
21
- return abort res[:error] if res[:error]
22
-
30
+ if res[:error]
31
+ warn "Error: #{res[:error]}"
32
+ exit 1
33
+ end
23
34
  puts(format == "ai" ? JSON.pretty_generate(res[:snapshot]) : res[:html])
24
35
  end
25
36
  end
@@ -1,16 +1,26 @@
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 Watch
9
+ extend CliOutput
10
+
8
11
  def self.run(client, args)
12
+ opts = Optimist.options(args) do
13
+ banner "Usage: browserctl watch <page> <selector> [--timeout N]"
14
+ opt :timeout, "Seconds to wait (default: 30)", default: 30.0, short: "-t"
15
+ end
9
16
  name = args.shift
10
17
  selector = args.shift
11
- timeout = (FlagExtractor.extract_opt(args, "--timeout") || 30).to_f
12
18
  abort "usage: browserctl watch <page> <selector> [--timeout N]" unless name && selector
13
- puts client.watch(name, selector, timeout: timeout).to_json
19
+ unless opts[:timeout].positive?
20
+ warn "Error: --timeout must be a positive number"
21
+ exit 1
22
+ end
23
+ print_result(client.watch(name, selector, timeout: opts[:timeout]))
14
24
  end
15
25
  end
16
26
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Browserctl
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: browserctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -103,9 +103,9 @@ files:
103
103
  - examples/the_internet/login.rb
104
104
  - lib/browserctl.rb
105
105
  - lib/browserctl/client.rb
106
+ - lib/browserctl/commands/cli_output.rb
106
107
  - lib/browserctl/commands/click.rb
107
108
  - lib/browserctl/commands/fill.rb
108
- - lib/browserctl/commands/flag_extractor.rb
109
109
  - lib/browserctl/commands/open_page.rb
110
110
  - lib/browserctl/commands/record.rb
111
111
  - lib/browserctl/commands/screenshot.rb
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Browserctl
4
- module Commands
5
- module FlagExtractor
6
- def self.extract_opt(args, flag)
7
- i = args.index(flag)
8
- return unless i
9
-
10
- sliced = args.slice!(i, 2)
11
- sliced.length == 2 ? sliced.last : nil
12
- end
13
-
14
- def self.extract_flag?(args, flag)
15
- i = args.index(flag)
16
- return false unless i
17
-
18
- args.delete_at(i)
19
- true
20
- end
21
- end
22
- end
23
- end