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 +4 -4
- data/CHANGELOG.md +31 -16
- data/lib/browserctl/server/command_dispatcher.rb +9 -5
- data/lib/browserctl/server.rb +17 -12
- data/lib/browserctl/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2976bab368591b4cb06cd3a34d64c1078bd8588590d70c67ba3d78ecd721e572
|
|
4
|
+
data.tar.gz: 0fa9c86ec532df2c6763780bfb6c2ac47380ff1df400ae2639fe914fc66e0d04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
## [
|
|
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
|
-
###
|
|
18
|
-
|
|
19
|
-
|
|
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] -
|
|
16
|
+
## [0.1.0] - 2026-04-19
|
|
22
17
|
|
|
23
18
|
### Added
|
|
24
|
-
-
|
|
25
|
-
- CLI (`browserctl`) with commands: open
|
|
26
|
-
- Ruby workflow DSL with
|
|
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]]
|
|
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
|
data/lib/browserctl/server.rb
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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 { @
|
|
95
|
+
@mutex.synchronize { @last_used = Time.now }
|
|
96
|
+
@dispatcher.dispatch(req)
|
|
92
97
|
end
|
|
93
98
|
|
|
94
99
|
def teardown(idle, server)
|
data/lib/browserctl/version.rb
CHANGED
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.
|
|
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:
|
|
84
|
-
|
|
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
|
|
147
|
+
summary: Persistent browser automation daemon and CLI for AI agents and developer
|
|
148
|
+
workflows
|
|
148
149
|
test_files: []
|