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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +385 -19
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +15 -38
  7. data/lib/brainiac/config.rb +40 -11
  8. data/lib/brainiac/handlers/discord/api.rb +196 -0
  9. data/lib/brainiac/handlers/discord/config.rb +134 -0
  10. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  11. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  12. data/lib/brainiac/handlers/discord/message.rb +933 -0
  13. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  14. data/lib/brainiac/handlers/discord.rb +14 -1892
  15. data/lib/brainiac/handlers/fizzy/assignment.rb +125 -0
  16. data/lib/brainiac/handlers/fizzy/comments.rb +730 -0
  17. data/lib/brainiac/handlers/fizzy/dedup.rb +74 -0
  18. data/lib/brainiac/handlers/fizzy/deploy.rb +152 -0
  19. data/lib/brainiac/{deployments.rb → handlers/fizzy/deployments.rb} +4 -2
  20. data/lib/brainiac/handlers/fizzy.rb +12 -1290
  21. data/lib/brainiac/handlers/github.rb +149 -212
  22. data/lib/brainiac/handlers/shared/git.rb +203 -0
  23. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  24. data/lib/brainiac/handlers/zoho.rb +79 -76
  25. data/lib/brainiac/helpers.rb +2 -121
  26. data/lib/brainiac/planning.rb +2 -2
  27. data/lib/brainiac/plugins.rb +129 -0
  28. data/lib/brainiac/restart.rb +94 -0
  29. data/lib/brainiac/routes/api.rb +427 -0
  30. data/lib/brainiac/version.rb +1 -1
  31. data/lib/brainiac/zoho_mail_api.rb +2 -1
  32. data/lib/brainiac.rb +7 -0
  33. data/monitor/daemon.rb +4 -27
  34. data/monitor/shared.rb +247 -0
  35. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +14 -86
  36. data/monitor/waybar/setup.rb +232 -0
  37. data/monitor/waybar/status.rb +51 -0
  38. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  39. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  40. data/monitor/xbar/plugin.rb +155 -0
  41. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  42. data/receiver.rb +91 -450
  43. data/templates/brainiac.json.example +9 -0
  44. data/templates/cli-providers/kiro.json.example +8 -2
  45. data/templates/plugins.json.example +3 -0
  46. metadata +30 -20
  47. data/lib/user_registry.rb +0 -159
  48. data/monitor/menubar.rb +0 -295
  49. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  50. data/monitor/setup-waybar-deployments.rb +0 -96
  51. data/monitor/setup-waybar-module.rb +0 -113
  52. data/monitor/setup-xbar-plugin.rb +0 -35
  53. data/monitor/view-logs.rb +0 -119
  54. data/monitor/waybar-config-updater.rb +0 -56
  55. data/monitor/waybar-deployments.rb +0 -239
  56. data/monitor/waybar.rb +0 -146
  57. data/monitor/xbar.3s.rb +0 -179
  58. /data/lib/brainiac/{card_index.rb → handlers/fizzy/card_index.rb} +0 -0
  59. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  60. /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
@@ -1,96 +0,0 @@
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"
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # One-time setup script to add Brainiac module to waybar config
5
- # Run this once, then the module will update dynamically without config rewrites
6
-
7
- require "json"
8
- require "fileutils"
9
-
10
- WAYBAR_CONFIG = File.expand_path("~/.config/waybar/config.jsonc")
11
- WAYBAR_SCRIPT = File.expand_path("~/.brainiac/bin/waybar-status")
12
-
13
- # Create a wrapper script that resolves the running server's waybar.rb dynamically
14
- wrapper_dir = File.expand_path("~/.brainiac/bin")
15
- FileUtils.mkdir_p(wrapper_dir)
16
- wrapper_path = File.join(wrapper_dir, "waybar-status")
17
- File.write(wrapper_path, <<~SCRIPT)
18
- #!/usr/bin/env ruby
19
- # Resolves the running Brainiac server's waybar module dynamically.
20
- # This allows worktrees / branches to work without reconfiguring waybar.
21
-
22
- root_file = File.expand_path("~/.brainiac/server.root")
23
- if File.exist?(root_file)
24
- server_root = File.read(root_file).strip
25
- waybar_script = File.join(server_root, "monitor", "waybar.rb")
26
- if File.exist?(waybar_script)
27
- load waybar_script
28
- exit
29
- end
30
- end
31
-
32
- # Fallback: no server root known, try the API directly
33
- require "json"
34
- require "net/http"
35
-
36
- begin
37
- uri = URI("http://localhost:4567/api/status")
38
- response = Net::HTTP.get_response(uri)
39
- if response.is_a?(Net::HTTPSuccess)
40
- data = JSON.parse(response.body)
41
- sessions = data["sessions"] || []
42
- if sessions.empty?
43
- puts({ text: "💤", tooltip: "No active agent sessions", class: "idle" }.to_json)
44
- else
45
- puts({ text: "🟢 \#{sessions.size}", tooltip: sessions.map { |s| s["agent"] }.join(", "), class: "working" }.to_json)
46
- end
47
- else
48
- puts({ text: "⚠️", tooltip: "Brainiac Error: HTTP \#{response.code}", class: "error" }.to_json)
49
- end
50
- rescue StandardError => e
51
- puts({ text: "⚠️", tooltip: "Brainiac Error: \#{e.message}", class: "error" }.to_json)
52
- end
53
- SCRIPT
54
- File.chmod(0o755, wrapper_path)
55
-
56
- def load_config
57
- content = File.read(WAYBAR_CONFIG)
58
- # Strip comments for JSON parsing
59
- json_content = content.lines.reject { |line| line.strip.start_with?("//") }.join
60
- JSON.parse(json_content)
61
- end
62
-
63
- def save_config(config)
64
- File.write(WAYBAR_CONFIG, JSON.pretty_generate(config))
65
- end
66
-
67
- # Load current config
68
- config = load_config
69
-
70
- # Remove old brainiac modules if they exist (from all module arrays)
71
- %w[modules-left modules-center modules-right].each do |section|
72
- next unless config[section].is_a?(Array)
73
-
74
- config[section].reject! { |m| ["custom/brainiac", "group/brainiac-agents"].include?(m.to_s) }
75
- end
76
- config.each_key do |key|
77
- config.delete(key) if ["custom/brainiac", "group/brainiac-agents"].include?(key.to_s)
78
- end
79
-
80
- # Add single dynamic module at the end of modules-center (after deploy envs)
81
- config["modules-center"] ||= []
82
- config["modules-center"].push("custom/brainiac")
83
-
84
- # Add module config
85
- config["custom/brainiac"] = {
86
- "exec" => WAYBAR_SCRIPT,
87
- "return-type" => "json",
88
- "interval" => 3,
89
- "format" => "{}",
90
- "tooltip" => true,
91
- "on-click" => File.expand_path("~/.brainiac/bin/waybar-logs").to_s
92
- }
93
-
94
- # Create on-click wrapper too
95
- logs_wrapper = File.expand_path("~/.brainiac/bin/waybar-logs")
96
- File.write(logs_wrapper, <<~SCRIPT)
97
- #!/usr/bin/env ruby
98
- root_file = File.expand_path("~/.brainiac/server.root")
99
- if File.exist?(root_file)
100
- server_root = File.read(root_file).strip
101
- script = File.join(server_root, "monitor", "view-logs-rofi.rb")
102
- exec("ruby", script) if File.exist?(script)
103
- end
104
- warn "Brainiac server root not found"
105
- SCRIPT
106
- File.chmod(0o755, logs_wrapper)
107
-
108
- # Save updated config
109
- save_config(config)
110
-
111
- puts "✓ Brainiac module added to waybar config"
112
- puts " Module will update every 3 seconds without config rewrites"
113
- puts " Restart waybar to apply: omarchy restart waybar"
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # One-time setup: symlinks the Brainiac xbar plugin into xbar's plugin directory
5
- # Run this once on macOS after installing xbar
6
-
7
- require "fileutils"
8
-
9
- XBAR_PLUGIN_DIR = File.expand_path("~/Library/Application Support/xbar/plugins")
10
- PLUGIN_SOURCE = File.expand_path("xbar.3s.rb", __dir__)
11
- PLUGIN_DEST = File.join(XBAR_PLUGIN_DIR, "brainiac.3s.rb")
12
-
13
- unless RUBY_PLATFORM.match?(/darwin/i)
14
- puts "⚠ This script is for macOS only (xbar doesn't run on Linux)"
15
- exit 1
16
- end
17
-
18
- unless File.directory?(XBAR_PLUGIN_DIR)
19
- puts "⚠ xbar plugin directory not found: #{XBAR_PLUGIN_DIR}"
20
- puts " Install xbar first: https://xbarapp.com"
21
- exit 1
22
- end
23
-
24
- if File.exist?(PLUGIN_DEST)
25
- puts "Removing existing plugin at #{PLUGIN_DEST}"
26
- File.delete(PLUGIN_DEST)
27
- end
28
-
29
- File.symlink(PLUGIN_SOURCE, PLUGIN_DEST)
30
- File.chmod(0o755, PLUGIN_SOURCE)
31
-
32
- puts "✓ Brainiac xbar plugin installed"
33
- puts " #{PLUGIN_SOURCE} → #{PLUGIN_DEST}"
34
- puts " Refresh interval: 3 seconds"
35
- puts " Restart xbar to activate"
data/monitor/view-logs.rb DELETED
@@ -1,119 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Brainiac Log Viewer
5
- # Shows a rofi menu to select which agent log to tail
6
-
7
- require "json"
8
- require "net/http"
9
- require "socket"
10
-
11
- SOCKET_PATH = "/tmp/brainiac-monitor.sock"
12
- API_URL = "http://localhost:4567/api/status"
13
- CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
14
-
15
- # Load agent configuration from JSON
16
- def load_agent_config
17
- config = JSON.parse(File.read(CONFIG_PATH))
18
- agents = {}
19
- config["agents"].each do |agent|
20
- agents[agent["name"].downcase] = agent["emoji"]
21
- end
22
- default_emoji = config["default_emoji"] || "❓"
23
- [agents, default_emoji]
24
- rescue StandardError => e
25
- warn "Failed to load waybar.json: #{e.message}"
26
- [{}, "❓"]
27
- end
28
-
29
- AGENTS, DEFAULT_EMOJI = load_agent_config
30
-
31
- def fetch_state_from_socket
32
- socket = UNIXSocket.new(SOCKET_PATH)
33
- data = socket.read
34
- socket.close
35
- JSON.parse(data)
36
- end
37
-
38
- def fetch_state_from_api
39
- uri = URI(API_URL)
40
- response = Net::HTTP.get_response(uri)
41
- return nil unless response.is_a?(Net::HTTPSuccess)
42
-
43
- JSON.parse(response.body)
44
- end
45
-
46
- def fetch_state
47
- # Prefer socket (daemon mode) — faster, no HTTP overhead
48
- fetch_state_from_socket
49
- rescue Errno::ENOENT, Errno::ECONNREFUSED
50
- # Daemon not running — fall back to direct API call
51
- data = fetch_state_from_api
52
- unless data
53
- puts "Error: Server not reachable"
54
- exit 1
55
- end
56
- data
57
- rescue StandardError => e
58
- puts "Error: #{e.message}"
59
- exit 1
60
- end
61
-
62
- def format_elapsed(seconds)
63
- return "#{seconds}s" if seconds < 60
64
-
65
- minutes = seconds / 60
66
- return "#{minutes}m" if minutes < 60
67
-
68
- hours = minutes / 60
69
- "#{hours}h"
70
- end
71
-
72
- state = fetch_state
73
- sessions = state["sessions"] || []
74
-
75
- if sessions.empty?
76
- system("notify-send", "Brainiac", "No active agent sessions")
77
- exit 0
78
- end
79
-
80
- # If only one session, open it directly
81
- if sessions.size == 1
82
- log_file = sessions[0]["log_file"]
83
- exec("alacritty", "-e", "tail", "-f", log_file) if log_file
84
- exit 0
85
- end
86
-
87
- # Multiple sessions: use fzf if available, otherwise just open the first one
88
- if system("which fzf > /dev/null 2>&1")
89
- # Build fzf menu
90
- options = sessions.map do |s|
91
- agent = s["agent"]
92
- elapsed = format_elapsed(s["elapsed_seconds"])
93
-
94
- card_key = s["card_key"]
95
- context = if card_key.start_with?("discord-")
96
- "Discord chat"
97
- elsif card_key.start_with?("card-")
98
- card_key.split("-")[1]
99
- else
100
- card_key
101
- end
102
-
103
- emoji = AGENTS[agent.downcase] || DEFAULT_EMOJI
104
-
105
- "#{emoji} #{agent}: #{context} (#{elapsed})|#{s["log_file"]}"
106
- end
107
-
108
- menu_text = options.join("\n")
109
- selected = `echo "#{menu_text}" | fzf --prompt="Agent Logs: "`.strip
110
-
111
- unless selected.empty?
112
- log_file = selected.split("|").last
113
- exec("alacritty", "-e", "tail", "-f", log_file)
114
- end
115
- else
116
- # No menu system, just open the first log
117
- log_file = sessions[0]["log_file"]
118
- exec("alacritty", "-e", "tail", "-f", log_file) if log_file
119
- end
@@ -1,56 +0,0 @@
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")
@@ -1,239 +0,0 @@
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