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
data/monitor/menubar.rb
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Brainiac macOS Menu Bar Plugin (xbar/SwiftBar)
|
|
5
|
+
# Reads from monitor daemon socket and outputs xbar-format text
|
|
6
|
+
# Mirrors monitor/waybar.rb patterns for macOS-native display
|
|
7
|
+
|
|
8
|
+
require "json"
|
|
9
|
+
require "net/http"
|
|
10
|
+
require "shellwords"
|
|
11
|
+
require "socket"
|
|
12
|
+
require "time"
|
|
13
|
+
require "uri"
|
|
14
|
+
|
|
15
|
+
SERVER_URL = "http://localhost:4567"
|
|
16
|
+
SOCKET_PATH = "/tmp/brainiac-monitor.sock"
|
|
17
|
+
CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
|
|
18
|
+
DEFAULT_EMOJI = "❓"
|
|
19
|
+
SELF_PATH = File.realpath(__FILE__)
|
|
20
|
+
|
|
21
|
+
def load_config
|
|
22
|
+
JSON.parse(File.read(CONFIG_PATH))
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
warn "Failed to load waybar.json: #{e.message}"
|
|
25
|
+
{}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
CONFIG = load_config.freeze
|
|
29
|
+
|
|
30
|
+
def load_agent_config
|
|
31
|
+
agents = {}
|
|
32
|
+
(CONFIG["agents"] || []).each do |agent|
|
|
33
|
+
agents[agent["name"].downcase] = { emoji: agent["emoji"], color: agent["color"] }
|
|
34
|
+
end
|
|
35
|
+
agents
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
AGENTS = load_agent_config.freeze
|
|
39
|
+
FIZZY_ACCOUNT_ID = CONFIG["fizzy_account_id"]
|
|
40
|
+
DISCORD_GUILD_ID = CONFIG["discord_guild_id"]
|
|
41
|
+
|
|
42
|
+
def fetch_state
|
|
43
|
+
socket = UNIXSocket.new(SOCKET_PATH)
|
|
44
|
+
data = socket.read
|
|
45
|
+
socket.close
|
|
46
|
+
JSON.parse(data)
|
|
47
|
+
rescue Errno::ENOENT
|
|
48
|
+
{ "sessions" => [], "count" => 0, "recent" => [], "error" => "daemon not running" }
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
{ "sessions" => [], "count" => 0, "recent" => [], "error" => e.message }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fetch_deployments
|
|
54
|
+
uri = URI("#{SERVER_URL}/api/deployments")
|
|
55
|
+
response = Net::HTTP.get_response(uri)
|
|
56
|
+
JSON.parse(response.body)["deployments"] || []
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
DEPLOY_RECENT_WINDOW = 30 * 60
|
|
62
|
+
|
|
63
|
+
def deploy_dot(dep)
|
|
64
|
+
status = dep["last_deploy_status"]
|
|
65
|
+
if status == "deploying"
|
|
66
|
+
"🟠"
|
|
67
|
+
elsif status == "failed"
|
|
68
|
+
"💥"
|
|
69
|
+
elsif dep["status"] == "occupied"
|
|
70
|
+
deploy_time = dep["last_deploy_at"] || dep["deployed_at"]
|
|
71
|
+
recent = deploy_time && (Time.now - Time.parse(deploy_time)) < DEPLOY_RECENT_WINDOW
|
|
72
|
+
recent ? "🚀" : "🔴"
|
|
73
|
+
else
|
|
74
|
+
"🟢"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
LOG_VIEWER_PATH = File.join(File.dirname(SELF_PATH), "view-logs-macos.rb")
|
|
79
|
+
DEPLOY_SCRIPT_PATH = File.join(File.dirname(SELF_PATH), "deploy-env-macos.rb")
|
|
80
|
+
|
|
81
|
+
ANSI_REGEX = /\e\[[0-9;]*[a-zA-Z]|\e\[\?[0-9;]*[a-zA-Z]/
|
|
82
|
+
LOG_PREVIEW_LINES = 15
|
|
83
|
+
LOG_LINE_MAX = 80
|
|
84
|
+
LOG_FONT = "SFMono-Regular"
|
|
85
|
+
LOG_SIZE = 12
|
|
86
|
+
|
|
87
|
+
def tail_log(log_file, lines: LOG_PREVIEW_LINES)
|
|
88
|
+
return [] unless log_file && File.exist?(log_file)
|
|
89
|
+
|
|
90
|
+
raw = `tail -n 50 #{log_file.shellescape} 2>/dev/null`
|
|
91
|
+
raw.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
92
|
+
.lines
|
|
93
|
+
.map { |l| l.gsub(ANSI_REGEX, "").gsub(/[^[:print:]\t]/, "").strip }
|
|
94
|
+
.reject(&:empty?)
|
|
95
|
+
.last(lines)
|
|
96
|
+
rescue StandardError
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def format_log_line(text)
|
|
101
|
+
text.length > LOG_LINE_MAX ? "#{text[0, LOG_LINE_MAX]}…" : text
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def format_elapsed(seconds)
|
|
105
|
+
return "#{seconds}s" if seconds < 60
|
|
106
|
+
|
|
107
|
+
minutes = seconds / 60
|
|
108
|
+
return "#{minutes}m" if minutes < 60
|
|
109
|
+
|
|
110
|
+
"#{minutes / 60}h"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def format_context(card_key)
|
|
114
|
+
return "" unless card_key
|
|
115
|
+
|
|
116
|
+
if card_key.start_with?("discord-")
|
|
117
|
+
"Discord"
|
|
118
|
+
elsif card_key.start_with?("card-")
|
|
119
|
+
"##{card_key.split("-")[1]}"
|
|
120
|
+
else
|
|
121
|
+
card_key
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def time_ago(iso_string)
|
|
126
|
+
return nil unless iso_string
|
|
127
|
+
|
|
128
|
+
seconds = (Time.now - Time.parse(iso_string)).to_i
|
|
129
|
+
"#{format_elapsed(seconds)} ago"
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def log_action(log_file)
|
|
135
|
+
return "" unless log_file
|
|
136
|
+
|
|
137
|
+
" | shell=#{LOG_VIEWER_PATH} param1=#{log_file} terminal=false refresh=false"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
OPEN_SCRIPT = File.join(File.dirname(SELF_PATH), "open-action.sh")
|
|
141
|
+
|
|
142
|
+
def full_log_action(log_file)
|
|
143
|
+
return "" unless log_file
|
|
144
|
+
|
|
145
|
+
" | shell=#{OPEN_SCRIPT} param1=#{log_file.shellescape} terminal=false refresh=false"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def prompt_url(card_key)
|
|
149
|
+
return nil unless card_key
|
|
150
|
+
|
|
151
|
+
if card_key.start_with?("card-")
|
|
152
|
+
card_num = card_key.split("-")[1]
|
|
153
|
+
"https://app.fizzy.do/#{FIZZY_ACCOUNT_ID}/cards/#{card_num}" if FIZZY_ACCOUNT_ID && card_num
|
|
154
|
+
elsif card_key.start_with?("discord-") && DISCORD_GUILD_ID
|
|
155
|
+
parts = card_key.split("-")
|
|
156
|
+
# discord-AGENT-CHANNEL_ID-MESSAGE_ID (agent name may contain hyphens, IDs are last two numeric parts)
|
|
157
|
+
channel_id = parts[-2]
|
|
158
|
+
message_id = parts[-1]
|
|
159
|
+
"https://discord.com/channels/#{DISCORD_GUILD_ID}/#{channel_id}/#{message_id}" if channel_id && message_id
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def prompt_action(card_key)
|
|
164
|
+
url = prompt_url(card_key)
|
|
165
|
+
return "" unless url
|
|
166
|
+
|
|
167
|
+
" | shell=#{OPEN_SCRIPT} param1=#{url} terminal=false refresh=false"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def worktree_path(log_file, card_key)
|
|
171
|
+
return nil unless log_file && card_key&.start_with?("card-")
|
|
172
|
+
|
|
173
|
+
dir = File.dirname(log_file, 2)
|
|
174
|
+
dir if File.directory?(dir) && dir != "/"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def worktree_action(log_file, card_key)
|
|
178
|
+
path = worktree_path(log_file, card_key)
|
|
179
|
+
return "" unless path
|
|
180
|
+
|
|
181
|
+
" | shell=#{OPEN_SCRIPT} param1=#{path.shellescape} terminal=false refresh=false"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
COLOR_MAP = {
|
|
185
|
+
"red" => "#ff5555", "green" => "#50fa7b", "blue" => "#8be9fd",
|
|
186
|
+
"yellow" => "#f1fa8c", "cyan" => "#8be9fd", "magenta" => "#ff79c6",
|
|
187
|
+
"purple" => "#bd93f9", "pink" => "#ff79c6", "white" => "#f8f8f2"
|
|
188
|
+
}.freeze
|
|
189
|
+
|
|
190
|
+
def hex_color(name)
|
|
191
|
+
COLOR_MAP[name] || name
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def generate_output
|
|
195
|
+
state = fetch_state
|
|
196
|
+
deployments = fetch_deployments
|
|
197
|
+
|
|
198
|
+
return ["⚠️", "---", state["error"], "---", "Refresh | refresh=true"].join("\n") if state["error"] && !deployments
|
|
199
|
+
|
|
200
|
+
sessions = state["sessions"] || []
|
|
201
|
+
recent = state["recent"] || []
|
|
202
|
+
lines = []
|
|
203
|
+
|
|
204
|
+
# Title line — agent emojis + deploy dots
|
|
205
|
+
parts = []
|
|
206
|
+
parts << sessions.map { |s| AGENTS.dig(s["agent"]&.downcase, :emoji) || DEFAULT_EMOJI }.join(" ") if sessions.any?
|
|
207
|
+
parts << deployments.map { |d| deploy_dot(d) }.join if deployments&.any?
|
|
208
|
+
title = parts.any? ? parts.join(" ") : "💤"
|
|
209
|
+
lines << title
|
|
210
|
+
lines << "---"
|
|
211
|
+
|
|
212
|
+
# Active sessions
|
|
213
|
+
if sessions.any?
|
|
214
|
+
lines << "Active | size=12"
|
|
215
|
+
sessions.each do |s|
|
|
216
|
+
agent_key = (s["agent"] || "").downcase
|
|
217
|
+
emoji = AGENTS.dig(agent_key, :emoji) || DEFAULT_EMOJI
|
|
218
|
+
color = AGENTS.dig(agent_key, :color)
|
|
219
|
+
color_str = color ? " color=#{hex_color(color)}" : ""
|
|
220
|
+
context = format_context(s["card_key"])
|
|
221
|
+
elapsed = format_elapsed(s["elapsed_seconds"] || 0)
|
|
222
|
+
lines << "#{emoji} #{s["agent"]}: #{context} (#{elapsed}) |#{color_str}"
|
|
223
|
+
|
|
224
|
+
tail_log(s["log_file"]).each do |line|
|
|
225
|
+
lines << "-- #{format_log_line(line)} | font=#{LOG_FONT} size=#{LOG_SIZE}"
|
|
226
|
+
end
|
|
227
|
+
lines << "-- ---" if s["log_file"]
|
|
228
|
+
lines << "-- Tail Log#{log_action(s["log_file"])}" if s["log_file"]
|
|
229
|
+
lines << "-- View Full Log#{full_log_action(s["log_file"])}" if s["log_file"]
|
|
230
|
+
lines << "-- Open Prompt#{prompt_action(s["card_key"])}" unless prompt_url(s["card_key"]).nil?
|
|
231
|
+
wt = worktree_path(s["log_file"], s["card_key"])
|
|
232
|
+
lines << "-- Open Worktree#{worktree_action(s["log_file"], s["card_key"])}" if wt
|
|
233
|
+
end
|
|
234
|
+
else
|
|
235
|
+
lines << "No active sessions | size=12"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Recent completed sessions
|
|
239
|
+
if recent.any?
|
|
240
|
+
lines << "---"
|
|
241
|
+
lines << "Recent | size=12"
|
|
242
|
+
recent.each do |s|
|
|
243
|
+
agent_key = (s["agent"] || "").downcase
|
|
244
|
+
emoji = AGENTS.dig(agent_key, :emoji) || DEFAULT_EMOJI
|
|
245
|
+
context = format_context(s["card_key"])
|
|
246
|
+
ago = time_ago(s["finished_at"]) || "?"
|
|
247
|
+
lines << "#{emoji} #{s["agent"]}: #{context} — #{ago}"
|
|
248
|
+
|
|
249
|
+
tail_log(s["log_file"]).each do |line|
|
|
250
|
+
lines << "-- #{format_log_line(line)} | font=#{LOG_FONT} size=#{LOG_SIZE}"
|
|
251
|
+
end
|
|
252
|
+
lines << "-- ---" if s["log_file"]
|
|
253
|
+
lines << "-- Tail Log#{log_action(s["log_file"])}" if s["log_file"]
|
|
254
|
+
lines << "-- View Full Log#{full_log_action(s["log_file"])}" if s["log_file"]
|
|
255
|
+
lines << "-- Open Prompt#{prompt_action(s["card_key"])}" unless prompt_url(s["card_key"]).nil?
|
|
256
|
+
wt = worktree_path(s["log_file"], s["card_key"])
|
|
257
|
+
lines << "-- Open Worktree#{worktree_action(s["log_file"], s["card_key"])}" if wt
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Deployments
|
|
262
|
+
if deployments&.any?
|
|
263
|
+
lines << "---"
|
|
264
|
+
lines << "Deployments | size=12"
|
|
265
|
+
deployments.each do |d|
|
|
266
|
+
label = d["label"] || d["env"]
|
|
267
|
+
env = d["env"]
|
|
268
|
+
dot = deploy_dot(d)
|
|
269
|
+
if d["status"] == "occupied"
|
|
270
|
+
card = d["card_number"] ? "##{d["card_number"]}" : d["branch"] || "unknown"
|
|
271
|
+
ago = time_ago(d["deployed_at"])
|
|
272
|
+
status_label = case d["last_deploy_status"]
|
|
273
|
+
when "deploying" then " — deploying…"
|
|
274
|
+
when "failed" then " — FAILED"
|
|
275
|
+
else ""
|
|
276
|
+
end
|
|
277
|
+
line = "#{dot} #{label}: #{card}#{status_label}#{" (#{ago})" if ago}"
|
|
278
|
+
url = d["url"]
|
|
279
|
+
lines << (url ? "#{line} | href=#{url}" : line)
|
|
280
|
+
else
|
|
281
|
+
ago = time_ago(d["cleared_at"])
|
|
282
|
+
last = d["last_card"] ? " (was ##{d["last_card"]})" : ""
|
|
283
|
+
lines << "#{dot} #{label}: Available#{" #{ago}" if ago}#{last}"
|
|
284
|
+
end
|
|
285
|
+
lines << "-- Deploy to #{label} | shell=#{DEPLOY_SCRIPT_PATH} param1=#{env} terminal=false refresh=true"
|
|
286
|
+
lines << "-- Open #{label} | shell=#{OPEN_SCRIPT} param1=#{d["url"]} terminal=false refresh=false" if d["status"] == "occupied" && d["url"]
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
lines << "---"
|
|
291
|
+
lines << "Refresh | refresh=true"
|
|
292
|
+
lines.join("\n")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
puts generate_output
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Opens a URL in browser, a directory in Finder, or a file in the default viewer.
|
|
3
|
+
# Used by the SwiftBar menubar plugin for prompt links, worktrees, and full log views.
|
|
4
|
+
|
|
5
|
+
arg="$1"
|
|
6
|
+
|
|
7
|
+
if [[ "$arg" == https://discord.com/* ]]; then
|
|
8
|
+
open "discord://${arg#https://}"
|
|
9
|
+
elif [[ "$arg" == http* ]]; then
|
|
10
|
+
open "$arg"
|
|
11
|
+
elif [[ -d "$arg" ]]; then
|
|
12
|
+
open -a Kiro "$arg"
|
|
13
|
+
elif [[ -f "$arg" ]]; then
|
|
14
|
+
open -a "Console" "$arg" 2>/dev/null || open "$arg"
|
|
15
|
+
fi
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# One-time setup script to install Brainiac menubar plugin into xbar or SwiftBar
|
|
5
|
+
# Run this once — the plugin will then auto-refresh on its configured interval
|
|
6
|
+
|
|
7
|
+
PLUGIN_APPS = [
|
|
8
|
+
{
|
|
9
|
+
name: "SwiftBar",
|
|
10
|
+
plugin_dir: File.expand_path("~/Library/Application Support/SwiftBar/Plugins"),
|
|
11
|
+
app_path: "/Applications/SwiftBar.app"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "xbar",
|
|
15
|
+
plugin_dir: File.expand_path("~/Library/Application Support/xbar/plugins"),
|
|
16
|
+
app_path: "/Applications/xbar.app"
|
|
17
|
+
}
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
SYMLINK_NAME = "brainiac.2s.rb"
|
|
21
|
+
SOURCE_PATH = File.join(File.dirname(File.expand_path(__FILE__)), "menubar.rb")
|
|
22
|
+
|
|
23
|
+
def detect_plugin_app
|
|
24
|
+
PLUGIN_APPS.each do |app|
|
|
25
|
+
return { name: app[:name], plugin_dir: app[:plugin_dir] } if Dir.exist?(app[:plugin_dir]) || File.exist?(app[:app_path])
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install_plugin(plugin_dir, source_path)
|
|
31
|
+
FileUtils.mkdir_p(plugin_dir)
|
|
32
|
+
link_path = File.join(plugin_dir, SYMLINK_NAME)
|
|
33
|
+
|
|
34
|
+
# Remove existing symlink/file if present
|
|
35
|
+
File.delete(link_path) if File.exist?(link_path) || File.symlink?(link_path)
|
|
36
|
+
|
|
37
|
+
File.symlink(source_path, link_path)
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
warn "✗ Failed to create symlink: #{e.message}"
|
|
40
|
+
warn " Source: #{source_path}"
|
|
41
|
+
warn " Target: #{link_path}"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def verify_executable!(path) # rubocop:disable Naming/PredicateMethod
|
|
46
|
+
unless File.executable?(path)
|
|
47
|
+
File.chmod(0o755, path)
|
|
48
|
+
warn " Fixed executable permission on #{path}"
|
|
49
|
+
end
|
|
50
|
+
File.executable?(path)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# --- Main ---
|
|
54
|
+
|
|
55
|
+
require "fileutils"
|
|
56
|
+
|
|
57
|
+
app = detect_plugin_app
|
|
58
|
+
|
|
59
|
+
unless app
|
|
60
|
+
puts "No xbar or SwiftBar installation detected."
|
|
61
|
+
puts ""
|
|
62
|
+
puts "Install one of the following to use the Brainiac menu bar plugin:"
|
|
63
|
+
puts " • xbar: https://xbarapp.com"
|
|
64
|
+
puts " • SwiftBar: https://github.com/swiftbar/SwiftBar"
|
|
65
|
+
puts ""
|
|
66
|
+
puts "After installing, re-run this script:"
|
|
67
|
+
puts " ruby #{__FILE__}"
|
|
68
|
+
exit 0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
puts "Detected #{app[:name]}"
|
|
72
|
+
install_plugin(app[:plugin_dir], SOURCE_PATH)
|
|
73
|
+
verify_executable!(SOURCE_PATH)
|
|
74
|
+
|
|
75
|
+
link_path = File.join(app[:plugin_dir], SYMLINK_NAME)
|
|
76
|
+
puts "✓ Installed Brainiac plugin into #{app[:name]}"
|
|
77
|
+
puts " Symlink: #{link_path} → #{SOURCE_PATH}"
|
|
78
|
+
puts " Refresh interval: 2s"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# One-time setup: replaces the single brainiac-deployments module
|
|
5
|
+
# with per-environment modules so each dot gets its own border/click.
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
|
|
10
|
+
WAYBAR_CONFIG = File.expand_path("~/.config/waybar/config.jsonc")
|
|
11
|
+
DEPLOY_SCRIPT = File.expand_path("~/.brainiac/bin/waybar-deploy-env")
|
|
12
|
+
DEPLOYMENTS_CONFIG = File.expand_path("~/.brainiac/deployments.json")
|
|
13
|
+
WAYBAR_STYLE = File.expand_path("~/.config/waybar/style.css")
|
|
14
|
+
|
|
15
|
+
# Create wrapper script that resolves from server.root
|
|
16
|
+
wrapper_dir = File.expand_path("~/.brainiac/bin")
|
|
17
|
+
FileUtils.mkdir_p(wrapper_dir)
|
|
18
|
+
File.write(DEPLOY_SCRIPT, <<~SCRIPT)
|
|
19
|
+
#!/usr/bin/env ruby
|
|
20
|
+
root_file = File.expand_path("~/.brainiac/server.root")
|
|
21
|
+
if File.exist?(root_file)
|
|
22
|
+
server_root = File.read(root_file).strip
|
|
23
|
+
script = File.join(server_root, "monitor", "waybar-deploy-env.rb")
|
|
24
|
+
if File.exist?(script)
|
|
25
|
+
ARGV.unshift if ARGV.empty?
|
|
26
|
+
load script
|
|
27
|
+
exit
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
require "json"
|
|
31
|
+
puts({ text: "", tooltip: "Brainiac server root not found", class: "error" }.to_json)
|
|
32
|
+
SCRIPT
|
|
33
|
+
File.chmod(0o755, DEPLOY_SCRIPT)
|
|
34
|
+
|
|
35
|
+
def load_config
|
|
36
|
+
content = File.read(WAYBAR_CONFIG)
|
|
37
|
+
json_content = content.lines.reject { |line| line.strip.start_with?("//") }.join
|
|
38
|
+
JSON.parse(json_content)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def save_config(config)
|
|
42
|
+
File.write(WAYBAR_CONFIG, JSON.pretty_generate(config))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
deployments = JSON.parse(File.read(DEPLOYMENTS_CONFIG))
|
|
46
|
+
envs = deployments["environments"].keys
|
|
47
|
+
|
|
48
|
+
config = load_config
|
|
49
|
+
|
|
50
|
+
# Remove old single deployments module from all bar positions
|
|
51
|
+
%w[modules-left modules-center modules-right].each do |pos|
|
|
52
|
+
next unless config[pos]
|
|
53
|
+
|
|
54
|
+
config[pos].reject! { |m| m.to_s.include?("brainiac-deploy") }
|
|
55
|
+
end
|
|
56
|
+
config.delete("custom/brainiac-deployments")
|
|
57
|
+
|
|
58
|
+
# Remove any existing per-env modules
|
|
59
|
+
config.each_key do |key|
|
|
60
|
+
config.delete(key) if key.start_with?("custom/brainiac-deploy-")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Insert per-env modules into modules-center, before custom/brainiac
|
|
64
|
+
center = config["modules-center"] || []
|
|
65
|
+
zc_idx = center.index("custom/brainiac") || center.length
|
|
66
|
+
envs.each_with_index do |env, i|
|
|
67
|
+
mod_name = "custom/brainiac-deploy-#{env}"
|
|
68
|
+
center.insert(zc_idx + i, mod_name) unless center.include?(mod_name)
|
|
69
|
+
end
|
|
70
|
+
config["modules-center"] = center
|
|
71
|
+
|
|
72
|
+
# Add module configs for each env
|
|
73
|
+
envs.each do |env|
|
|
74
|
+
mod_name = "custom/brainiac-deploy-#{env}"
|
|
75
|
+
config[mod_name] = {
|
|
76
|
+
"exec" => "#{DEPLOY_SCRIPT} #{env}",
|
|
77
|
+
"return-type" => "json",
|
|
78
|
+
"interval" => 30,
|
|
79
|
+
"format" => "{}",
|
|
80
|
+
"tooltip" => true,
|
|
81
|
+
"escape" => false,
|
|
82
|
+
"on-click" => "#{DEPLOY_SCRIPT} #{env} --click",
|
|
83
|
+
"on-click-right" => "#{DEPLOY_SCRIPT} #{env} --deploy"
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
save_config(config)
|
|
88
|
+
puts "✓ Added per-environment deploy modules: #{envs.map { |e| "custom/brainiac-deploy-#{e}" }.join(", ")}"
|
|
89
|
+
|
|
90
|
+
# Update CSS — remove old single-module styles, add per-env styles
|
|
91
|
+
style = File.read(WAYBAR_STYLE)
|
|
92
|
+
|
|
93
|
+
# Remove old block
|
|
94
|
+
style.gsub!(%r{/\* Brainiac deployment environment dots \*/.*?(?=\n\n|\n/\*|\z)}m, "")
|
|
95
|
+
style.gsub!(/\n*#custom-brainiac-deployments[^{]*\{[^}]*\}\n*/m, "")
|
|
96
|
+
|
|
97
|
+
# Add new per-env styles
|
|
98
|
+
unless style.include?("#custom-brainiac-deploy-")
|
|
99
|
+
css = <<~CSS
|
|
100
|
+
|
|
101
|
+
/* Brainiac per-environment deploy dots */
|
|
102
|
+
[id^="custom-brainiac-deploy-"] {
|
|
103
|
+
font-size: 28px;
|
|
104
|
+
padding: 0 6px;
|
|
105
|
+
border-radius: 8px;
|
|
106
|
+
border: 2px solid transparent;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
[id^="custom-brainiac-deploy-"].deploy-recent {
|
|
110
|
+
border: 2px solid #4488ff;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[id^="custom-brainiac-deploy-"].deploy-failed {
|
|
114
|
+
border: 2px solid #ff4444;
|
|
115
|
+
}
|
|
116
|
+
CSS
|
|
117
|
+
File.write(WAYBAR_STYLE, "#{style.strip}\n#{css}")
|
|
118
|
+
puts "✓ Updated waybar CSS with per-environment border styles"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
puts "✓ Restart waybar to apply: omarchy restart waybar"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# One-time setup: adds Brainiac deployments module to waybar config
|
|
5
|
+
# and repositions the agent session module with more breathing room.
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
WAYBAR_CONFIG = File.expand_path("~/.config/waybar/config.jsonc")
|
|
10
|
+
DEPLOY_SCRIPT = File.expand_path("~/Code/brainiac/monitor/waybar-deployments.rb")
|
|
11
|
+
WAYBAR_STYLE = File.expand_path("~/.config/waybar/style.css")
|
|
12
|
+
|
|
13
|
+
def load_config
|
|
14
|
+
content = File.read(WAYBAR_CONFIG)
|
|
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
|
+
config = load_config
|
|
24
|
+
|
|
25
|
+
# Remove any existing deployment module
|
|
26
|
+
config["modules-center"]&.reject! { |m| m.to_s.include?("brainiac-deploy") }
|
|
27
|
+
config["modules-right"]&.reject! { |m| m.to_s.include?("brainiac-deploy") }
|
|
28
|
+
config.delete("custom/brainiac-deployments")
|
|
29
|
+
|
|
30
|
+
# Move agent session module from modules-right to modules-center (after indicators)
|
|
31
|
+
if config["modules-right"]&.delete("custom/brainiac")
|
|
32
|
+
config["modules-center"] ||= []
|
|
33
|
+
config["modules-center"] << "custom/brainiac" unless config["modules-center"].include?("custom/brainiac")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Add deployments module right before agent sessions in modules-center
|
|
37
|
+
center = config["modules-center"] || []
|
|
38
|
+
zc_idx = center.index("custom/brainiac")
|
|
39
|
+
if zc_idx
|
|
40
|
+
center.insert(zc_idx, "custom/brainiac-deployments") unless center.include?("custom/brainiac-deployments")
|
|
41
|
+
else
|
|
42
|
+
center << "custom/brainiac-deployments" unless center.include?("custom/brainiac-deployments")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Add module config
|
|
46
|
+
config["custom/brainiac-deployments"] = {
|
|
47
|
+
"exec" => DEPLOY_SCRIPT,
|
|
48
|
+
"return-type" => "json",
|
|
49
|
+
"interval" => 30,
|
|
50
|
+
"format" => "{}",
|
|
51
|
+
"tooltip" => true,
|
|
52
|
+
"format-alt" => "{}",
|
|
53
|
+
"escape" => false,
|
|
54
|
+
"on-click" => "#{DEPLOY_SCRIPT} --click",
|
|
55
|
+
"on-click-right" => "#{DEPLOY_SCRIPT} --deploy"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
save_config(config)
|
|
59
|
+
|
|
60
|
+
# Add CSS for the deployments module
|
|
61
|
+
style = File.read(WAYBAR_STYLE)
|
|
62
|
+
unless style.include?("#custom-brainiac-deployments")
|
|
63
|
+
css = <<~CSS
|
|
64
|
+
|
|
65
|
+
/* Brainiac deployment environment dots */
|
|
66
|
+
#custom-brainiac-deployments {
|
|
67
|
+
margin-left: 100px;
|
|
68
|
+
margin-right: 40px;
|
|
69
|
+
font-size: 14px;
|
|
70
|
+
}
|
|
71
|
+
CSS
|
|
72
|
+
File.write(WAYBAR_STYLE, style + css)
|
|
73
|
+
puts "✓ Added deployment styles to waybar CSS"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Add padding-right to agent sessions module
|
|
77
|
+
unless style.include?("padding-right") && style.include?("#custom-brainiac")
|
|
78
|
+
updated_style = File.read(WAYBAR_STYLE)
|
|
79
|
+
if updated_style.include?("#custom-brainiac {")
|
|
80
|
+
updated_style.sub!(/(#custom-brainiac\s*\{[^}]*)(\})/) do
|
|
81
|
+
block = Regexp.last_match(1)
|
|
82
|
+
close = Regexp.last_match(2)
|
|
83
|
+
if block.include?("padding-right")
|
|
84
|
+
"#{block}#{close}"
|
|
85
|
+
else
|
|
86
|
+
"#{block}\n padding-right: 100px;\n#{close}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
File.write(WAYBAR_STYLE, updated_style)
|
|
90
|
+
puts "✓ Added padding-right to agent session module"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
puts "✓ Deployments module added to waybar config"
|
|
95
|
+
puts " Positioned: [deploy-dots] [agent-sessions] in center bar"
|
|
96
|
+
puts " Restart waybar to apply: omarchy restart waybar"
|