zillacore 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 +0 -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/zillacore +1521 -0
- data/certs/stowzilla.pem +26 -0
- data/docs/waybar-config.md +96 -0
- data/lib/user_registry.rb +159 -0
- data/lib/zillacore/agents.rb +203 -0
- data/lib/zillacore/brain.rb +197 -0
- data/lib/zillacore/card_index.rb +389 -0
- data/lib/zillacore/config.rb +263 -0
- data/lib/zillacore/cron.rb +629 -0
- data/lib/zillacore/deployments.rb +258 -0
- data/lib/zillacore/handlers/discord.rb +1643 -0
- data/lib/zillacore/handlers/fizzy.rb +1249 -0
- data/lib/zillacore/handlers/github.rb +598 -0
- data/lib/zillacore/handlers/zoho.rb +487 -0
- data/lib/zillacore/helpers.rb +760 -0
- data/lib/zillacore/planning.rb +237 -0
- data/lib/zillacore/prompts.rb +620 -0
- data/lib/zillacore/sessions.rb +282 -0
- data/lib/zillacore/skills.rb +276 -0
- data/lib/zillacore/users.rb +76 -0
- data/lib/zillacore/version.rb +6 -0
- data/lib/zillacore/zoho_mail_api.rb +109 -0
- data/lib/zillacore.rb +10 -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/zillacore.gemspec +30 -0
- data.tar.gz.sig +2 -0
- metadata +235 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ZillaCore Log Viewer (Rofi version)
|
|
5
|
+
# Shows a rofi menu to select which agent log to tail
|
|
6
|
+
# Non-blocking, safe for waybar on-click
|
|
7
|
+
|
|
8
|
+
require "json"
|
|
9
|
+
require "net/http"
|
|
10
|
+
require "socket"
|
|
11
|
+
|
|
12
|
+
SOCKET_PATH = "/tmp/zillacore-monitor.sock"
|
|
13
|
+
API_URL = "http://localhost:4567/api/status"
|
|
14
|
+
CONFIG_PATH = File.expand_path("~/.zillacore/waybar.json")
|
|
15
|
+
|
|
16
|
+
# Load agent configuration from JSON
|
|
17
|
+
def load_agent_config
|
|
18
|
+
config = JSON.parse(File.read(CONFIG_PATH))
|
|
19
|
+
agents = {}
|
|
20
|
+
config["agents"].each do |agent|
|
|
21
|
+
agents[agent["name"].downcase] = agent["emoji"]
|
|
22
|
+
end
|
|
23
|
+
default_emoji = config["default_emoji"] || "❓"
|
|
24
|
+
[agents, default_emoji]
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
warn "Failed to load waybar.json: #{e.message}"
|
|
27
|
+
[{}, "❓"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
AGENTS, DEFAULT_EMOJI = load_agent_config
|
|
31
|
+
|
|
32
|
+
def fetch_state_from_socket
|
|
33
|
+
socket = UNIXSocket.new(SOCKET_PATH)
|
|
34
|
+
data = socket.read
|
|
35
|
+
socket.close
|
|
36
|
+
JSON.parse(data)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch_state_from_api
|
|
40
|
+
uri = URI(API_URL)
|
|
41
|
+
response = Net::HTTP.get_response(uri)
|
|
42
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
43
|
+
|
|
44
|
+
JSON.parse(response.body)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fetch_state
|
|
48
|
+
# Prefer socket (daemon mode) — faster, no HTTP overhead
|
|
49
|
+
fetch_state_from_socket
|
|
50
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED
|
|
51
|
+
# Daemon not running — fall back to direct API call
|
|
52
|
+
data = fetch_state_from_api
|
|
53
|
+
unless data
|
|
54
|
+
system("notify-send", "ZillaCore", "Server not reachable")
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
57
|
+
data
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
system("notify-send", "ZillaCore Error", e.message)
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def format_elapsed(seconds)
|
|
64
|
+
return "#{seconds}s" if seconds < 60
|
|
65
|
+
|
|
66
|
+
minutes = seconds / 60
|
|
67
|
+
return "#{minutes}m" if minutes < 60
|
|
68
|
+
|
|
69
|
+
hours = minutes / 60
|
|
70
|
+
"#{hours}h"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
state = fetch_state
|
|
74
|
+
sessions = state["sessions"] || []
|
|
75
|
+
|
|
76
|
+
if sessions.empty?
|
|
77
|
+
system("notify-send", "ZillaCore", "No active agent sessions")
|
|
78
|
+
exit 0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
INFRA_CMDS = %w[kiro-cli-chat ruby-lsp clangd gopls].freeze
|
|
82
|
+
|
|
83
|
+
# Build menu entries: sessions + their child processes
|
|
84
|
+
entries = []
|
|
85
|
+
sessions.each do |s|
|
|
86
|
+
agent = s["agent"]
|
|
87
|
+
elapsed = format_elapsed(s["elapsed_seconds"])
|
|
88
|
+
card_key = s["card_key"]
|
|
89
|
+
context = if card_key.start_with?("discord-")
|
|
90
|
+
"Discord chat"
|
|
91
|
+
elsif card_key.start_with?("card-")
|
|
92
|
+
card_key.split("-")[1]
|
|
93
|
+
else
|
|
94
|
+
card_key
|
|
95
|
+
end
|
|
96
|
+
emoji = AGENTS[agent.downcase] || DEFAULT_EMOJI
|
|
97
|
+
entries << { display: "#{emoji} #{agent}: #{context} (#{elapsed})", type: :log, log: s["log_file"] }
|
|
98
|
+
entries << { display: " ⛔ Kill session: #{agent} (#{context})", type: :kill_session, card_key: card_key, agent: agent }
|
|
99
|
+
|
|
100
|
+
(s["children"] || []).each do |c|
|
|
101
|
+
cmd_short = c["cmd"].to_s.split("/").last.to_s.split.first.to_s
|
|
102
|
+
cmd_short = c["cmd"].to_s[0..40] if cmd_short.empty?
|
|
103
|
+
next if INFRA_CMDS.any? { |ic| cmd_short.start_with?(ic) }
|
|
104
|
+
|
|
105
|
+
entries << {
|
|
106
|
+
display: " └ 🔪 Kill: #{cmd_short} (#{format_elapsed(c["elapsed_seconds"])}) [PID #{c["pid"]}]",
|
|
107
|
+
type: :kill_child, pid: c["pid"], cmd: cmd_short
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# If only one session with no children, open log directly
|
|
113
|
+
if entries.size == 1 && entries[0][:type] == :log
|
|
114
|
+
spawn("alacritty", "-e", "tail", "-f", entries[0][:log]) if entries[0][:log]
|
|
115
|
+
exit 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def find_launcher
|
|
119
|
+
%w[rofi fuzzel wofi zenity].find { |cmd| system("which #{cmd} > /dev/null 2>&1") }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def run_menu(launcher, entries)
|
|
123
|
+
menu_text = entries.map { |e| e[:display] }.join("\n")
|
|
124
|
+
case launcher
|
|
125
|
+
when "rofi"
|
|
126
|
+
IO.popen(%w[rofi -dmenu -i -p] + ["Agent Sessions"], "r+") do |io|
|
|
127
|
+
io.puts menu_text
|
|
128
|
+
io.close_write
|
|
129
|
+
io.read.strip
|
|
130
|
+
end
|
|
131
|
+
when "fuzzel"
|
|
132
|
+
IO.popen(%w[fuzzel --dmenu --prompt] + ["Agent Sessions: "], "r+") do |io|
|
|
133
|
+
io.puts menu_text
|
|
134
|
+
io.close_write
|
|
135
|
+
io.read.strip
|
|
136
|
+
end
|
|
137
|
+
when "wofi"
|
|
138
|
+
IO.popen(%w[wofi --dmenu --prompt] + ["Agent Sessions"], "r+") do |io|
|
|
139
|
+
io.puts menu_text
|
|
140
|
+
io.close_write
|
|
141
|
+
io.read.strip
|
|
142
|
+
end
|
|
143
|
+
when "zenity"
|
|
144
|
+
IO.popen(["zenity", "--list", "--title", "Agent Sessions", "--column", "Session", "--width", "600", "--height", "400"], "r+",
|
|
145
|
+
err: "/dev/null") do |io|
|
|
146
|
+
entries.each { |e| io.puts e[:display] }
|
|
147
|
+
io.close_write
|
|
148
|
+
io.read.strip
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
launcher = find_launcher
|
|
154
|
+
unless launcher
|
|
155
|
+
system("notify-send", "ZillaCore", "No menu launcher found (install rofi, fuzzel, wofi, or zenity)")
|
|
156
|
+
exit 1
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
selected_line = run_menu(launcher, entries)
|
|
160
|
+
|
|
161
|
+
unless selected_line.to_s.empty?
|
|
162
|
+
selected = entries.find { |e| e[:display].strip == selected_line.strip }
|
|
163
|
+
if selected
|
|
164
|
+
case selected[:type]
|
|
165
|
+
when :log
|
|
166
|
+
spawn("alacritty", "-e", "tail", "-f", selected[:log]) if selected[:log]
|
|
167
|
+
when :kill_session
|
|
168
|
+
card_key = selected[:card_key]
|
|
169
|
+
uri = URI("http://localhost:4567/api/sessions/kill/#{card_key}")
|
|
170
|
+
response = Net::HTTP.post(uri, "", { "Content-Type" => "application/json" })
|
|
171
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
172
|
+
system("notify-send", "ZillaCore", "Killed session: #{selected[:agent]}")
|
|
173
|
+
else
|
|
174
|
+
system("notify-send", "ZillaCore", "Failed to kill session: #{selected[:agent]}")
|
|
175
|
+
end
|
|
176
|
+
when :kill_child
|
|
177
|
+
pid = selected[:pid]
|
|
178
|
+
begin
|
|
179
|
+
Process.kill("TERM", pid)
|
|
180
|
+
rescue StandardError
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
Thread.new do
|
|
184
|
+
sleep 3
|
|
185
|
+
begin
|
|
186
|
+
Process.kill("KILL", pid)
|
|
187
|
+
rescue StandardError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
system("notify-send", "ZillaCore", "Killed #{selected[:cmd]} (PID #{pid})")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ZillaCore 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/zillacore-monitor.sock"
|
|
12
|
+
API_URL = "http://localhost:4567/api/status"
|
|
13
|
+
CONFIG_PATH = File.expand_path("~/.zillacore/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", "ZillaCore", "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
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ZillaCore 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/zillacore/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 zillacore_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 ZillaCore modules
|
|
32
|
+
zillacore_data = zillacore_modules
|
|
33
|
+
modules = zillacore_data["modules"]
|
|
34
|
+
module_configs = zillacore_data["config"]
|
|
35
|
+
|
|
36
|
+
# Remove old zillacore modules and groups from modules-right
|
|
37
|
+
config["modules-right"].reject! { |m| m.to_s.start_with?("custom/zillacore") || m.to_s == "group/zillacore-agents" }
|
|
38
|
+
|
|
39
|
+
# Insert new modules at the beginning of modules-right
|
|
40
|
+
config["modules-right"] = modules + config["modules-right"]
|
|
41
|
+
|
|
42
|
+
# Remove old zillacore module configs and groups
|
|
43
|
+
config.each_key do |key|
|
|
44
|
+
config.delete(key) if key.to_s.start_with?("custom/zillacore") || key.to_s == "group/zillacore-agents"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Add new module configs
|
|
48
|
+
module_configs.each do |name, cfg|
|
|
49
|
+
config[name] = cfg
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Save updated config
|
|
53
|
+
save_config(config)
|
|
54
|
+
|
|
55
|
+
# Reload waybar
|
|
56
|
+
system("killall", "-SIGUSR2", "waybar")
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# ZillaCore Waybar Per-Environment Deploy Module
|
|
5
|
+
# Usage: waybar-deploy-env.rb <env_key>
|
|
6
|
+
# waybar-deploy-env.rb <env_key> --click
|
|
7
|
+
# waybar-deploy-env.rb <env_key> --deploy
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
require "net/http"
|
|
11
|
+
require "shellwords"
|
|
12
|
+
require "uri"
|
|
13
|
+
require "time"
|
|
14
|
+
|
|
15
|
+
SERVER_URL = "http://localhost:4567"
|
|
16
|
+
RECENT_WINDOW = 30 * 60
|
|
17
|
+
|
|
18
|
+
env_key = ARGV.find { |a| !a.start_with?("--") }
|
|
19
|
+
unless env_key
|
|
20
|
+
puts({ text: "?", tooltip: "No env specified", class: "error" }.to_json)
|
|
21
|
+
exit
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch_deployments
|
|
25
|
+
uri = URI("#{SERVER_URL}/api/deployments")
|
|
26
|
+
response = Net::HTTP.get_response(uri)
|
|
27
|
+
JSON.parse(response.body)["deployments"] || []
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def time_ago(iso_time)
|
|
33
|
+
return nil unless iso_time
|
|
34
|
+
|
|
35
|
+
seconds = (Time.now - Time.parse(iso_time)).to_i
|
|
36
|
+
return "#{seconds}s ago" if seconds < 60
|
|
37
|
+
|
|
38
|
+
minutes = seconds / 60
|
|
39
|
+
return "#{minutes}m ago" if minutes < 60
|
|
40
|
+
|
|
41
|
+
hours = minutes / 60
|
|
42
|
+
"#{hours}h ago"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resize_deploy_terminal
|
|
46
|
+
# Shrink the deploy terminal to ~15% width after it tiles in at 50%
|
|
47
|
+
# Calculates resize delta from monitor width dynamically
|
|
48
|
+
script = "sleep 0.5 && " \
|
|
49
|
+
'width=$(hyprctl monitors -j | ruby -rjson -e "puts JSON.parse(STDIN.read)[0][%q(width)]") && ' \
|
|
50
|
+
"delta=$(( (width / 2) - (width * 15 / 100) )) && " \
|
|
51
|
+
'hyprctl --batch "dispatch focuswindow class:zillacore-deploy; dispatch resizeactive -${delta} 0"'
|
|
52
|
+
spawn("bash", "-c", script, %i[out err] => "/dev/null")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def handle_click(env_key, deployment)
|
|
56
|
+
return unless deployment
|
|
57
|
+
|
|
58
|
+
if deployment["last_deploy_status"] == "failed" && deployment["last_deploy_log"]
|
|
59
|
+
log = deployment["last_deploy_log"]
|
|
60
|
+
if File.exist?(log.to_s)
|
|
61
|
+
spawn("alacritty", "--class", "zillacore-deploy", "-e", "bash", "-c",
|
|
62
|
+
"echo '=== Deploy failure: #{deployment["label"] || env_key} ===' && echo && cat #{Shellwords.escape(log)} && echo && echo 'Press Enter to close...' && read",
|
|
63
|
+
%i[out err] => "/dev/null")
|
|
64
|
+
resize_deploy_terminal
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
url = deployment["url"]
|
|
70
|
+
spawn("xdg-open", url, %i[out err] => "/dev/null") if url
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_deploy(env_key, deployment)
|
|
74
|
+
return unless deployment
|
|
75
|
+
|
|
76
|
+
prefill = deployment["status"] == "occupied" && deployment["card_number"] ? deployment["card_number"].to_s : ""
|
|
77
|
+
card_number = `timeout 60 zenity --entry --title="Deploy to #{env_key}" --text="Fizzy card number:"#{unless prefill.empty?
|
|
78
|
+
" --entry-text=#{Shellwords.escape(prefill)}"
|
|
79
|
+
end} 2>/dev/null`.strip
|
|
80
|
+
return if card_number.empty?
|
|
81
|
+
|
|
82
|
+
matches = Dir.glob(File.expand_path("~/Code/*fizzy-#{card_number}-*/"))
|
|
83
|
+
worktree = matches.find { |d| File.directory?(d) }
|
|
84
|
+
unless worktree
|
|
85
|
+
`timeout 10 zenity --error --text="No worktree found for card ##{card_number}" 2>/dev/null`
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Resolve AWS_PROFILE from deployments config
|
|
90
|
+
aws_profile = nil
|
|
91
|
+
config_file = File.expand_path("~/.zillacore/deployments.json")
|
|
92
|
+
if File.exist?(config_file)
|
|
93
|
+
cfg = begin
|
|
94
|
+
JSON.parse(File.read(config_file))
|
|
95
|
+
rescue StandardError
|
|
96
|
+
{}
|
|
97
|
+
end
|
|
98
|
+
aws_profile = cfg.dig("environments", env_key, "aws_profile")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
deploy_script = <<~BASH
|
|
102
|
+
cd #{Shellwords.escape(worktree)}
|
|
103
|
+
#{"export AWS_PROFILE=#{Shellwords.escape(aws_profile)}" if aws_profile}
|
|
104
|
+
echo "🚀 #{env_key} deploy in progress..."
|
|
105
|
+
echo
|
|
106
|
+
logfile=$(mktemp)
|
|
107
|
+
./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1 | tee "$logfile"
|
|
108
|
+
status=${PIPESTATUS[0]}
|
|
109
|
+
if [ $status -ne 0 ] && grep -q "checksums previously recorded in the dependency lock file" "$logfile"; then
|
|
110
|
+
echo
|
|
111
|
+
echo "⚠️ Terraform lock file mismatch — removing lock and running init -upgrade..."
|
|
112
|
+
echo
|
|
113
|
+
rm -f infrastructure/#{Shellwords.escape(env_key)}/.terraform.lock.hcl
|
|
114
|
+
(cd infrastructure/#{Shellwords.escape(env_key)} && terraform init -upgrade)
|
|
115
|
+
echo
|
|
116
|
+
echo "🔄 Retrying deploy..."
|
|
117
|
+
echo
|
|
118
|
+
./scripts/deploy.sh #{Shellwords.escape(env_key)} 2>&1
|
|
119
|
+
status=$?
|
|
120
|
+
fi
|
|
121
|
+
rm -f "$logfile"
|
|
122
|
+
echo
|
|
123
|
+
if [ $status -eq 0 ]; then echo "✅ Deploy complete"; else echo "❌ Deploy failed (exit $status)"; fi
|
|
124
|
+
echo "Press Enter to close..."
|
|
125
|
+
read
|
|
126
|
+
BASH
|
|
127
|
+
|
|
128
|
+
# Mark deploying via API so waybar turns orange immediately
|
|
129
|
+
begin
|
|
130
|
+
uri = URI("#{SERVER_URL}/api/deployments/#{env_key}/deploying")
|
|
131
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
132
|
+
req.body = { worktree: worktree }.to_json
|
|
133
|
+
Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
134
|
+
rescue StandardError
|
|
135
|
+
# Non-fatal — deploy proceeds even if server is unreachable
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
spawn("alacritty", "--class", "zillacore-deploy", "-e", "bash", "-c", deploy_script, %i[out err] => "/dev/null")
|
|
139
|
+
resize_deploy_terminal
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def generate_output(env_key)
|
|
143
|
+
deployments = fetch_deployments
|
|
144
|
+
unless deployments
|
|
145
|
+
puts({ text: "", tooltip: "#{env_key}: server unreachable", class: "error" }.to_json)
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
d = deployments.find { |dep| dep["env"] == env_key }
|
|
150
|
+
unless d
|
|
151
|
+
puts({ text: "", tooltip: "#{env_key}: not configured", class: "error" }.to_json)
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
label = d["label"] || env_key
|
|
156
|
+
|
|
157
|
+
if d["status"] == "occupied"
|
|
158
|
+
deploy_time = d["last_deploy_at"] || d["deployed_at"]
|
|
159
|
+
recent = deploy_time && (Time.now - Time.parse(deploy_time)) < RECENT_WINDOW
|
|
160
|
+
status = d["last_deploy_status"]
|
|
161
|
+
|
|
162
|
+
if status == "deploying"
|
|
163
|
+
dot = '<span color="#ffaa00">●</span>'
|
|
164
|
+
css_class = "deploy-deploying"
|
|
165
|
+
elsif status == "failed"
|
|
166
|
+
dot = '<span color="#ff4444">●</span>'
|
|
167
|
+
css_class = "deploy-failed"
|
|
168
|
+
elsif recent && status == "success"
|
|
169
|
+
dot = '<span color="#4488ff">●</span>'
|
|
170
|
+
css_class = "deploy-recent"
|
|
171
|
+
else
|
|
172
|
+
dot = '<span color="#ff4444">●</span>'
|
|
173
|
+
css_class = "deploy-occupied"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
card = d["card_number"] ? "##{d["card_number"]}" : d["branch"] || "unknown"
|
|
177
|
+
branch = d["branch"] ? " — #{d["branch"]}" : ""
|
|
178
|
+
ago = time_ago(d["deployed_at"])
|
|
179
|
+
status_icon = case status
|
|
180
|
+
when "deploying" then "🚀"
|
|
181
|
+
when "failed" then "💥"
|
|
182
|
+
when "success" then recent ? "🚀✅" : "🔴"
|
|
183
|
+
else "🔴"
|
|
184
|
+
end
|
|
185
|
+
tooltip = "#{status_icon} #{label}: #{card}#{branch}#{" (#{ago})" if ago}\nClick: open URL | Right-click: deploy"
|
|
186
|
+
else
|
|
187
|
+
dot = '<span color="#44ff44">●</span>'
|
|
188
|
+
css_class = "deploy-available"
|
|
189
|
+
ago = time_ago(d["cleared_at"])
|
|
190
|
+
last = d["last_card"] ? " (was ##{d["last_card"]})" : ""
|
|
191
|
+
tooltip = "🟢 #{label}: Available#{" #{ago}" if ago}#{last}\nRight-click: deploy"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
puts({ text: dot, tooltip: tooltip, class: css_class }.to_json)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
deployments = fetch_deployments
|
|
198
|
+
deployment = deployments&.find { |d| d["env"] == env_key }
|
|
199
|
+
|
|
200
|
+
if ARGV.include?("--click")
|
|
201
|
+
handle_click(env_key, deployment)
|
|
202
|
+
elsif ARGV.include?("--deploy")
|
|
203
|
+
handle_deploy(env_key, deployment)
|
|
204
|
+
else
|
|
205
|
+
generate_output(env_key)
|
|
206
|
+
end
|