gitbroker 0.5.0 → 0.6.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/lib/gitbroker/api.rb +10 -4
- data/lib/gitbroker/cli.rb +76 -10
- data/lib/gitbroker/clipboard.rb +26 -0
- data/lib/gitbroker/config.rb +6 -0
- data/lib/gitbroker/daemon.rb +12 -2
- data/lib/gitbroker/login.rb +17 -8
- data/lib/gitbroker/version.rb +1 -1
- data/lib/gitbroker.rb +1 -0
- data/plugin/.claude-plugin/plugin.json +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 946c2fd1fc092b025239f5e0bed964713648ef3c722ae2cd1955d129079ec107
|
|
4
|
+
data.tar.gz: 3cb2803f1de324bc10195b6136b302ededcd3e34a632bdeaba823c4d98639ea4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd93a7d8e7fd151960b28b786ee9409b520f89405552725e3d23dcdde98628afdd952b16cabcd1d841e5c4009215291bd237e93f829a8a7ebd92e339cb38fdb9
|
|
7
|
+
data.tar.gz: 492201664eca213938e466a6e4e5f8ac920a8d726d746700989de4028fd51644275c2a45fb5e630cd04aaea0935cb7032b74ea1756ac5b6f047c7fb796198f03
|
data/lib/gitbroker/api.rb
CHANGED
|
@@ -23,13 +23,21 @@ module Gitbroker
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def post(path, body = {})
|
|
26
|
+
send_request(Net::HTTP::Post, path) { |request| request.body = JSON.generate(body) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get(path) = send_request(Net::HTTP::Get, path)
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def send_request(verb, path)
|
|
26
34
|
uri = URI("#{@server}#{path}")
|
|
27
|
-
request =
|
|
35
|
+
request = verb.new(uri)
|
|
28
36
|
request["Content-Type"] = "application/json"
|
|
29
37
|
request["Accept"] = "application/json"
|
|
30
38
|
request["User-Agent"] = "gitbroker/#{VERSION}"
|
|
31
39
|
request["Authorization"] = "Bearer #{@token}" if @token
|
|
32
|
-
request
|
|
40
|
+
yield request if block_given?
|
|
33
41
|
|
|
34
42
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
|
|
35
43
|
http.request(request)
|
|
@@ -40,8 +48,6 @@ module Gitbroker
|
|
|
40
48
|
parsed
|
|
41
49
|
end
|
|
42
50
|
|
|
43
|
-
private
|
|
44
|
-
|
|
45
51
|
def parse(body)
|
|
46
52
|
return {} if body.to_s.strip.empty?
|
|
47
53
|
|
data/lib/gitbroker/cli.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "openssl"
|
|
4
5
|
require "optparse"
|
|
5
6
|
require "rbconfig"
|
|
6
7
|
require "socket"
|
|
@@ -11,15 +12,18 @@ module Gitbroker
|
|
|
11
12
|
Usage: gitbroker <command> [options]
|
|
12
13
|
|
|
13
14
|
Commands:
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
start [--server URL] [--name NAME] Set this machine up and keep it connected: pairs it (paste the code
|
|
16
|
+
it prints at git.broker/devices), installs the background service and the
|
|
17
|
+
"Start companion" button, then runs your agent when you ask from a PR card.
|
|
18
|
+
Run it again any time; it only does what is still missing.
|
|
16
19
|
launch Start the companion in the background if it is not running
|
|
17
20
|
stop Stop the companion (and its background service, so it stays stopped)
|
|
18
21
|
restart Restart the companion, e.g. after `gem update gitbroker`
|
|
19
22
|
upgrade Install the newest gem, remove old versions, restart the companion
|
|
20
23
|
uninstall Remove the service, the start link, the Claude Code hooks and the gem (keeps your pairing)
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
login [--server URL] [--name NAME] Only pair this machine (`start` does this when needed)
|
|
25
|
+
install-service Only (re)install the background service (launchd / systemd --user) and the
|
|
26
|
+
"Start companion" handler (`start` does this when needed)
|
|
23
27
|
install-skill Install the gitbroker skills into ~/.claude/skills
|
|
24
28
|
status Show pairing, configuration and the repositories found
|
|
25
29
|
clean Remove finished agent worktrees (keeps ones with uncommitted work)
|
|
@@ -88,15 +92,77 @@ module Gitbroker
|
|
|
88
92
|
Login.new(config:, out: @out, name: options[:name] || Socket.gethostname).run
|
|
89
93
|
end
|
|
90
94
|
|
|
95
|
+
# In a terminal: whatever is still missing (pairing, the background service) is set up, then the companion is left
|
|
96
|
+
# running in the background. Without a terminal (the service, `gitbroker launch`) or with --foreground: the daemon
|
|
97
|
+
# itself, in this process.
|
|
91
98
|
def start
|
|
92
|
-
|
|
99
|
+
options = {}
|
|
100
|
+
OptionParser.new do |parser|
|
|
101
|
+
parser.on("--server URL") { options[:server] = _1 }
|
|
102
|
+
parser.on("--name NAME") { options[:name] = _1 }
|
|
103
|
+
parser.on("--foreground") { options[:foreground] = true }
|
|
104
|
+
end.parse!(@argv)
|
|
105
|
+
@config = config.save!("server" => options[:server]) if options[:server]
|
|
106
|
+
|
|
107
|
+
return run_daemon if options[:foreground] || !@input.tty?
|
|
108
|
+
|
|
109
|
+
set_up(options[:name] || Socket.gethostname)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def run_daemon
|
|
113
|
+
raise Error, "This machine is not paired. Run `gitbroker start` in a terminal to pair it." unless config.paired?
|
|
93
114
|
|
|
94
115
|
@out.puts "Git Broker companion #{VERSION} → #{config.server} (agent: #{config.agent}, terminal: #{config.terminal})"
|
|
95
116
|
Daemon.build(config, out: @out).run
|
|
117
|
+
rescue Daemon::Rejected
|
|
118
|
+
# The pairing is dead. Forget it and remove the service, so nothing restarts a daemon that can never connect.
|
|
119
|
+
config.clear_credentials!
|
|
120
|
+
ServiceInstaller.new(home: @home, env: @env).uninstall!
|
|
121
|
+
raise
|
|
96
122
|
end
|
|
97
123
|
|
|
124
|
+
def set_up(name)
|
|
125
|
+
forget_dead_pairing if config.paired? && pairing_rejected?
|
|
126
|
+
Login.new(config:, out: @out, name:).run unless config.paired?
|
|
127
|
+
install_background_service
|
|
128
|
+
@out.puts "Git Broker is running in the background. Open #{config.server} to see it connected."
|
|
129
|
+
rescue Error => e
|
|
130
|
+
raise unless service_unavailable?(e)
|
|
131
|
+
|
|
132
|
+
@out.puts "No background service is available here (#{e.message}). Keeping the companion in this window; run " \
|
|
133
|
+
"`gitbroker start` again after a restart. Press Ctrl-C to stop it."
|
|
134
|
+
run_daemon
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# True only when the server answered "unauthorized". An unreachable server proves nothing, so the pairing is kept.
|
|
138
|
+
def pairing_rejected?
|
|
139
|
+
Api.new(server: config.server, token: config.device_token).get("/device/ping")
|
|
140
|
+
false
|
|
141
|
+
rescue Api::HttpError => e
|
|
142
|
+
e.status == 401
|
|
143
|
+
rescue SystemCallError, IOError, SocketError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def forget_dead_pairing
|
|
148
|
+
@out.puts "Git Broker no longer knows this machine, so it needs pairing again."
|
|
149
|
+
config.clear_credentials!
|
|
150
|
+
ServiceInstaller.new(home: @home, env: @env).uninstall!
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Installs what is missing and starts it. A service that is already installed is only started (installing again
|
|
154
|
+
# would restart a daemon that may be running a task).
|
|
155
|
+
def install_background_service
|
|
156
|
+
service = ServiceInstaller.new(home: @home, env: @env)
|
|
157
|
+
return lifecycle.launch if service.installed?
|
|
158
|
+
|
|
159
|
+
install_service
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def service_unavailable?(error) = error.message.include?("not found on PATH") || error.message.include?("systemctl")
|
|
163
|
+
|
|
98
164
|
def install_service
|
|
99
|
-
raise Error, "This machine is not paired. Run `gitbroker
|
|
165
|
+
raise Error, "This machine is not paired. Run `gitbroker start` first." unless config.paired?
|
|
100
166
|
|
|
101
167
|
path = ServiceInstaller.new(home: @home, env: @env).install!
|
|
102
168
|
@out.puts "Installed and started #{path}."
|
|
@@ -112,7 +178,7 @@ module Gitbroker
|
|
|
112
178
|
# The target of the website's gitbroker://start link and of `gitbroker launch` by hand: start the background
|
|
113
179
|
# service, or the daemon itself when none is installed. Safe to run when it is already running.
|
|
114
180
|
def launch
|
|
115
|
-
raise Error, "This machine is not paired. Run `gitbroker
|
|
181
|
+
raise Error, "This machine is not paired. Run `gitbroker start` first." unless config.paired?
|
|
116
182
|
lifecycle.launch
|
|
117
183
|
end
|
|
118
184
|
|
|
@@ -125,7 +191,7 @@ module Gitbroker
|
|
|
125
191
|
end
|
|
126
192
|
|
|
127
193
|
def restart
|
|
128
|
-
raise Error, "This machine is not paired. Run `gitbroker
|
|
194
|
+
raise Error, "This machine is not paired. Run `gitbroker start` first." unless config.paired?
|
|
129
195
|
|
|
130
196
|
lifecycle.restart
|
|
131
197
|
@out.puts "Restarting the companion."
|
|
@@ -137,7 +203,7 @@ module Gitbroker
|
|
|
137
203
|
# the companion comes back on the new version. An unpaired machine has no companion to restart.
|
|
138
204
|
def upgrade
|
|
139
205
|
maintenance.upgrade!
|
|
140
|
-
return @out.puts("Upgraded. Run `gitbroker
|
|
206
|
+
return @out.puts("Upgraded. Run `gitbroker start` to pair this machine.") unless config.paired?
|
|
141
207
|
|
|
142
208
|
lifecycle.restart
|
|
143
209
|
@out.puts "Upgraded. Restarting the companion."
|
|
@@ -159,7 +225,7 @@ module Gitbroker
|
|
|
159
225
|
def status
|
|
160
226
|
@out.puts "Git Broker companion #{VERSION}"
|
|
161
227
|
@out.puts "Server: #{config.server}"
|
|
162
|
-
@out.puts "Paired: #{config.paired? ? "yes (device #{config.device_id})" : "no (run `gitbroker
|
|
228
|
+
@out.puts "Paired: #{config.paired? ? "yes (device #{config.device_id})" : "no (run `gitbroker start`)"}"
|
|
163
229
|
@out.puts "Agent: #{config.agent}"
|
|
164
230
|
@out.puts "Terminal: #{config.terminal}"
|
|
165
231
|
@out.puts "Hooks: #{hooks_installer.installed? ? "installed" : "not installed (gitbroker install-hooks)"}"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Gitbroker
|
|
6
|
+
# Puts a short string on the system clipboard so the user can paste it into the browser instead of typing it
|
|
7
|
+
# (the pairing code). Best effort: false when no clipboard tool exists or it failed, never an error.
|
|
8
|
+
module Clipboard
|
|
9
|
+
COMMANDS = {
|
|
10
|
+
"macos" => [ [ "pbcopy" ] ],
|
|
11
|
+
"linux" => [ [ "wl-copy" ], [ "xclip", "-selection", "clipboard" ], [ "xsel", "--clipboard", "--input" ], [ "clip.exe" ] ]
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.copy(text, platform: Gitbroker.platform, commands: COMMANDS)
|
|
15
|
+
Array(commands[platform]).any? { |argv| pipe(argv, text) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.pipe(argv, text)
|
|
19
|
+
_out, status = Open3.capture2e(*argv, stdin_data: text)
|
|
20
|
+
status.success?
|
|
21
|
+
rescue SystemCallError
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
private_class_method :pipe
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/gitbroker/config.rb
CHANGED
|
@@ -67,6 +67,12 @@ module Gitbroker
|
|
|
67
67
|
@credentials = nil
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Forgets the pairing (the server no longer knows this device); the next `gitbroker start` pairs again.
|
|
71
|
+
def clear_credentials!
|
|
72
|
+
FileUtils.rm_f(credentials_path)
|
|
73
|
+
@credentials = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
70
76
|
private
|
|
71
77
|
|
|
72
78
|
def credentials
|
data/lib/gitbroker/daemon.rb
CHANGED
|
@@ -8,15 +8,20 @@ module Gitbroker
|
|
|
8
8
|
# `gitbroker start`: keep the device connected, report checkouts, claim and run tasks, and relay the status of
|
|
9
9
|
# agents the user started themselves (relay: Task 31, process scanner: Task 32).
|
|
10
10
|
class Daemon
|
|
11
|
+
class Rejected < Error; end
|
|
12
|
+
|
|
11
13
|
RESCAN_EVERY = 300
|
|
12
14
|
MAX_BACKOFF = 60
|
|
13
15
|
BATCH = 50
|
|
14
16
|
# Spec §5.2 / main §18.4: nothing else about an observed agent ever leaves the machine.
|
|
17
|
+
# The server answered "unauthorized" this many times in a row (a revoked device, or a server that lost its devices):
|
|
18
|
+
# the pairing is dead, so the daemon stops asking instead of retrying for ever. One or two could be a blip.
|
|
19
|
+
REJECTIONS_BEFORE_GIVING_UP = 3
|
|
15
20
|
OBSERVATION_FIELDS = %w[event agent external_session_id repository branch head_sha occurred_at].freeze
|
|
16
21
|
|
|
17
22
|
def self.build(config, out: $stdout, relay: LocalRelay.new(path: File.join(config.dir, "daemon.sock")),
|
|
18
23
|
process_scanner: build_process_scanner(config))
|
|
19
|
-
raise Error, "This machine is not paired. Run `gitbroker
|
|
24
|
+
raise Error, "This machine is not paired. Run `gitbroker start` in a terminal to pair it." unless config.paired?
|
|
20
25
|
|
|
21
26
|
new(scanner: RepoScanner.new(roots: config.roots, depth: config.scan_depth, exclude: [ config.worktree_root ]),
|
|
22
27
|
client_factory: -> { CableClient.new(url: config.cable_url, token: config.device_token, origin: config.server) },
|
|
@@ -58,6 +63,7 @@ module Gitbroker
|
|
|
58
63
|
|
|
59
64
|
def run
|
|
60
65
|
@backoff = 1
|
|
66
|
+
@rejections = 0
|
|
61
67
|
@relay&.start
|
|
62
68
|
@lifecycle&.write_pid
|
|
63
69
|
until @stopping
|
|
@@ -66,7 +72,10 @@ module Gitbroker
|
|
|
66
72
|
wire(@client)
|
|
67
73
|
@client.run
|
|
68
74
|
rescue CableClient::Unauthorized
|
|
69
|
-
|
|
75
|
+
@rejections += 1
|
|
76
|
+
raise Rejected, "Git Broker no longer knows this device (revoked?). Run `gitbroker start` to pair again." if @rejections >= REJECTIONS_BEFORE_GIVING_UP
|
|
77
|
+
|
|
78
|
+
@out.puts "Git Broker rejected this device (#{@rejections} of #{REJECTIONS_BEFORE_GIVING_UP}); trying again."
|
|
70
79
|
rescue Error, SystemCallError, IOError, SocketError, OpenSSL::SSL::SSLError => e
|
|
71
80
|
@out.puts "Connection problem: #{e.message}"
|
|
72
81
|
end
|
|
@@ -96,6 +105,7 @@ module Gitbroker
|
|
|
96
105
|
def wire(client)
|
|
97
106
|
client.on_subscribed do
|
|
98
107
|
@backoff = 1
|
|
108
|
+
@rejections = 0
|
|
99
109
|
rescan(client)
|
|
100
110
|
client.perform("hello", "version" => VERSION, "platform" => Gitbroker.platform,
|
|
101
111
|
"service" => @lifecycle ? @lifecycle.service_installed? : false)
|
data/lib/gitbroker/login.rb
CHANGED
|
@@ -10,7 +10,8 @@ module Gitbroker
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def initialize(config:, api: Api.new(server: config.server), out: $stdout, name: Socket.gethostname,
|
|
13
|
-
sleeper: ->(seconds) { sleep(seconds) }, opener: ->(url) { Login.open_browser(url) }, clock: -> { Time.now }
|
|
13
|
+
sleeper: ->(seconds) { sleep(seconds) }, opener: ->(url) { Login.open_browser(url) }, clock: -> { Time.now },
|
|
14
|
+
copier: ->(text) { Clipboard.copy(text) })
|
|
14
15
|
@config = config
|
|
15
16
|
@api = api
|
|
16
17
|
@out = out
|
|
@@ -18,14 +19,22 @@ module Gitbroker
|
|
|
18
19
|
@sleeper = sleeper
|
|
19
20
|
@opener = opener
|
|
20
21
|
@clock = clock
|
|
22
|
+
@copier = copier
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def run
|
|
24
26
|
auth = @api.post("/device_authorizations", { client_name: @name, platform: Gitbroker.platform })
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@
|
|
28
|
-
|
|
27
|
+
code = auth.fetch("user_code")
|
|
28
|
+
uri = auth.fetch("verification_uri")
|
|
29
|
+
copied = @copier.call(code)
|
|
30
|
+
# No pre-filled link: nothing in a URL may select a code, the user pastes (or types) it on the plain page.
|
|
31
|
+
@out.puts "Pairing #{@name} with Git Broker."
|
|
32
|
+
@out.puts
|
|
33
|
+
@out.puts " Your code: #{code}#{" (copied to your clipboard)" if copied}"
|
|
34
|
+
@out.puts
|
|
35
|
+
@out.puts "Paste it at #{uri}, press Pair, then check the device details and press Approve."
|
|
36
|
+
@out.puts "Only enter a code you see here, in your own terminal."
|
|
37
|
+
@opener.call(uri)
|
|
29
38
|
poll(auth)
|
|
30
39
|
end
|
|
31
40
|
|
|
@@ -36,7 +45,7 @@ module Gitbroker
|
|
|
36
45
|
deadline = @clock.call + auth.fetch("expires_in", 600).to_i
|
|
37
46
|
|
|
38
47
|
loop do
|
|
39
|
-
raise Error, "The pairing code expired. Run `gitbroker
|
|
48
|
+
raise Error, "The pairing code expired. Run `gitbroker start` again." if @clock.call > deadline
|
|
40
49
|
|
|
41
50
|
@sleeper.call(interval)
|
|
42
51
|
begin
|
|
@@ -46,14 +55,14 @@ module Gitbroker
|
|
|
46
55
|
case e.body["error"]
|
|
47
56
|
when "authorization_pending" then next
|
|
48
57
|
when "slow_down" then interval += 5
|
|
49
|
-
when "expired_token" then raise Error, "The pairing code expired. Run `gitbroker
|
|
58
|
+
when "expired_token" then raise Error, "The pairing code expired. Run `gitbroker start` again."
|
|
50
59
|
else raise Error, "Pairing failed: #{e.message}"
|
|
51
60
|
end
|
|
52
61
|
next
|
|
53
62
|
end
|
|
54
63
|
|
|
55
64
|
@config.save_credentials!(device_token: token.fetch("access_token"), device_id: token.fetch("device_id"))
|
|
56
|
-
@out.puts "Paired.
|
|
65
|
+
@out.puts "Paired."
|
|
57
66
|
return token["device_id"]
|
|
58
67
|
end
|
|
59
68
|
end
|
data/lib/gitbroker/version.rb
CHANGED
data/lib/gitbroker.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Gitbroker
|
|
|
16
16
|
autoload :Agents, File.expand_path("gitbroker/agents", __dir__)
|
|
17
17
|
autoload :Api, File.expand_path("gitbroker/api", __dir__)
|
|
18
18
|
autoload :CableClient, File.expand_path("gitbroker/cable_client", __dir__)
|
|
19
|
+
autoload :Clipboard, File.expand_path("gitbroker/clipboard", __dir__)
|
|
19
20
|
autoload :CLI, File.expand_path("gitbroker/cli", __dir__)
|
|
20
21
|
autoload :Config, File.expand_path("gitbroker/config", __dir__)
|
|
21
22
|
autoload :Daemon, File.expand_path("gitbroker/daemon", __dir__)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitbroker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yuri Sidorov (@newstler)
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- lib/gitbroker/api.rb
|
|
42
42
|
- lib/gitbroker/cable_client.rb
|
|
43
43
|
- lib/gitbroker/cli.rb
|
|
44
|
+
- lib/gitbroker/clipboard.rb
|
|
44
45
|
- lib/gitbroker/config.rb
|
|
45
46
|
- lib/gitbroker/daemon.rb
|
|
46
47
|
- lib/gitbroker/explain_verifier.rb
|