brainiac 0.0.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 +7 -0
- checksums.yaml.gz.sig +2 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +126 -0
- data/README.md +1166 -0
- data/Rakefile +12 -0
- data/bin/brainiac +1521 -0
- data/brainiac.gemspec +30 -0
- data/certs/stowzilla.pem +26 -0
- data/docs/waybar-config.md +96 -0
- data/lib/brainiac/agents.rb +203 -0
- data/lib/brainiac/brain.rb +197 -0
- data/lib/brainiac/card_index.rb +389 -0
- data/lib/brainiac/config.rb +263 -0
- data/lib/brainiac/cron.rb +629 -0
- data/lib/brainiac/deployments.rb +258 -0
- data/lib/brainiac/handlers/discord.rb +1643 -0
- data/lib/brainiac/handlers/fizzy.rb +1249 -0
- data/lib/brainiac/handlers/github.rb +598 -0
- data/lib/brainiac/handlers/zoho.rb +487 -0
- data/lib/brainiac/helpers.rb +760 -0
- data/lib/brainiac/planning.rb +237 -0
- data/lib/brainiac/prompts.rb +620 -0
- data/lib/brainiac/sessions.rb +282 -0
- data/lib/brainiac/skills.rb +276 -0
- data/lib/brainiac/users.rb +76 -0
- data/lib/brainiac/version.rb +6 -0
- data/lib/brainiac/zoho_mail_api.rb +109 -0
- data/lib/brainiac.rb +10 -0
- data/lib/user_registry.rb +159 -0
- data/monitor/daemon.rb +99 -0
- data/monitor/deploy-env-macos.rb +131 -0
- data/monitor/menubar.rb +295 -0
- data/monitor/open-action.sh +15 -0
- data/monitor/setup-menubar.rb +78 -0
- data/monitor/setup-waybar-deploy-envs.rb +121 -0
- data/monitor/setup-waybar-deployments.rb +96 -0
- data/monitor/setup-waybar-module.rb +113 -0
- data/monitor/setup-xbar-plugin.rb +35 -0
- data/monitor/view-logs-macos.rb +210 -0
- data/monitor/view-logs-rofi.rb +194 -0
- data/monitor/view-logs.rb +119 -0
- data/monitor/waybar-config-updater.rb +56 -0
- data/monitor/waybar-deploy-env.rb +206 -0
- data/monitor/waybar-deployments.rb +239 -0
- data/monitor/waybar.rb +146 -0
- data/monitor/xbar.3s.rb +179 -0
- data/receiver.rb +956 -0
- data/templates/agents.json.example +10 -0
- data/templates/discord.json.example +17 -0
- data/templates/fizzy.json.example +24 -0
- data/templates/github.json.example +4 -0
- data/templates/testflight.json.example +8 -0
- data/templates/users.json.example +121 -0
- data/templates/zoho.json.example +27 -0
- data/views/dashboard.erb +437 -0
- data.tar.gz.sig +0 -0
- metadata +235 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Brainiac Waybar Config Updater
|
|
5
|
+
# Dynamically updates waybar config with per-agent modules
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
WAYBAR_CONFIG = File.expand_path("~/.config/waybar/config.jsonc")
|
|
10
|
+
WAYBAR_SCRIPT = File.expand_path("~/Code/brainiac/monitor/waybar.rb")
|
|
11
|
+
|
|
12
|
+
def load_config
|
|
13
|
+
content = File.read(WAYBAR_CONFIG)
|
|
14
|
+
# Strip comments for JSON parsing
|
|
15
|
+
json_content = content.lines.reject { |line| line.strip.start_with?("//") }.join
|
|
16
|
+
JSON.parse(json_content)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def save_config(config)
|
|
20
|
+
File.write(WAYBAR_CONFIG, JSON.pretty_generate(config))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def brainiac_modules
|
|
24
|
+
output = `#{WAYBAR_SCRIPT} --config`
|
|
25
|
+
JSON.parse(output)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Load current config
|
|
29
|
+
config = load_config
|
|
30
|
+
|
|
31
|
+
# Get dynamic Brainiac modules
|
|
32
|
+
brainiac_data = brainiac_modules
|
|
33
|
+
modules = brainiac_data["modules"]
|
|
34
|
+
module_configs = brainiac_data["config"]
|
|
35
|
+
|
|
36
|
+
# Remove old brainiac modules and groups from modules-right
|
|
37
|
+
config["modules-right"].reject! { |m| m.to_s.start_with?("custom/brainiac") || m.to_s == "group/brainiac-agents" }
|
|
38
|
+
|
|
39
|
+
# Insert new modules at the beginning of modules-right
|
|
40
|
+
config["modules-right"] = modules + config["modules-right"]
|
|
41
|
+
|
|
42
|
+
# Remove old brainiac module configs and groups
|
|
43
|
+
config.each_key do |key|
|
|
44
|
+
config.delete(key) if key.to_s.start_with?("custom/brainiac") || key.to_s == "group/brainiac-agents"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Add new module configs
|
|
48
|
+
module_configs.each do |name, cfg|
|
|
49
|
+
config[name] = cfg
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Save updated config
|
|
53
|
+
save_config(config)
|
|
54
|
+
|
|
55
|
+
# Reload waybar
|
|
56
|
+
system("killall", "-SIGUSR2", "waybar")
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Brainiac Waybar Per-Environment Deploy Module
|
|
5
|
+
# Usage: waybar-deploy-env.rb <env_key>
|
|
6
|
+
# waybar-deploy-env.rb <env_key> --click
|
|
7
|
+
# waybar-deploy-env.rb <env_key> --deploy
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
require "net/http"
|
|
11
|
+
require "shellwords"
|
|
12
|
+
require "uri"
|
|
13
|
+
require "time"
|
|
14
|
+
|
|
15
|
+
SERVER_URL = "http://localhost:4567"
|
|
16
|
+
RECENT_WINDOW = 30 * 60
|
|
17
|
+
|
|
18
|
+
env_key = ARGV.find { |a| !a.start_with?("--") }
|
|
19
|
+
unless env_key
|
|
20
|
+
puts({ text: "?", tooltip: "No env specified", class: "error" }.to_json)
|
|
21
|
+
exit
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch_deployments
|
|
25
|
+
uri = URI("#{SERVER_URL}/api/deployments")
|
|
26
|
+
response = Net::HTTP.get_response(uri)
|
|
27
|
+
JSON.parse(response.body)["deployments"] || []
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def time_ago(iso_time)
|
|
33
|
+
return nil unless iso_time
|
|
34
|
+
|
|
35
|
+
seconds = (Time.now - Time.parse(iso_time)).to_i
|
|
36
|
+
return "#{seconds}s ago" if seconds < 60
|
|
37
|
+
|
|
38
|
+
minutes = seconds / 60
|
|
39
|
+
return "#{minutes}m ago" if minutes < 60
|
|
40
|
+
|
|
41
|
+
hours = minutes / 60
|
|
42
|
+
"#{hours}h ago"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resize_deploy_terminal
|
|
46
|
+
# Shrink the deploy terminal to ~15% width after it tiles in at 50%
|
|
47
|
+
# Calculates resize delta from monitor width dynamically
|
|
48
|
+
script = "sleep 0.5 && " \
|
|
49
|
+
'width=$(hyprctl monitors -j | ruby -rjson -e "puts JSON.parse(STDIN.read)[0][%q(width)]") && ' \
|
|
50
|
+
"delta=$(( (width / 2) - (width * 15 / 100) )) && " \
|
|
51
|
+
'hyprctl --batch "dispatch focuswindow class:brainiac-deploy; dispatch resizeactive -${delta} 0"'
|
|
52
|
+
spawn("bash", "-c", script, %i[out err] => "/dev/null")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def handle_click(env_key, deployment)
|
|
56
|
+
return unless deployment
|
|
57
|
+
|
|
58
|
+
if deployment["last_deploy_status"] == "failed" && deployment["last_deploy_log"]
|
|
59
|
+
log = deployment["last_deploy_log"]
|
|
60
|
+
if File.exist?(log.to_s)
|
|
61
|
+
spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c",
|
|
62
|
+
"echo '=== Deploy failure: #{deployment["label"] || env_key} ===' && echo && cat #{Shellwords.escape(log)} && echo && echo 'Press Enter to close...' && read",
|
|
63
|
+
%i[out err] => "/dev/null")
|
|
64
|
+
resize_deploy_terminal
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
url = deployment["url"]
|
|
70
|
+
spawn("xdg-open", url, %i[out err] => "/dev/null") if url
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_deploy(env_key, deployment)
|
|
74
|
+
return unless deployment
|
|
75
|
+
|
|
76
|
+
prefill = deployment["status"] == "occupied" && deployment["card_number"] ? deployment["card_number"].to_s : ""
|
|
77
|
+
card_number = `timeout 60 zenity --entry --title="Deploy to #{env_key}" --text="Fizzy card number:"#{unless prefill.empty?
|
|
78
|
+
" --entry-text=#{Shellwords.escape(prefill)}"
|
|
79
|
+
end} 2>/dev/null`.strip
|
|
80
|
+
return if card_number.empty?
|
|
81
|
+
|
|
82
|
+
matches = Dir.glob(File.expand_path("~/Code/*fizzy-#{card_number}-*/"))
|
|
83
|
+
worktree = matches.find { |d| File.directory?(d) }
|
|
84
|
+
unless worktree
|
|
85
|
+
`timeout 10 zenity --error --text="No worktree found for card ##{card_number}" 2>/dev/null`
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Resolve AWS_PROFILE from deployments config
|
|
90
|
+
aws_profile = nil
|
|
91
|
+
config_file = File.expand_path("~/.brainiac/deployments.json")
|
|
92
|
+
if File.exist?(config_file)
|
|
93
|
+
cfg = begin
|
|
94
|
+
JSON.parse(File.read(config_file))
|
|
95
|
+
rescue StandardError
|
|
96
|
+
{}
|
|
97
|
+
end
|
|
98
|
+
aws_profile = cfg.dig("environments", env_key, "aws_profile")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
deploy_script = <<~BASH
|
|
102
|
+
cd #{Shellwords.escape(worktree)}
|
|
103
|
+
#{"export AWS_PROFILE=#{Shellwords.escape(aws_profile)}" if aws_profile}
|
|
104
|
+
echo "🚀 #{env_key} deploy in progress..."
|
|
105
|
+
echo
|
|
106
|
+
logfile=$(mktemp)
|
|
107
|
+
./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1 | tee "$logfile"
|
|
108
|
+
status=${PIPESTATUS[0]}
|
|
109
|
+
if [ $status -ne 0 ] && grep -q "checksums previously recorded in the dependency lock file" "$logfile"; then
|
|
110
|
+
echo
|
|
111
|
+
echo "⚠️ Terraform lock file mismatch — removing lock and running init -upgrade..."
|
|
112
|
+
echo
|
|
113
|
+
rm -f infrastructure/#{Shellwords.escape(env_key)}/.terraform.lock.hcl
|
|
114
|
+
(cd infrastructure/#{Shellwords.escape(env_key)} && terraform init -upgrade)
|
|
115
|
+
echo
|
|
116
|
+
echo "🔄 Retrying deploy..."
|
|
117
|
+
echo
|
|
118
|
+
./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1
|
|
119
|
+
status=$?
|
|
120
|
+
fi
|
|
121
|
+
rm -f "$logfile"
|
|
122
|
+
echo
|
|
123
|
+
if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
|
|
124
|
+
echo "Press Enter to close..."
|
|
125
|
+
read
|
|
126
|
+
BASH
|
|
127
|
+
|
|
128
|
+
# Mark deploying via API so waybar turns orange immediately
|
|
129
|
+
begin
|
|
130
|
+
uri = URI("#{SERVER_URL}/api/deployments/#{env_key}/deploying")
|
|
131
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
132
|
+
req.body = { worktree: worktree }.to_json
|
|
133
|
+
Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
134
|
+
rescue StandardError
|
|
135
|
+
# Non-fatal — deploy proceeds even if server is unreachable
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c", deploy_script, %i[out err] => "/dev/null")
|
|
139
|
+
resize_deploy_terminal
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def generate_output(env_key)
|
|
143
|
+
deployments = fetch_deployments
|
|
144
|
+
unless deployments
|
|
145
|
+
puts({ text: "", tooltip: "#{env_key}: server unreachable", class: "error" }.to_json)
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
d = deployments.find { |dep| dep["env"] == env_key }
|
|
150
|
+
unless d
|
|
151
|
+
puts({ text: "", tooltip: "#{env_key}: not configured", class: "error" }.to_json)
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
label = d["label"] || env_key
|
|
156
|
+
|
|
157
|
+
if d["status"] == "occupied"
|
|
158
|
+
deploy_time = d["last_deploy_at"] || d["deployed_at"]
|
|
159
|
+
recent = deploy_time && (Time.now - Time.parse(deploy_time)) < RECENT_WINDOW
|
|
160
|
+
status = d["last_deploy_status"]
|
|
161
|
+
|
|
162
|
+
if status == "deploying"
|
|
163
|
+
dot = '<span color="#ffaa00">●</span>'
|
|
164
|
+
css_class = "deploy-deploying"
|
|
165
|
+
elsif status == "failed"
|
|
166
|
+
dot = '<span color="#ff4444">●</span>'
|
|
167
|
+
css_class = "deploy-failed"
|
|
168
|
+
elsif recent && status == "success"
|
|
169
|
+
dot = '<span color="#4488ff">●</span>'
|
|
170
|
+
css_class = "deploy-recent"
|
|
171
|
+
else
|
|
172
|
+
dot = '<span color="#ff4444">●</span>'
|
|
173
|
+
css_class = "deploy-occupied"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
card = d["card_number"] ? "##{d["card_number"]}" : d["branch"] || "unknown"
|
|
177
|
+
branch = d["branch"] ? " — #{d["branch"]}" : ""
|
|
178
|
+
ago = time_ago(d["deployed_at"])
|
|
179
|
+
status_icon = case status
|
|
180
|
+
when "deploying" then "🚀"
|
|
181
|
+
when "failed" then "💥"
|
|
182
|
+
when "success" then recent ? "🚀✅" : "🔴"
|
|
183
|
+
else "🔴"
|
|
184
|
+
end
|
|
185
|
+
tooltip = "#{status_icon} #{label}: #{card}#{branch}#{" (#{ago})" if ago}\nClick: open URL | Right-click: deploy"
|
|
186
|
+
else
|
|
187
|
+
dot = '<span color="#44ff44">●</span>'
|
|
188
|
+
css_class = "deploy-available"
|
|
189
|
+
ago = time_ago(d["cleared_at"])
|
|
190
|
+
last = d["last_card"] ? " (was ##{d["last_card"]})" : ""
|
|
191
|
+
tooltip = "🟢 #{label}: Available#{" #{ago}" if ago}#{last}\nRight-click: deploy"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
puts({ text: dot, tooltip: tooltip, class: css_class }.to_json)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
deployments = fetch_deployments
|
|
198
|
+
deployment = deployments&.find { |d| d["env"] == env_key }
|
|
199
|
+
|
|
200
|
+
if ARGV.include?("--click")
|
|
201
|
+
handle_click(env_key, deployment)
|
|
202
|
+
elsif ARGV.include?("--deploy")
|
|
203
|
+
handle_deploy(env_key, deployment)
|
|
204
|
+
else
|
|
205
|
+
generate_output(env_key)
|
|
206
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Brainiac Waybar Deployments Module
|
|
5
|
+
# Polls /api/deployments and outputs JSON for waybar
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
require "net/http"
|
|
9
|
+
require "shellwords"
|
|
10
|
+
require "uri"
|
|
11
|
+
require "time"
|
|
12
|
+
|
|
13
|
+
SERVER_URL = "http://localhost:4567"
|
|
14
|
+
|
|
15
|
+
def fetch_deployments
|
|
16
|
+
uri = URI("#{SERVER_URL}/api/deployments")
|
|
17
|
+
response = Net::HTTP.get_response(uri)
|
|
18
|
+
JSON.parse(response.body)["deployments"] || []
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def time_ago(iso_time)
|
|
24
|
+
return nil unless iso_time
|
|
25
|
+
|
|
26
|
+
seconds = (Time.now - Time.parse(iso_time)).to_i
|
|
27
|
+
return "#{seconds}s ago" if seconds < 60
|
|
28
|
+
|
|
29
|
+
minutes = seconds / 60
|
|
30
|
+
return "#{minutes}m ago" if minutes < 60
|
|
31
|
+
|
|
32
|
+
hours = minutes / 60
|
|
33
|
+
return "#{hours}h ago" if hours < 24
|
|
34
|
+
|
|
35
|
+
"#{hours / 24}d ago"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_output
|
|
39
|
+
deployments = fetch_deployments
|
|
40
|
+
unless deployments
|
|
41
|
+
puts({ text: "", tooltip: "Deploy tracker: server unreachable", class: "error" }.to_json)
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if deployments.empty?
|
|
46
|
+
puts({ text: "", tooltip: "No environments configured", class: "empty" }.to_json)
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
recent_window = 30 * 60 # 30 minutes
|
|
51
|
+
|
|
52
|
+
dots = deployments.map do |d|
|
|
53
|
+
if d["status"] == "occupied"
|
|
54
|
+
deploy_time = d["last_deploy_at"] || d["deployed_at"]
|
|
55
|
+
recent = deploy_time && (Time.now - Time.parse(deploy_time)) < recent_window
|
|
56
|
+
status = d["last_deploy_status"]
|
|
57
|
+
|
|
58
|
+
if status == "failed"
|
|
59
|
+
'<span color="#ff4444" background="#440000">●</span>'
|
|
60
|
+
elsif recent && status == "success"
|
|
61
|
+
'<span color="#4488ff">●</span>'
|
|
62
|
+
else
|
|
63
|
+
'<span color="#ff4444">●</span>'
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
'<span color="#44ff44">●</span>'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
text = dots.join(" ")
|
|
70
|
+
|
|
71
|
+
# Determine CSS class based on deploy states
|
|
72
|
+
has_recent_success = deployments.any? do |d|
|
|
73
|
+
t = d["last_deploy_at"] || d["deployed_at"]
|
|
74
|
+
d["last_deploy_status"] == "success" && t && (Time.now - Time.parse(t)) < recent_window
|
|
75
|
+
end
|
|
76
|
+
has_failure = deployments.any? { |d| d["last_deploy_status"] == "failed" }
|
|
77
|
+
|
|
78
|
+
css_class = if has_failure
|
|
79
|
+
"deploy-failed"
|
|
80
|
+
elsif has_recent_success
|
|
81
|
+
"deploy-recent"
|
|
82
|
+
else
|
|
83
|
+
"deployments"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
tooltip_lines = deployments.map do |d|
|
|
87
|
+
label = d["label"] || d["env"]
|
|
88
|
+
if d["status"] == "occupied"
|
|
89
|
+
card = d["card_number"] ? "##{d["card_number"]}" : d["branch"] || "unknown"
|
|
90
|
+
branch = d["branch"] ? " — #{d["branch"]}" : ""
|
|
91
|
+
ago = time_ago(d["deployed_at"])
|
|
92
|
+
status_icon = case d["last_deploy_status"]
|
|
93
|
+
when "failed" then "💥"
|
|
94
|
+
when "success"
|
|
95
|
+
t = d["last_deploy_at"] || d["deployed_at"]
|
|
96
|
+
t && (Time.now - Time.parse(t)) < recent_window ? "🚀✅" : "🔴"
|
|
97
|
+
else "🔴"
|
|
98
|
+
end
|
|
99
|
+
"#{status_icon} #{label}: #{card}#{branch}#{" (#{ago})" if ago}"
|
|
100
|
+
else
|
|
101
|
+
ago = time_ago(d["cleared_at"])
|
|
102
|
+
last = d["last_card"] ? " (was ##{d["last_card"]})" : ""
|
|
103
|
+
"🟢 #{label}: Available#{" #{ago}" if ago}#{last}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
puts({ text: text, tooltip: tooltip_lines.join("\n"), class: css_class }.to_json)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def resize_deploy_terminal
|
|
111
|
+
script = "sleep 0.5 && " \
|
|
112
|
+
'width=$(hyprctl monitors -j | ruby -rjson -e "puts JSON.parse(STDIN.read)[0][%q(width)]") && ' \
|
|
113
|
+
"delta=$(( (width / 2) - (width * 15 / 100) )) && " \
|
|
114
|
+
'hyprctl --batch "dispatch focuswindow class:brainiac-deploy; dispatch resizeactive -${delta} 0"'
|
|
115
|
+
spawn("bash", "-c", script, %i[out err] => "/dev/null")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def handle_click
|
|
119
|
+
deployments = fetch_deployments
|
|
120
|
+
return unless deployments&.any?
|
|
121
|
+
|
|
122
|
+
# If any environment has a failed deploy, show the log
|
|
123
|
+
failed = deployments.find { |d| d["last_deploy_status"] == "failed" && d["last_deploy_log"] }
|
|
124
|
+
if failed && File.exist?(failed["last_deploy_log"].to_s)
|
|
125
|
+
spawn("alacritty", "-e", "bash", "-c",
|
|
126
|
+
"echo '=== Deploy failure: #{failed["label"] || failed["env"]} ===' && echo && cat #{Shellwords.escape(failed["last_deploy_log"])} && echo && echo 'Press Enter to close...' && read",
|
|
127
|
+
%i[out err] => "/dev/null")
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Otherwise open environment URLs
|
|
132
|
+
options = deployments.filter_map do |d|
|
|
133
|
+
url = d["url"]
|
|
134
|
+
next unless url
|
|
135
|
+
|
|
136
|
+
label = d["label"] || d["env"]
|
|
137
|
+
status = d["status"] == "occupied" ? "🔴" : "🟢"
|
|
138
|
+
card = d["card_number"] ? " ##{d["card_number"]}" : ""
|
|
139
|
+
["#{status} #{label}#{card}", url]
|
|
140
|
+
end
|
|
141
|
+
return if options.empty?
|
|
142
|
+
|
|
143
|
+
if options.length == 1
|
|
144
|
+
spawn("xdg-open", options[0][1], %i[out err] => "/dev/null")
|
|
145
|
+
else
|
|
146
|
+
labels = options.map(&:first)
|
|
147
|
+
choice = `timeout 30 zenity --list --title="Open Environment" --column="Environment" #{labels.map do |l|
|
|
148
|
+
Shellwords.escape(l)
|
|
149
|
+
end.join(" ")} 2>/dev/null`.strip
|
|
150
|
+
return if choice.empty?
|
|
151
|
+
|
|
152
|
+
selected = options.find { |label, _| label == choice }
|
|
153
|
+
spawn("xdg-open", selected[1], %i[out err] => "/dev/null") if selected
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def handle_deploy
|
|
158
|
+
deployments = fetch_deployments
|
|
159
|
+
return unless deployments&.any?
|
|
160
|
+
|
|
161
|
+
# Pick environment
|
|
162
|
+
envs = deployments.map { |d| [d["env"], d["label"] || d["env"]] }
|
|
163
|
+
if envs.length == 1
|
|
164
|
+
env_key = envs[0][0]
|
|
165
|
+
else
|
|
166
|
+
labels = envs.map { |key, label| "#{key}|#{label}" }
|
|
167
|
+
choice = `timeout 30 zenity --list --title="Deploy to..." --column="Env" --column="Label" #{labels.map do |l|
|
|
168
|
+
l.split("|").map do |p|
|
|
169
|
+
Shellwords.escape(p)
|
|
170
|
+
end.join(" ")
|
|
171
|
+
end.join(" ")} 2>/dev/null`.strip
|
|
172
|
+
return if choice.empty?
|
|
173
|
+
|
|
174
|
+
env_key = choice
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Get card number — pre-fill with current card if environment is occupied
|
|
178
|
+
selected_dep = deployments.find { |d| d["env"] == env_key }
|
|
179
|
+
prefill = selected_dep && selected_dep["status"] == "occupied" && selected_dep["card_number"] ? selected_dep["card_number"].to_s : ""
|
|
180
|
+
card_number = `timeout 60 zenity --entry --title="Deploy to #{env_key}" --text="Fizzy card number:"#{unless prefill.empty?
|
|
181
|
+
" --entry-text=#{Shellwords.escape(prefill)}"
|
|
182
|
+
end} 2>/dev/null`.strip
|
|
183
|
+
return if card_number.empty?
|
|
184
|
+
|
|
185
|
+
# Resolve worktree via glob (same pattern as fz shell function)
|
|
186
|
+
matches = Dir.glob(File.expand_path("~/Code/*fizzy-#{card_number}-*/"))
|
|
187
|
+
worktree = matches.find { |d| File.directory?(d) }
|
|
188
|
+
unless worktree
|
|
189
|
+
`timeout 10 zenity --error --text="No worktree found for card ##{card_number}" 2>/dev/null`
|
|
190
|
+
return
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
deploy_script = <<~BASH
|
|
194
|
+
cd #{Shellwords.escape(worktree)}
|
|
195
|
+
echo "🚀 #{env_key} deploy in progress..."
|
|
196
|
+
echo
|
|
197
|
+
logfile=$(mktemp)
|
|
198
|
+
./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1 | tee "$logfile"
|
|
199
|
+
status=${PIPESTATUS[0]}
|
|
200
|
+
if [ $status -ne 0 ] && grep -q "checksums previously recorded in the dependency lock file" "$logfile"; then
|
|
201
|
+
echo
|
|
202
|
+
echo "⚠️ Terraform lock file mismatch — removing lock and running init -upgrade..."
|
|
203
|
+
echo
|
|
204
|
+
rm -f infrastructure/#{Shellwords.escape(env_key)}/.terraform.lock.hcl
|
|
205
|
+
(cd infrastructure/#{Shellwords.escape(env_key)} && terraform init -upgrade)
|
|
206
|
+
echo
|
|
207
|
+
echo "🔄 Retrying deploy..."
|
|
208
|
+
echo
|
|
209
|
+
./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1
|
|
210
|
+
status=$?
|
|
211
|
+
fi
|
|
212
|
+
rm -f "$logfile"
|
|
213
|
+
echo
|
|
214
|
+
if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
|
|
215
|
+
echo "Press Enter to close..."
|
|
216
|
+
read
|
|
217
|
+
BASH
|
|
218
|
+
|
|
219
|
+
# Mark deploying via API so waybar turns orange immediately
|
|
220
|
+
begin
|
|
221
|
+
uri = URI("#{SERVER_URL}/api/deployments/#{env_key}/deploying")
|
|
222
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
223
|
+
req.body = { worktree: worktree }.to_json
|
|
224
|
+
Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
225
|
+
rescue StandardError
|
|
226
|
+
# Non-fatal — deploy proceeds even if server is unreachable
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c", deploy_script, %i[out err] => "/dev/null")
|
|
230
|
+
resize_deploy_terminal
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if ARGV.include?("--click")
|
|
234
|
+
handle_click
|
|
235
|
+
elsif ARGV.include?("--deploy")
|
|
236
|
+
handle_deploy
|
|
237
|
+
else
|
|
238
|
+
generate_output
|
|
239
|
+
end
|
data/monitor/waybar.rb
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Brainiac Waybar Module
|
|
5
|
+
# Reads from monitor daemon socket and outputs JSON for waybar
|
|
6
|
+
# Single module that updates content dynamically (no config rewrites)
|
|
7
|
+
|
|
8
|
+
require "json"
|
|
9
|
+
require "socket"
|
|
10
|
+
require "net/http"
|
|
11
|
+
|
|
12
|
+
SOCKET_PATH = "/tmp/brainiac-monitor.sock"
|
|
13
|
+
API_URL = "http://localhost:4567/api/status"
|
|
14
|
+
CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
|
|
15
|
+
|
|
16
|
+
# Load agent configuration from JSON
|
|
17
|
+
def load_agent_config
|
|
18
|
+
config = JSON.parse(File.read(CONFIG_PATH))
|
|
19
|
+
agents = {}
|
|
20
|
+
config["agents"].each do |agent|
|
|
21
|
+
agents[agent["name"].downcase] = agent["emoji"]
|
|
22
|
+
end
|
|
23
|
+
agents
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
warn "Failed to load waybar.json: #{e.message}"
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
AGENTS = load_agent_config.freeze
|
|
30
|
+
DEFAULT_EMOJI = "❓"
|
|
31
|
+
|
|
32
|
+
def normalize_agent_name(name)
|
|
33
|
+
name.downcase
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_state_from_socket
|
|
37
|
+
socket = UNIXSocket.new(SOCKET_PATH)
|
|
38
|
+
data = socket.read
|
|
39
|
+
socket.close
|
|
40
|
+
JSON.parse(data)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetch_state_from_api
|
|
44
|
+
uri = URI(API_URL)
|
|
45
|
+
response = Net::HTTP.get_response(uri)
|
|
46
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
47
|
+
|
|
48
|
+
JSON.parse(response.body)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_state
|
|
52
|
+
# Prefer socket (daemon mode) — faster, no HTTP overhead
|
|
53
|
+
fetch_state_from_socket
|
|
54
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED
|
|
55
|
+
# Daemon not running — fall back to direct API call
|
|
56
|
+
fetch_state_from_api || { "sessions" => [], "count" => 0, "error" => "server not reachable" }
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
{ "sessions" => [], "count" => 0, "error" => e.message }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def format_elapsed(seconds)
|
|
62
|
+
return "#{seconds}s" if seconds < 60
|
|
63
|
+
|
|
64
|
+
minutes = seconds / 60
|
|
65
|
+
return "#{minutes}m" if minutes < 60
|
|
66
|
+
|
|
67
|
+
hours = minutes / 60
|
|
68
|
+
"#{hours}h"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
INFRA_CMDS = %w[kiro-cli-chat ruby-lsp clangd gopls].freeze
|
|
72
|
+
|
|
73
|
+
def infra_process?(cmd_short)
|
|
74
|
+
INFRA_CMDS.any? { |ic| cmd_short.start_with?(ic) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def escape_pango(str)
|
|
78
|
+
str.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def generate_output
|
|
82
|
+
state = fetch_state
|
|
83
|
+
|
|
84
|
+
if state["error"]
|
|
85
|
+
return {
|
|
86
|
+
text: "⚠️",
|
|
87
|
+
tooltip: "Brainiac Error: #{escape_pango(state["error"])}",
|
|
88
|
+
class: "error"
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sessions = state["sessions"] || []
|
|
93
|
+
|
|
94
|
+
if sessions.empty?
|
|
95
|
+
return {
|
|
96
|
+
text: "💤",
|
|
97
|
+
tooltip: "No active agent sessions",
|
|
98
|
+
class: "idle"
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Build text: show emoji for each active agent
|
|
103
|
+
text_parts = sessions.map { |s| AGENTS[normalize_agent_name(s["agent"])] || DEFAULT_EMOJI }
|
|
104
|
+
text = text_parts.join(" ")
|
|
105
|
+
|
|
106
|
+
# Build tooltip
|
|
107
|
+
tooltip_lines = sessions.map do |s|
|
|
108
|
+
agent_display = s["agent"]
|
|
109
|
+
emoji = AGENTS[normalize_agent_name(agent_display)] || DEFAULT_EMOJI
|
|
110
|
+
elapsed = format_elapsed(s["elapsed_seconds"])
|
|
111
|
+
|
|
112
|
+
card_key = s["card_key"]
|
|
113
|
+
context = if card_key.start_with?("discord-")
|
|
114
|
+
"Discord chat"
|
|
115
|
+
elsif card_key.start_with?("card-")
|
|
116
|
+
card_key.split("-")[1]
|
|
117
|
+
else
|
|
118
|
+
card_key
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
lines = ["#{emoji} #{agent_display}: #{context} (#{elapsed})"]
|
|
122
|
+
|
|
123
|
+
children = (s["children"] || []).reject do |c|
|
|
124
|
+
cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
|
|
125
|
+
infra_process?(cmd_short)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
children.each do |c|
|
|
129
|
+
cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
|
|
130
|
+
cmd_short = c["cmd"].to_s[0..40] if cmd_short.empty?
|
|
131
|
+
lines << " └ #{escape_pango(cmd_short)} (#{format_elapsed(c["elapsed_seconds"])}) [PID #{c["pid"]}]"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
lines.join("\n")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
tooltip_lines << "\n[Click to manage]"
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
text: text,
|
|
141
|
+
tooltip: tooltip_lines.join("\n"),
|
|
142
|
+
class: "working"
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
puts generate_output.to_json
|