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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +2 -0
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +126 -0
  6. data/README.md +1166 -0
  7. data/Rakefile +12 -0
  8. data/bin/brainiac +1521 -0
  9. data/brainiac.gemspec +30 -0
  10. data/certs/stowzilla.pem +26 -0
  11. data/docs/waybar-config.md +96 -0
  12. data/lib/brainiac/agents.rb +203 -0
  13. data/lib/brainiac/brain.rb +197 -0
  14. data/lib/brainiac/card_index.rb +389 -0
  15. data/lib/brainiac/config.rb +263 -0
  16. data/lib/brainiac/cron.rb +629 -0
  17. data/lib/brainiac/deployments.rb +258 -0
  18. data/lib/brainiac/handlers/discord.rb +1643 -0
  19. data/lib/brainiac/handlers/fizzy.rb +1249 -0
  20. data/lib/brainiac/handlers/github.rb +598 -0
  21. data/lib/brainiac/handlers/zoho.rb +487 -0
  22. data/lib/brainiac/helpers.rb +760 -0
  23. data/lib/brainiac/planning.rb +237 -0
  24. data/lib/brainiac/prompts.rb +620 -0
  25. data/lib/brainiac/sessions.rb +282 -0
  26. data/lib/brainiac/skills.rb +276 -0
  27. data/lib/brainiac/users.rb +76 -0
  28. data/lib/brainiac/version.rb +6 -0
  29. data/lib/brainiac/zoho_mail_api.rb +109 -0
  30. data/lib/brainiac.rb +10 -0
  31. data/lib/user_registry.rb +159 -0
  32. data/monitor/daemon.rb +99 -0
  33. data/monitor/deploy-env-macos.rb +131 -0
  34. data/monitor/menubar.rb +295 -0
  35. data/monitor/open-action.sh +15 -0
  36. data/monitor/setup-menubar.rb +78 -0
  37. data/monitor/setup-waybar-deploy-envs.rb +121 -0
  38. data/monitor/setup-waybar-deployments.rb +96 -0
  39. data/monitor/setup-waybar-module.rb +113 -0
  40. data/monitor/setup-xbar-plugin.rb +35 -0
  41. data/monitor/view-logs-macos.rb +210 -0
  42. data/monitor/view-logs-rofi.rb +194 -0
  43. data/monitor/view-logs.rb +119 -0
  44. data/monitor/waybar-config-updater.rb +56 -0
  45. data/monitor/waybar-deploy-env.rb +206 -0
  46. data/monitor/waybar-deployments.rb +239 -0
  47. data/monitor/waybar.rb +146 -0
  48. data/monitor/xbar.3s.rb +179 -0
  49. data/receiver.rb +956 -0
  50. data/templates/agents.json.example +10 -0
  51. data/templates/discord.json.example +17 -0
  52. data/templates/fizzy.json.example +24 -0
  53. data/templates/github.json.example +4 -0
  54. data/templates/testflight.json.example +8 -0
  55. data/templates/users.json.example +121 -0
  56. data/templates/zoho.json.example +27 -0
  57. data/views/dashboard.erb +437 -0
  58. data.tar.gz.sig +0 -0
  59. metadata +235 -0
  60. metadata.gz.sig +0 -0
@@ -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"