brainiac 0.0.5 → 0.0.7
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/Gemfile.lock +2 -2
- data/README.md +136 -6
- data/bin/brainiac +385 -19
- data/bin/brainiac-completion.bash +1 -1
- data/lib/brainiac/agents.rb +15 -38
- data/lib/brainiac/config.rb +40 -11
- data/lib/brainiac/handlers/discord/api.rb +196 -0
- data/lib/brainiac/handlers/discord/config.rb +134 -0
- data/lib/brainiac/handlers/discord/delivery.rb +196 -0
- data/lib/brainiac/handlers/discord/gateway.rb +212 -0
- data/lib/brainiac/handlers/discord/message.rb +933 -0
- data/lib/brainiac/handlers/discord/reactions.rb +215 -0
- data/lib/brainiac/handlers/discord.rb +14 -1892
- data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
- data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
- data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
- data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
- data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
- data/lib/brainiac/handlers/fizzy.rb +12 -1290
- data/lib/brainiac/handlers/github.rb +149 -212
- data/lib/brainiac/handlers/shared/git.rb +203 -0
- data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
- data/lib/brainiac/handlers/zoho.rb +79 -76
- data/lib/brainiac/helpers.rb +2 -121
- data/lib/brainiac/planning.rb +2 -2
- data/lib/brainiac/plugins.rb +129 -0
- data/lib/brainiac/restart.rb +94 -0
- data/lib/brainiac/routes/api.rb +427 -0
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac/zoho_mail_api.rb +2 -1
- data/lib/brainiac.rb +7 -0
- data/monitor/daemon.rb +4 -27
- data/monitor/shared.rb +247 -0
- data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
- data/monitor/waybar/setup.rb +232 -0
- data/monitor/waybar/status.rb +51 -0
- data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
- data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
- data/monitor/xbar/plugin.rb +155 -0
- data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
- data/receiver.rb +91 -450
- data/templates/brainiac.json.example +9 -0
- data/templates/cli-providers/kiro.json.example +8 -2
- data/templates/plugins.json.example +3 -0
- metadata +30 -20
- data/lib/user_registry.rb +0 -159
- data/monitor/menubar.rb +0 -295
- data/monitor/setup-waybar-deploy-envs.rb +0 -121
- data/monitor/setup-waybar-deployments.rb +0 -96
- data/monitor/setup-waybar-module.rb +0 -113
- data/monitor/setup-xbar-plugin.rb +0 -35
- data/monitor/view-logs.rb +0 -119
- data/monitor/waybar-config-updater.rb +0 -56
- data/monitor/waybar-deployments.rb +0 -239
- data/monitor/waybar.rb +0 -146
- data/monitor/xbar.3s.rb +0 -179
- /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
- /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
- /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
data/monitor/daemon.rb
CHANGED
|
@@ -2,32 +2,13 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
# Brainiac Monitor Daemon
|
|
5
|
-
# Polls /api/status and exposes agent state via Unix socket for waybar
|
|
6
|
-
#
|
|
5
|
+
# Polls /api/status and exposes agent state via Unix socket for waybar/xbar.
|
|
6
|
+
# Modules (waybar/status.rb, xbar/plugin.rb) read from this socket.
|
|
7
7
|
|
|
8
|
-
require "
|
|
9
|
-
|
|
10
|
-
require "socket"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require_relative "shared"
|
|
11
10
|
|
|
12
|
-
SOCKET_PATH = "/tmp/brainiac-monitor.sock"
|
|
13
|
-
API_URL = "http://localhost:4567/api/status"
|
|
14
11
|
POLL_INTERVAL = 2 # seconds
|
|
15
|
-
CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
|
|
16
|
-
|
|
17
|
-
# Load agent configuration from JSON
|
|
18
|
-
def load_agent_config
|
|
19
|
-
config = JSON.parse(File.read(CONFIG_PATH))
|
|
20
|
-
agents = {}
|
|
21
|
-
config["agents"].each do |agent|
|
|
22
|
-
agents[agent["name"]] = { color: agent["color"], emoji: agent["emoji"] }
|
|
23
|
-
end
|
|
24
|
-
agents
|
|
25
|
-
rescue StandardError => e
|
|
26
|
-
warn "Failed to load waybar.json: #{e.message}"
|
|
27
|
-
{}
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
AGENTS = load_agent_config.freeze
|
|
31
12
|
|
|
32
13
|
@state = { sessions: [], count: 0, recent: [], last_update: nil }
|
|
33
14
|
|
|
@@ -67,12 +48,10 @@ def start_server
|
|
|
67
48
|
server = UNIXServer.new(SOCKET_PATH)
|
|
68
49
|
File.chmod(0o666, SOCKET_PATH)
|
|
69
50
|
|
|
70
|
-
# Write PID file
|
|
71
51
|
File.write("/tmp/brainiac-daemon.pid", Process.pid)
|
|
72
52
|
|
|
73
53
|
puts "Monitor daemon started, socket: #{SOCKET_PATH}"
|
|
74
54
|
|
|
75
|
-
# Start polling thread
|
|
76
55
|
poller = Thread.new do
|
|
77
56
|
loop do
|
|
78
57
|
update_state
|
|
@@ -80,10 +59,8 @@ def start_server
|
|
|
80
59
|
end
|
|
81
60
|
end
|
|
82
61
|
|
|
83
|
-
# Initial state fetch
|
|
84
62
|
update_state
|
|
85
63
|
|
|
86
|
-
# Accept client connections
|
|
87
64
|
loop do
|
|
88
65
|
client = server.accept
|
|
89
66
|
Thread.new { handle_client(client) }
|
data/monitor/shared.rb
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared helpers for all monitor scripts (waybar, xbar, daemon).
|
|
4
|
+
#
|
|
5
|
+
# Provides: constants, config loading, state fetching, formatting utilities.
|
|
6
|
+
# Require this at the top of any monitor script.
|
|
7
|
+
|
|
8
|
+
require "json"
|
|
9
|
+
require "net/http"
|
|
10
|
+
require "socket"
|
|
11
|
+
require "uri"
|
|
12
|
+
|
|
13
|
+
# --- Constants ---
|
|
14
|
+
|
|
15
|
+
SOCKET_PATH = "/tmp/brainiac-monitor.sock"
|
|
16
|
+
API_URL = "http://localhost:4567/api/status"
|
|
17
|
+
SERVER_URL = "http://localhost:4567"
|
|
18
|
+
CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
|
|
19
|
+
BRAINIAC_DIR = File.expand_path("~/.brainiac")
|
|
20
|
+
|
|
21
|
+
# --- Agent Config ---
|
|
22
|
+
|
|
23
|
+
# Load agent configuration from ~/.brainiac/waybar.json.
|
|
24
|
+
# Returns { "galen" => { emoji: "🤖", color: "blue" }, ... }
|
|
25
|
+
def load_agent_config
|
|
26
|
+
config = JSON.parse(File.read(CONFIG_PATH, encoding: "utf-8"))
|
|
27
|
+
agents = {}
|
|
28
|
+
(config["agents"] || []).each do |agent|
|
|
29
|
+
agents[agent["name"].downcase] = { emoji: agent["emoji"], color: agent["color"] }
|
|
30
|
+
end
|
|
31
|
+
agents
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
warn "Failed to load waybar.json: #{e.message}"
|
|
34
|
+
{}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_monitor_config
|
|
38
|
+
return {} unless File.exist?(CONFIG_PATH)
|
|
39
|
+
|
|
40
|
+
JSON.parse(File.read(CONFIG_PATH, encoding: "utf-8"))
|
|
41
|
+
rescue StandardError
|
|
42
|
+
{}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
DEFAULT_EMOJI = "❓"
|
|
46
|
+
|
|
47
|
+
# --- State Fetching ---
|
|
48
|
+
|
|
49
|
+
# Fetch state from daemon socket (fast, no HTTP overhead).
|
|
50
|
+
def fetch_state_from_socket
|
|
51
|
+
socket = UNIXSocket.new(SOCKET_PATH)
|
|
52
|
+
data = socket.read
|
|
53
|
+
socket.close
|
|
54
|
+
JSON.parse(data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Fetch state from brainiac HTTP API (fallback when daemon not running).
|
|
58
|
+
def fetch_state_from_api
|
|
59
|
+
uri = URI(API_URL)
|
|
60
|
+
response = Net::HTTP.get_response(uri)
|
|
61
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
62
|
+
|
|
63
|
+
JSON.parse(response.body)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Fetch agent state — tries socket first, falls back to API.
|
|
69
|
+
def fetch_state
|
|
70
|
+
fetch_state_from_socket
|
|
71
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED
|
|
72
|
+
fetch_state_from_api || { "sessions" => [], "count" => 0, "error" => "server not reachable" }
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
{ "sessions" => [], "count" => 0, "error" => e.message }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Fetch deployment state from API.
|
|
78
|
+
def fetch_deployments
|
|
79
|
+
uri = URI("#{SERVER_URL}/api/deployments")
|
|
80
|
+
response = Net::HTTP.get_response(uri)
|
|
81
|
+
JSON.parse(response.body)["deployments"] || []
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# --- Formatting ---
|
|
87
|
+
|
|
88
|
+
def format_elapsed(seconds)
|
|
89
|
+
return "#{seconds}s" if seconds < 60
|
|
90
|
+
|
|
91
|
+
minutes = seconds / 60
|
|
92
|
+
return "#{minutes}m" if minutes < 60
|
|
93
|
+
|
|
94
|
+
hours = minutes / 60
|
|
95
|
+
return "#{hours}h" if hours < 24
|
|
96
|
+
|
|
97
|
+
"#{hours / 24}d"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def format_context(card_key)
|
|
101
|
+
return "" unless card_key
|
|
102
|
+
|
|
103
|
+
if card_key.start_with?("discord-")
|
|
104
|
+
"Discord"
|
|
105
|
+
elsif card_key.start_with?("card-")
|
|
106
|
+
"##{card_key.split("-")[1]}"
|
|
107
|
+
else
|
|
108
|
+
card_key
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def time_ago(iso_string)
|
|
113
|
+
return nil unless iso_string
|
|
114
|
+
|
|
115
|
+
seconds = (Time.now - Time.parse(iso_string)).to_i
|
|
116
|
+
"#{format_elapsed(seconds)} ago"
|
|
117
|
+
rescue StandardError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# --- Log Preview ---
|
|
122
|
+
|
|
123
|
+
ANSI_REGEX = /\e\[[0-9;]*[a-zA-Z]|\e\[\?[0-9;]*[a-zA-Z]/
|
|
124
|
+
LOG_PREVIEW_LINES = 15
|
|
125
|
+
LOG_LINE_MAX = 80
|
|
126
|
+
LOG_FONT = "SFMono-Regular"
|
|
127
|
+
LOG_SIZE = 12
|
|
128
|
+
|
|
129
|
+
def tail_log(log_file, lines: LOG_PREVIEW_LINES)
|
|
130
|
+
return [] unless log_file && File.exist?(log_file)
|
|
131
|
+
|
|
132
|
+
raw = `tail -n 50 #{log_file.shellescape} 2>/dev/null`
|
|
133
|
+
raw.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
134
|
+
.lines
|
|
135
|
+
.map { |l| l.gsub(ANSI_REGEX, "").gsub(/[^[:print:]\t]/, "").strip }
|
|
136
|
+
.reject(&:empty?)
|
|
137
|
+
.last(lines)
|
|
138
|
+
rescue StandardError
|
|
139
|
+
[]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def format_log_line(text)
|
|
143
|
+
text.length > LOG_LINE_MAX ? "#{text[0, LOG_LINE_MAX]}…" : text
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# --- Deploy Helpers ---
|
|
147
|
+
|
|
148
|
+
DEPLOY_RECENT_WINDOW = 30 * 60 # 30 minutes
|
|
149
|
+
|
|
150
|
+
def deploy_dot_emoji(dep)
|
|
151
|
+
status = dep["last_deploy_status"]
|
|
152
|
+
if status == "deploying"
|
|
153
|
+
"🟠"
|
|
154
|
+
elsif status == "failed"
|
|
155
|
+
"💥"
|
|
156
|
+
elsif dep["status"] == "occupied"
|
|
157
|
+
deploy_time = dep["last_deploy_at"] || dep["deployed_at"]
|
|
158
|
+
recent = deploy_time && (Time.now - Time.parse(deploy_time)) < DEPLOY_RECENT_WINDOW
|
|
159
|
+
recent ? "🚀" : "🔴"
|
|
160
|
+
else
|
|
161
|
+
"🟢"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Resolve worktree path for a card number from card_map.json or filesystem glob.
|
|
166
|
+
def resolve_worktree(card_number, glob_base: "~/Code")
|
|
167
|
+
# Try card_map.json first
|
|
168
|
+
card_map_path = File.join(BRAINIAC_DIR, "card_map.json")
|
|
169
|
+
if File.exist?(card_map_path)
|
|
170
|
+
card_map = begin
|
|
171
|
+
JSON.parse(File.read(card_map_path))
|
|
172
|
+
rescue StandardError
|
|
173
|
+
{}
|
|
174
|
+
end
|
|
175
|
+
entry = card_map.values.find { |e| e["number"].to_s == card_number.to_s }
|
|
176
|
+
return entry["worktree"] if entry && entry["worktree"] && File.directory?(entry["worktree"].to_s)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Fallback: glob
|
|
180
|
+
matches = Dir.glob(File.expand_path("#{glob_base}/*fizzy-#{card_number}-*/"))
|
|
181
|
+
matches.find { |d| File.directory?(d) }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Generate the bash deploy script with terraform lock retry logic.
|
|
185
|
+
def deploy_bash_script(env_key, worktree:, aws_profile: nil)
|
|
186
|
+
<<~BASH
|
|
187
|
+
cd #{worktree.shellescape}
|
|
188
|
+
#{"export AWS_PROFILE=#{aws_profile.shellescape}" if aws_profile}
|
|
189
|
+
echo "🚀 Deploying to #{env_key}..."
|
|
190
|
+
echo
|
|
191
|
+
logfile=$(mktemp)
|
|
192
|
+
./scripts/deploy.sh #{env_key.shellescape} 2>&1 | tee "$logfile"
|
|
193
|
+
status=${PIPESTATUS[0]}
|
|
194
|
+
if [ $status -ne 0 ] && grep -q "checksums previously recorded in the dependency lock file" "$logfile"; then
|
|
195
|
+
echo
|
|
196
|
+
echo "⚠️ Terraform lock file mismatch — removing lock and running init -upgrade..."
|
|
197
|
+
echo
|
|
198
|
+
rm -f infrastructure/#{env_key.shellescape}/.terraform.lock.hcl
|
|
199
|
+
(cd infrastructure/#{env_key.shellescape} && terraform init -upgrade)
|
|
200
|
+
echo
|
|
201
|
+
echo "🔄 Retrying deploy..."
|
|
202
|
+
echo
|
|
203
|
+
./scripts/deploy.sh #{env_key.shellescape} 2>&1
|
|
204
|
+
status=$?
|
|
205
|
+
fi
|
|
206
|
+
rm -f "$logfile"
|
|
207
|
+
echo
|
|
208
|
+
if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
|
|
209
|
+
echo "Press Enter to close..."
|
|
210
|
+
read
|
|
211
|
+
BASH
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Mark an environment as deploying via the brainiac API.
|
|
215
|
+
def mark_deploying_via_api(env_key, worktree:)
|
|
216
|
+
uri = URI("#{SERVER_URL}/api/deployments/#{env_key}/deploying")
|
|
217
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
218
|
+
req.body = { worktree: worktree }.to_json
|
|
219
|
+
Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
220
|
+
rescue StandardError
|
|
221
|
+
# Non-fatal — deploy proceeds even if server is unreachable
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Resolve AWS_PROFILE from deployments.json for an environment.
|
|
225
|
+
def resolve_aws_profile(env_key)
|
|
226
|
+
config_file = File.join(BRAINIAC_DIR, "deployments.json")
|
|
227
|
+
return nil unless File.exist?(config_file)
|
|
228
|
+
|
|
229
|
+
cfg = begin
|
|
230
|
+
JSON.parse(File.read(config_file))
|
|
231
|
+
rescue StandardError
|
|
232
|
+
{}
|
|
233
|
+
end
|
|
234
|
+
cfg.dig("environments", env_key, "aws_profile")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# --- Color ---
|
|
238
|
+
|
|
239
|
+
COLOR_MAP = {
|
|
240
|
+
"red" => "#ff5555", "green" => "#50fa7b", "blue" => "#8be9fd",
|
|
241
|
+
"yellow" => "#f1fa8c", "cyan" => "#8be9fd", "magenta" => "#ff79c6",
|
|
242
|
+
"purple" => "#bd93f9", "pink" => "#ff79c6", "white" => "#f8f8f2"
|
|
243
|
+
}.freeze
|
|
244
|
+
|
|
245
|
+
def hex_color(name)
|
|
246
|
+
COLOR_MAP[name] || name
|
|
247
|
+
end
|
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
# Brainiac Waybar Per-Environment Deploy Module
|
|
5
|
-
# Usage:
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Usage: deploy_env.rb <env_key>
|
|
6
|
+
# deploy_env.rb <env_key> --click
|
|
7
|
+
# deploy_env.rb <env_key> --deploy
|
|
8
8
|
|
|
9
|
-
require "json"
|
|
10
|
-
require "net/http"
|
|
11
9
|
require "shellwords"
|
|
12
|
-
require "uri"
|
|
13
10
|
require "time"
|
|
14
|
-
|
|
15
|
-
SERVER_URL = "http://localhost:4567"
|
|
16
|
-
RECENT_WINDOW = 30 * 60
|
|
11
|
+
require_relative "../shared"
|
|
17
12
|
|
|
18
13
|
env_key = ARGV.find { |a| !a.start_with?("--") }
|
|
19
14
|
unless env_key
|
|
@@ -21,30 +16,7 @@ unless env_key
|
|
|
21
16
|
exit
|
|
22
17
|
end
|
|
23
18
|
|
|
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
19
|
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
20
|
script = "sleep 0.5 && " \
|
|
49
21
|
'width=$(hyprctl monitors -j | ruby -rjson -e "puts JSON.parse(STDIN.read)[0][%q(width)]") && ' \
|
|
50
22
|
"delta=$(( (width / 2) - (width * 15 / 100) )) && " \
|
|
@@ -58,8 +30,10 @@ def handle_click(env_key, deployment)
|
|
|
58
30
|
if deployment["last_deploy_status"] == "failed" && deployment["last_deploy_log"]
|
|
59
31
|
log = deployment["last_deploy_log"]
|
|
60
32
|
if File.exist?(log.to_s)
|
|
61
|
-
|
|
62
|
-
|
|
33
|
+
label = deployment["label"] || env_key
|
|
34
|
+
cmd = "echo '=== Deploy failure: #{label} ===' && echo && " \
|
|
35
|
+
"cat #{Shellwords.escape(log)} && echo && echo 'Press Enter to close...' && read"
|
|
36
|
+
spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c", cmd,
|
|
63
37
|
%i[out err] => "/dev/null")
|
|
64
38
|
resize_deploy_terminal
|
|
65
39
|
return
|
|
@@ -79,63 +53,17 @@ def handle_deploy(env_key, deployment)
|
|
|
79
53
|
end} 2>/dev/null`.strip
|
|
80
54
|
return if card_number.empty?
|
|
81
55
|
|
|
82
|
-
|
|
83
|
-
worktree = matches.find { |d| File.directory?(d) }
|
|
56
|
+
worktree = resolve_worktree(card_number)
|
|
84
57
|
unless worktree
|
|
85
58
|
`timeout 10 zenity --error --text="No worktree found for card ##{card_number}" 2>/dev/null`
|
|
86
59
|
return
|
|
87
60
|
end
|
|
88
61
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
62
|
+
aws_profile = resolve_aws_profile(env_key)
|
|
63
|
+
mark_deploying_via_api(env_key, worktree: worktree)
|
|
137
64
|
|
|
138
|
-
|
|
65
|
+
script = deploy_bash_script(env_key, worktree: worktree, aws_profile: aws_profile)
|
|
66
|
+
spawn("alacritty", "--class", "brainiac-deploy", "-e", "bash", "-c", script, %i[out err] => "/dev/null")
|
|
139
67
|
resize_deploy_terminal
|
|
140
68
|
end
|
|
141
69
|
|
|
@@ -156,7 +84,7 @@ def generate_output(env_key)
|
|
|
156
84
|
|
|
157
85
|
if d["status"] == "occupied"
|
|
158
86
|
deploy_time = d["last_deploy_at"] || d["deployed_at"]
|
|
159
|
-
recent = deploy_time && (Time.now - Time.parse(deploy_time)) <
|
|
87
|
+
recent = deploy_time && (Time.now - Time.parse(deploy_time)) < DEPLOY_RECENT_WINDOW
|
|
160
88
|
status = d["last_deploy_status"]
|
|
161
89
|
|
|
162
90
|
if status == "deploying"
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# One-time setup: installs Brainiac modules into waybar config.
|
|
5
|
+
#
|
|
6
|
+
# Adds:
|
|
7
|
+
# - Agent session status module (custom/brainiac)
|
|
8
|
+
# - Per-environment deploy dots (custom/brainiac-deploy-<env>)
|
|
9
|
+
#
|
|
10
|
+
# Usage: ruby monitor/waybar/setup.rb
|
|
11
|
+
|
|
12
|
+
require "json"
|
|
13
|
+
require "fileutils"
|
|
14
|
+
|
|
15
|
+
WAYBAR_CONFIG = File.expand_path("~/.config/waybar/config.jsonc")
|
|
16
|
+
WAYBAR_STYLE = File.expand_path("~/.config/waybar/style.css")
|
|
17
|
+
BRAINIAC_DIR = File.expand_path("~/.brainiac")
|
|
18
|
+
DEPLOYMENTS_CONFIG = File.join(BRAINIAC_DIR, "deployments.json")
|
|
19
|
+
|
|
20
|
+
# Wrapper scripts go to ~/.brainiac/bin/ and resolve from /api/status (server_root field)
|
|
21
|
+
# Falls back to ~/.brainiac/server.root file for cold-start scenarios
|
|
22
|
+
WRAPPER_DIR = File.join(BRAINIAC_DIR, "bin")
|
|
23
|
+
FileUtils.mkdir_p(WRAPPER_DIR)
|
|
24
|
+
|
|
25
|
+
def load_waybar_config
|
|
26
|
+
content = File.read(WAYBAR_CONFIG)
|
|
27
|
+
json_content = content.lines.reject { |line| line.strip.start_with?("//") }.join
|
|
28
|
+
JSON.parse(json_content)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save_waybar_config(config)
|
|
32
|
+
File.write(WAYBAR_CONFIG, JSON.pretty_generate(config))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# --- Create wrapper scripts ---
|
|
36
|
+
|
|
37
|
+
# Status wrapper
|
|
38
|
+
status_wrapper = File.join(WRAPPER_DIR, "waybar-status")
|
|
39
|
+
File.write(status_wrapper, <<~SCRIPT)
|
|
40
|
+
#!/usr/bin/env ruby
|
|
41
|
+
require "json"
|
|
42
|
+
require "net/http"
|
|
43
|
+
require "uri"
|
|
44
|
+
|
|
45
|
+
def resolve_server_root
|
|
46
|
+
# Primary: query the running server's /api/status endpoint
|
|
47
|
+
uri = URI("http://localhost:4567/api/status")
|
|
48
|
+
response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 1, read_timeout: 2) { |http| http.get(uri.path) }
|
|
49
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
50
|
+
data = JSON.parse(response.body)
|
|
51
|
+
root = data["server_root"]
|
|
52
|
+
return root if root && File.directory?(root)
|
|
53
|
+
end
|
|
54
|
+
# Fallback: read the file (cold-start or server returned no root)
|
|
55
|
+
root_file = File.expand_path("~/.brainiac/server.root")
|
|
56
|
+
File.read(root_file).strip if File.exist?(root_file)
|
|
57
|
+
rescue StandardError
|
|
58
|
+
# Fallback: read the file (server unreachable)
|
|
59
|
+
root_file = File.expand_path("~/.brainiac/server.root")
|
|
60
|
+
File.exist?(root_file) ? File.read(root_file).strip : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
server_root = resolve_server_root
|
|
64
|
+
if server_root
|
|
65
|
+
script = File.join(server_root, "monitor", "waybar", "status.rb")
|
|
66
|
+
if File.exist?(script)
|
|
67
|
+
load script
|
|
68
|
+
exit
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
puts({ text: "⚠️", tooltip: "Brainiac server root not found", class: "error" }.to_json)
|
|
72
|
+
SCRIPT
|
|
73
|
+
File.chmod(0o755, status_wrapper)
|
|
74
|
+
|
|
75
|
+
# Log viewer wrapper
|
|
76
|
+
logs_wrapper = File.join(WRAPPER_DIR, "waybar-logs")
|
|
77
|
+
File.write(logs_wrapper, <<~SCRIPT)
|
|
78
|
+
#!/usr/bin/env ruby
|
|
79
|
+
require "json"
|
|
80
|
+
require "net/http"
|
|
81
|
+
require "uri"
|
|
82
|
+
|
|
83
|
+
def resolve_server_root
|
|
84
|
+
uri = URI("http://localhost:4567/api/status")
|
|
85
|
+
response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 1, read_timeout: 2) { |http| http.get(uri.path) }
|
|
86
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
87
|
+
data = JSON.parse(response.body)
|
|
88
|
+
root = data["server_root"]
|
|
89
|
+
return root if root && File.directory?(root)
|
|
90
|
+
end
|
|
91
|
+
root_file = File.expand_path("~/.brainiac/server.root")
|
|
92
|
+
File.read(root_file).strip if File.exist?(root_file)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
root_file = File.expand_path("~/.brainiac/server.root")
|
|
95
|
+
File.exist?(root_file) ? File.read(root_file).strip : nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
server_root = resolve_server_root
|
|
99
|
+
if server_root
|
|
100
|
+
script = File.join(server_root, "monitor", "waybar", "view_logs.rb")
|
|
101
|
+
exec("ruby", script) if File.exist?(script)
|
|
102
|
+
end
|
|
103
|
+
warn "Brainiac server root not found"
|
|
104
|
+
SCRIPT
|
|
105
|
+
File.chmod(0o755, logs_wrapper)
|
|
106
|
+
|
|
107
|
+
# Deploy env wrapper
|
|
108
|
+
deploy_wrapper = File.join(WRAPPER_DIR, "waybar-deploy-env")
|
|
109
|
+
File.write(deploy_wrapper, <<~SCRIPT)
|
|
110
|
+
#!/usr/bin/env ruby
|
|
111
|
+
require "json"
|
|
112
|
+
require "net/http"
|
|
113
|
+
require "uri"
|
|
114
|
+
|
|
115
|
+
def resolve_server_root
|
|
116
|
+
uri = URI("http://localhost:4567/api/status")
|
|
117
|
+
response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 1, read_timeout: 2) { |http| http.get(uri.path) }
|
|
118
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
119
|
+
data = JSON.parse(response.body)
|
|
120
|
+
root = data["server_root"]
|
|
121
|
+
return root if root && File.directory?(root)
|
|
122
|
+
end
|
|
123
|
+
root_file = File.expand_path("~/.brainiac/server.root")
|
|
124
|
+
File.read(root_file).strip if File.exist?(root_file)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
root_file = File.expand_path("~/.brainiac/server.root")
|
|
127
|
+
File.exist?(root_file) ? File.read(root_file).strip : nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
server_root = resolve_server_root
|
|
131
|
+
if server_root
|
|
132
|
+
script = File.join(server_root, "monitor", "waybar", "deploy_env.rb")
|
|
133
|
+
if File.exist?(script)
|
|
134
|
+
load script
|
|
135
|
+
exit
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
puts({ text: "", tooltip: "Brainiac server root not found", class: "error" }.to_json)
|
|
139
|
+
SCRIPT
|
|
140
|
+
File.chmod(0o755, deploy_wrapper)
|
|
141
|
+
|
|
142
|
+
puts "✓ Created wrapper scripts in #{WRAPPER_DIR}"
|
|
143
|
+
|
|
144
|
+
# --- Update waybar config ---
|
|
145
|
+
|
|
146
|
+
config = load_waybar_config
|
|
147
|
+
|
|
148
|
+
# Clean up old brainiac modules from all positions
|
|
149
|
+
%w[modules-left modules-center modules-right].each do |pos|
|
|
150
|
+
next unless config[pos]
|
|
151
|
+
|
|
152
|
+
config[pos].reject! { |m| m.to_s.include?("brainiac") }
|
|
153
|
+
end
|
|
154
|
+
config.each_key { |key| config.delete(key) if key.to_s.include?("brainiac") }
|
|
155
|
+
|
|
156
|
+
# Add agent session module to modules-center
|
|
157
|
+
config["modules-center"] ||= []
|
|
158
|
+
config["modules-center"] << "custom/brainiac"
|
|
159
|
+
|
|
160
|
+
config["custom/brainiac"] = {
|
|
161
|
+
"exec" => status_wrapper,
|
|
162
|
+
"return-type" => "json",
|
|
163
|
+
"interval" => 3,
|
|
164
|
+
"format" => "{}",
|
|
165
|
+
"tooltip" => true,
|
|
166
|
+
"on-click" => logs_wrapper
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Add per-environment deploy modules (if deployments.json exists)
|
|
170
|
+
if File.exist?(DEPLOYMENTS_CONFIG)
|
|
171
|
+
deployments = JSON.parse(File.read(DEPLOYMENTS_CONFIG))
|
|
172
|
+
envs = (deployments["environments"] || {}).keys
|
|
173
|
+
|
|
174
|
+
center = config["modules-center"]
|
|
175
|
+
brainiac_idx = center.index("custom/brainiac") || center.length
|
|
176
|
+
|
|
177
|
+
envs.each_with_index do |env, i|
|
|
178
|
+
mod_name = "custom/brainiac-deploy-#{env}"
|
|
179
|
+
center.insert(brainiac_idx + i, mod_name)
|
|
180
|
+
|
|
181
|
+
config[mod_name] = {
|
|
182
|
+
"exec" => "#{deploy_wrapper} #{env}",
|
|
183
|
+
"return-type" => "json",
|
|
184
|
+
"interval" => 30,
|
|
185
|
+
"format" => "{}",
|
|
186
|
+
"tooltip" => true,
|
|
187
|
+
"escape" => false,
|
|
188
|
+
"on-click" => "#{deploy_wrapper} #{env} --click",
|
|
189
|
+
"on-click-right" => "#{deploy_wrapper} #{env} --deploy"
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
puts "✓ Added #{envs.size} deploy environment module(s): #{envs.join(", ")}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
save_waybar_config(config)
|
|
197
|
+
puts "✓ Updated waybar config at #{WAYBAR_CONFIG}"
|
|
198
|
+
|
|
199
|
+
# --- Update CSS ---
|
|
200
|
+
|
|
201
|
+
style = File.exist?(WAYBAR_STYLE) ? File.read(WAYBAR_STYLE) : ""
|
|
202
|
+
|
|
203
|
+
unless style.include?("#custom-brainiac")
|
|
204
|
+
css = <<~CSS
|
|
205
|
+
|
|
206
|
+
/* Brainiac agent session module */
|
|
207
|
+
#custom-brainiac {
|
|
208
|
+
padding-right: 100px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Brainiac per-environment deploy dots */
|
|
212
|
+
[id^="custom-brainiac-deploy-"] {
|
|
213
|
+
font-size: 28px;
|
|
214
|
+
padding: 0 6px;
|
|
215
|
+
border-radius: 8px;
|
|
216
|
+
border: 2px solid transparent;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[id^="custom-brainiac-deploy-"].deploy-recent {
|
|
220
|
+
border: 2px solid #4488ff;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
[id^="custom-brainiac-deploy-"].deploy-failed {
|
|
224
|
+
border: 2px solid #ff4444;
|
|
225
|
+
}
|
|
226
|
+
CSS
|
|
227
|
+
File.write(WAYBAR_STYLE, "#{style.strip}\n#{css}")
|
|
228
|
+
puts "✓ Added brainiac styles to #{WAYBAR_STYLE}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
puts ""
|
|
232
|
+
puts "Restart waybar to apply: omarchy restart waybar"
|