browserctl 0.1.0 → 0.1.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: db03c20d156ddbfc0c81267889ce46be79cb9973f2ff55af65739ddfe370f9f6
4
- data.tar.gz: 712a9ffd6206cd0c1e3614d0d7230bb1c998beed56d315313328b0c768c5d3ea
3
+ metadata.gz: 2976bab368591b4cb06cd3a34d64c1078bd8588590d70c67ba3d78ecd721e572
4
+ data.tar.gz: 0fa9c86ec532df2c6763780bfb6c2ac47380ff1df400ae2639fe914fc66e0d04
5
5
  SHA512:
6
- metadata.gz: 7780abfa304abcbfb252fee4a41ff5bd592275e8fe6ac66b30ab3e2022b8e0a48c94b046458f5d10b215cef89f75c94691de16b0158378357f863bf6a2dc5a15
7
- data.tar.gz: d7b0051c9b2c13bf79313cb0e5edb58062ebe4a9ae9a77a836a2e1394a5f537d72aa2bf61bbb3003dcb30d8eef66cd873d68b6cda394eb9a5c6f078ae6143c90
6
+ metadata.gz: ef5ab4df359bde2cc6b635f6ac77c1147df57e85c57ff61222179f223e4d41ef4d58b622aa928cde19c41b1927d6ebafd273847ada8c55bf74cfdc09b6ddb5c7
7
+ data.tar.gz: 0af88adb9b7076a4408c65ac25e7d13fd0f70aa2cdf79448fe9be7953695bd12de01d9c98626628163ce8edb738b4d2c83a925a77e2504924b1ee6bfe85c39d3
data/CHANGELOG.md CHANGED
@@ -5,24 +5,39 @@ 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
- ## [Unreleased]
8
+ ## [0.1.1](https://github.com/patrick204nqh/browserctl/compare/v0.1.0...v0.1.1) (2026-04-19)
9
9
 
10
- ### Fixed
11
- - Thread safety: all server dispatch calls now protected by a Mutex
12
- - `fill` no longer queries the DOM twice; returns an error if selector is not found
13
- - `click` now returns an error when the selector is not found instead of silently succeeding
14
- - `wait_for` now polls for the selector until timeout instead of checking once after network idle
15
- - Circular workflow `invoke` calls now raise a descriptive `WorkflowError`
16
10
 
17
- ### Added
18
- - GitHub Actions CI (lint + test across Ruby 3.2–3.4)
19
- - Gemspec metadata: `changelog_uri`, `source_code_uri`, `bug_tracker_uri`, `rubygems_mfa_required`
11
+ ### Bug Fixes
12
+
13
+ * resolve rubocop offenses in gemspec ([dfb67dc](https://github.com/patrick204nqh/browserctl/commit/dfb67dc135d9538847a2b58a17d1dd03b7ab4b45))
14
+ * synchronize access to pages in CommandDispatcher methods ([c61d129](https://github.com/patrick204nqh/browserctl/commit/c61d129076026d8cae200fdbebb2337f9feccd70))
20
15
 
21
- ## [0.1.0] - 2025-01-01
16
+ ## [0.1.0] - 2026-04-19
22
17
 
23
18
  ### Added
24
- - Initial release: persistent browser daemon (`browserd`) over Unix socket
25
- - CLI (`browserctl`) with commands: open, close, pages, goto, fill, click, shot, snap, url, eval, ping, shutdown
26
- - Ruby workflow DSL with params, steps, assertions, and `invoke`
27
- - AI-optimized DOM snapshot format (JSON with ref IDs)
28
- - Ferrum-backed Chrome/Chromium automation
19
+ - Persistent browser daemon (`browserd`) over Unix socket — preserves cookies, localStorage, and open tabs across discrete commands
20
+ - CLI (`browserctl`) with commands: `open`, `close`, `pages`, `goto`, `fill`, `click`, `shot`, `snap`, `url`, `eval`, `ping`, `shutdown`
21
+ - Ruby workflow DSL with `param`, `step`, `assert`, `page(:name)`, and `invoke` for composing workflows
22
+ - AI-optimized DOM snapshot format (`snap --format ai`) — compact JSON array of interactable elements with stable ref IDs
23
+ - Ferrum-backed Chrome/Chromium automation via Chrome DevTools Protocol
24
+ - Named page handles — multiple pages open simultaneously under descriptive names
25
+ - `wait_for(selector, timeout:)` — polls until an element appears, useful for async content
26
+ - Workflow search paths: `.browserctl/workflows/` (project) and `~/.browserctl/workflows/` (user)
27
+ - `browserctl run <name|file.rb>` — run workflows by name or directly by file path
28
+ - `browserctl workflows` and `browserctl describe <name>` — discover and inspect available workflows
29
+ - GitHub Actions CI: lint (RuboCop) and integration tests
30
+ - Gemspec metadata: `changelog_uri`, `source_code_uri`, `bug_tracker_uri`, `rubygems_mfa_required`
31
+ - Release automation via release-please and RubyGems push on `v*.*.*` tags
32
+ - AI agent skill (`skills/browserctl/SKILL.md`) with probe-before-workflow guidance
33
+ - Workflow authoring guide (`docs/writing-workflows.md`)
34
+ - Smoke test examples against the-internet.herokuapp.com with auto-generated screenshots
35
+
36
+ ### Fixed
37
+ - Thread safety: all server dispatch calls protected by a Mutex
38
+ - `fill` returns an error when selector is not found instead of querying the DOM twice
39
+ - `click` returns an error when selector is not found instead of silently succeeding
40
+ - `wait_for` polls for the selector until timeout instead of checking once after network idle
41
+ - Circular workflow `invoke` calls raise a descriptive `WorkflowError`
42
+ - Chrome launch on CI: added `--no-sandbox` and `--disable-dev-shm-usage` flags for container environments
43
+ - Daemon shutdown no longer hangs: `browser.quit` is wrapped in a 5s timeout; `stop_daemon` sends `SIGKILL` as a fallback after grace period
@@ -20,10 +20,11 @@ module Browserctl
20
20
  "shutdown" => :cmd_shutdown
21
21
  }.freeze
22
22
 
23
- def initialize(pages, browser, snapshot_builder = SnapshotBuilder.new)
23
+ def initialize(pages, browser, snapshot_builder = SnapshotBuilder.new, mutex: Mutex.new)
24
24
  @pages = pages
25
25
  @browser = browser
26
26
  @snapshot = snapshot_builder
27
+ @mutex = mutex
27
28
  end
28
29
 
29
30
  def dispatch(req)
@@ -36,18 +37,21 @@ module Browserctl
36
37
  private
37
38
 
38
39
  def cmd_open_page(req)
39
- page = @pages[req[:name]] ||= @browser.create_page
40
+ page = @mutex.synchronize { @pages[req[:name]] } || begin
41
+ new_page = @browser.create_page
42
+ @mutex.synchronize { @pages[req[:name]] ||= new_page }
43
+ end
40
44
  page.go_to(req[:url]) if req[:url]
41
45
  { ok: true, name: req[:name] }
42
46
  end
43
47
 
44
48
  def cmd_close_page(req)
45
- @pages.delete(req[:name])&.close
49
+ @mutex.synchronize { @pages.delete(req[:name]) }&.close
46
50
  { ok: true }
47
51
  end
48
52
 
49
53
  def cmd_list_pages(_req)
50
- { pages: @pages.keys }
54
+ { pages: @mutex.synchronize { @pages.keys } }
51
55
  end
52
56
 
53
57
  def cmd_goto(req)
@@ -126,7 +130,7 @@ module Browserctl
126
130
  end
127
131
 
128
132
  def with_page(name)
129
- page = @pages[name]
133
+ page = @mutex.synchronize { @pages[name] }
130
134
  return { error: "no page named '#{name}'" } unless page
131
135
 
132
136
  yield page
@@ -13,7 +13,7 @@ module Browserctl
13
13
  class Server
14
14
  def initialize(headless: true)
15
15
  prepare_runtime(headless)
16
- @dispatcher = CommandDispatcher.new(@pages, @browser)
16
+ @dispatcher = CommandDispatcher.new(@pages, @browser, mutex: @mutex)
17
17
  end
18
18
 
19
19
  def run
@@ -35,12 +35,12 @@ module Browserctl
35
35
  end
36
36
 
37
37
  def init_browser(headless)
38
- Ferrum::Browser.new(
39
- headless: headless,
40
- timeout: 30,
41
- process_timeout: 30,
42
- browser_options: { "no-sandbox" => nil, "disable-dev-shm-usage" => nil, "disable-gpu" => nil }
43
- )
38
+ Ferrum::Browser.new(headless: headless, **ferrum_options)
39
+ end
40
+
41
+ def ferrum_options
42
+ { timeout: 30, process_timeout: 30,
43
+ browser_options: { "no-sandbox" => nil, "disable-dev-shm-usage" => nil, "disable-gpu" => nil } }
44
44
  end
45
45
 
46
46
  def init_state
@@ -76,19 +76,24 @@ module Browserctl
76
76
  end
77
77
 
78
78
  def handle(socket)
79
- if (line = socket.gets)
80
- @mutex.synchronize { @last_used = Time.now }
81
- socket.puts JSON.generate(process(line))
82
- end
79
+ dispatch(socket, socket.gets)
83
80
  rescue StandardError => e
84
81
  quietly { socket.puts JSON.generate({ error: e.message }) }
85
82
  ensure
86
83
  quietly { socket.close }
87
84
  end
88
85
 
86
+ def dispatch(socket, line)
87
+ return unless line
88
+
89
+ @mutex.synchronize { @last_used = Time.now }
90
+ socket.puts JSON.generate(process(line))
91
+ end
92
+
89
93
  def process(line)
90
94
  req = JSON.parse(line.chomp, symbolize_names: true)
91
- @mutex.synchronize { @dispatcher.dispatch(req) }
95
+ @mutex.synchronize { @last_used = Time.now }
96
+ @dispatcher.dispatch(req)
92
97
  end
93
98
 
94
99
  def teardown(idle, server)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Browserctl
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -80,8 +80,8 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.65'
83
- description: Ferrum-backed browser daemon with a CLI and Ruby workflow DSL, designed
84
- for AI agent use.
83
+ description: Named browser sessions, Ruby workflow DSL, and a token-efficient DOM
84
+ snapshot format. Built on Ferrum (Chrome DevTools Protocol).
85
85
  email:
86
86
  - patrick204nqh@gmail.com
87
87
  executables:
@@ -144,5 +144,6 @@ requirements: []
144
144
  rubygems_version: 3.5.22
145
145
  signing_key:
146
146
  specification_version: 4
147
- summary: Persistent browser automation daemon + CLI for AI agents
147
+ summary: Persistent browser automation daemon and CLI for AI agents and developer
148
+ workflows
148
149
  test_files: []