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,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
# Deployment environment tracking.
|
|
6
|
+
# Tracks which dev environments have active card deploys and which are available.
|
|
7
|
+
|
|
8
|
+
DEPLOYMENTS_CONFIG_FILE = File.join(ZILLACORE_DIR, "deployments.json")
|
|
9
|
+
DEPLOYMENT_STATE_FILE = File.join(ZILLACORE_DIR, "deployment_state.json")
|
|
10
|
+
|
|
11
|
+
def load_deployments_config
|
|
12
|
+
return {} unless File.exist?(DEPLOYMENTS_CONFIG_FILE)
|
|
13
|
+
|
|
14
|
+
JSON.parse(File.read(DEPLOYMENTS_CONFIG_FILE))
|
|
15
|
+
rescue JSON::ParserError => e
|
|
16
|
+
LOG.error "Failed to parse deployments config: #{e.message}"
|
|
17
|
+
{}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load_deployment_state
|
|
21
|
+
return {} unless File.exist?(DEPLOYMENT_STATE_FILE)
|
|
22
|
+
|
|
23
|
+
JSON.parse(File.read(DEPLOYMENT_STATE_FILE))
|
|
24
|
+
rescue JSON::ParserError => e
|
|
25
|
+
LOG.error "Failed to parse deployment state: #{e.message}"
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def save_deployment_state(state)
|
|
30
|
+
File.write(DEPLOYMENT_STATE_FILE, JSON.pretty_generate(state))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
DEPLOYMENTS_CONFIG = load_deployments_config
|
|
34
|
+
DEPLOYMENT_STATE = load_deployment_state
|
|
35
|
+
|
|
36
|
+
def reload_deployments_config!(force: false)
|
|
37
|
+
return unless file_changed?(DEPLOYMENTS_CONFIG_FILE, force: force)
|
|
38
|
+
|
|
39
|
+
DEPLOYMENTS_CONFIG.replace(load_deployments_config)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reload_deployment_state!(force: false)
|
|
43
|
+
return unless file_changed?(DEPLOYMENT_STATE_FILE, force: force)
|
|
44
|
+
|
|
45
|
+
DEPLOYMENT_STATE.replace(load_deployment_state)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Mark an environment as actively deploying (in-progress state for waybar).
|
|
49
|
+
def mark_deploying(env_key, worktree_path:)
|
|
50
|
+
state = load_deployment_state
|
|
51
|
+
state[env_key] ||= {}
|
|
52
|
+
state[env_key]["status"] = "occupied"
|
|
53
|
+
state[env_key]["last_deploy_status"] = "deploying"
|
|
54
|
+
state[env_key]["last_deploy_at"] = Time.now.iso8601
|
|
55
|
+
save_deployment_state(state)
|
|
56
|
+
DEPLOYMENT_STATE.replace(state)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Mark an environment as occupied. Resolves card info from the card map using the worktree path.
|
|
60
|
+
def deploy_to_environment(env_key, worktree_path:, deployed_by: nil)
|
|
61
|
+
config = DEPLOYMENTS_CONFIG["environments"] || {}
|
|
62
|
+
unless config.key?(env_key)
|
|
63
|
+
LOG.warn "[Deploy] Unknown environment: #{env_key}"
|
|
64
|
+
return { error: "Unknown environment: #{env_key}" }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
state = load_deployment_state
|
|
68
|
+
entry = { "status" => "occupied", "deployed_at" => Time.now.iso8601, "deployed_by" => deployed_by,
|
|
69
|
+
"last_deploy_status" => "success", "last_deploy_at" => Time.now.iso8601 }
|
|
70
|
+
|
|
71
|
+
# Resolve card info from card map by matching worktree path
|
|
72
|
+
map = load_card_map
|
|
73
|
+
card_entry = map.values.find { |info| info["worktree"] == worktree_path }
|
|
74
|
+
if card_entry
|
|
75
|
+
entry["card_number"] = card_entry["number"]
|
|
76
|
+
entry["card_title"] = card_entry["title"]
|
|
77
|
+
entry["branch"] = card_entry["branch"]
|
|
78
|
+
pr = (card_entry["prs"] || []).last
|
|
79
|
+
if pr
|
|
80
|
+
entry["pr_number"] = pr["number"]
|
|
81
|
+
entry["pr_url"] = pr["url"]
|
|
82
|
+
end
|
|
83
|
+
# Store card tags for URL resolution (e.g. ops-web-app → ops URL)
|
|
84
|
+
card_idx = CARD_INDEX[card_entry["number"].to_s]
|
|
85
|
+
entry["card_tags"] = card_idx["tags"] if card_idx && card_idx["tags"]
|
|
86
|
+
else
|
|
87
|
+
# No card map match — record branch from git
|
|
88
|
+
branch = `git -C #{Shellwords.escape(worktree_path)} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
89
|
+
entry["branch"] = branch unless branch.empty?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
commit = `git -C #{Shellwords.escape(worktree_path)} rev-parse --short HEAD 2>/dev/null`.strip
|
|
93
|
+
entry["commit"] = commit unless commit.empty?
|
|
94
|
+
|
|
95
|
+
state[env_key] = entry
|
|
96
|
+
save_deployment_state(state)
|
|
97
|
+
DEPLOYMENT_STATE.replace(state)
|
|
98
|
+
LOG.info "[Deploy] #{env_key} marked occupied — card ##{entry["card_number"] || "none"}, branch: #{entry["branch"]}"
|
|
99
|
+
entry
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
DEPLOY_LOGS_DIR = File.join(ZILLACORE_DIR, "deploy_logs")
|
|
103
|
+
|
|
104
|
+
# Record a failed deploy — saves output to a log file and updates state.
|
|
105
|
+
def record_deploy_failure(env_key, worktree_path:, stdout: "", stderr: "")
|
|
106
|
+
FileUtils.mkdir_p(DEPLOY_LOGS_DIR)
|
|
107
|
+
log_file = File.join(DEPLOY_LOGS_DIR, "#{env_key}-#{Time.now.strftime("%Y%m%d-%H%M%S")}.log")
|
|
108
|
+
File.write(log_file, "=== STDOUT ===\n#{stdout}\n\n=== STDERR ===\n#{stderr}")
|
|
109
|
+
|
|
110
|
+
state = load_deployment_state
|
|
111
|
+
state[env_key] ||= {}
|
|
112
|
+
state[env_key]["last_deploy_status"] = "failed"
|
|
113
|
+
state[env_key]["last_deploy_at"] = Time.now.iso8601
|
|
114
|
+
state[env_key]["last_deploy_log"] = log_file
|
|
115
|
+
save_deployment_state(state)
|
|
116
|
+
DEPLOYMENT_STATE.replace(state)
|
|
117
|
+
LOG.info "[Deploy] #{env_key} deploy failed — log at #{log_file}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Auto-deploy after agent session when [deploy] tag was present.
|
|
121
|
+
# deploy_intent is either a specific env key (e.g. "dev04"), :auto (auto-detect), or nil (no deploy).
|
|
122
|
+
def auto_deploy_after_session(deploy_intent:, card_internal_id:, card_number:, worktree_path:, agent_name:)
|
|
123
|
+
state = load_deployment_state
|
|
124
|
+
config = DEPLOYMENTS_CONFIG["environments"] || {}
|
|
125
|
+
|
|
126
|
+
env_key = resolve_deploy_environment(deploy_intent, state, card_number)
|
|
127
|
+
return unless env_key
|
|
128
|
+
|
|
129
|
+
unless config.key?(env_key)
|
|
130
|
+
LOG.warn "[Deploy] Auto-deploy skipped — unknown environment: #{env_key}"
|
|
131
|
+
return
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
env_owner = config[env_key]["owner"]
|
|
135
|
+
unless env_owner && env_owner.downcase == AI_AGENT_NAME.downcase
|
|
136
|
+
LOG.info "[Deploy] Auto-deploy skipped #{env_key} — owner is #{env_owner.inspect}, this machine is #{AI_AGENT_NAME}"
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
deploy_script = File.join(worktree_path, "scripts", "deploy.sh")
|
|
141
|
+
unless File.exist?(deploy_script)
|
|
142
|
+
LOG.warn "[Deploy] Auto-deploy skipped — no deploy script at #{deploy_script}"
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
LOG.info "[Deploy] Auto-deploying card ##{card_number} to #{env_key} (triggered by [deploy] tag)"
|
|
147
|
+
mark_deploying(env_key, worktree_path: worktree_path)
|
|
148
|
+
|
|
149
|
+
deploy_env = {}
|
|
150
|
+
aws_profile = config.dig(env_key, "aws_profile")
|
|
151
|
+
deploy_env["AWS_PROFILE"] = aws_profile if aws_profile
|
|
152
|
+
|
|
153
|
+
run_deploy(deploy_env, deploy_script, env_key, worktree_path: worktree_path, card_number: card_number, agent_name: agent_name)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Resolve which environment to deploy to from the intent.
|
|
157
|
+
def resolve_deploy_environment(deploy_intent, state, card_number)
|
|
158
|
+
if deploy_intent.is_a?(String) && !deploy_intent.empty?
|
|
159
|
+
deploy_intent
|
|
160
|
+
else
|
|
161
|
+
existing = state.find { |_k, v| v["card_number"] == card_number && v["status"] == "occupied" }&.first
|
|
162
|
+
LOG.info "[Deploy] Auto-deploy skipped — card ##{card_number} not currently deployed to any environment" unless existing
|
|
163
|
+
existing
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Execute deploy script with terraform lock retry logic.
|
|
168
|
+
def run_deploy(deploy_env, deploy_script, env_key, worktree_path:, card_number:, agent_name:)
|
|
169
|
+
stdout, stderr, status = Open3.capture3(deploy_env, deploy_script, env_key, chdir: worktree_path)
|
|
170
|
+
|
|
171
|
+
if status.success?
|
|
172
|
+
deploy_to_environment(env_key, worktree_path: worktree_path, deployed_by: "#{agent_name} [deploy]")
|
|
173
|
+
LOG.info "[Deploy] Auto-deploy to #{env_key} succeeded for card ##{card_number}"
|
|
174
|
+
elsif terraform_lock_error?(stdout, stderr)
|
|
175
|
+
retry_deploy_after_lock_fix(deploy_env, deploy_script, env_key, worktree_path: worktree_path, card_number: card_number, agent_name: agent_name)
|
|
176
|
+
else
|
|
177
|
+
record_deploy_failure(env_key, worktree_path: worktree_path, stdout: stdout, stderr: stderr)
|
|
178
|
+
LOG.error "[Deploy] Auto-deploy to #{env_key} failed for card ##{card_number}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Retry deploy after clearing terraform lock.
|
|
183
|
+
def retry_deploy_after_lock_fix(deploy_env, deploy_script, env_key, worktree_path:, card_number:, agent_name:)
|
|
184
|
+
lock_file = File.join(worktree_path, "infrastructure/#{env_key}/.terraform.lock.hcl")
|
|
185
|
+
FileUtils.rm_f(lock_file)
|
|
186
|
+
Open3.capture3("terraform", "init", "-upgrade", chdir: File.join(worktree_path, "infrastructure/#{env_key}"))
|
|
187
|
+
stdout2, stderr2, status2 = Open3.capture3(deploy_env, deploy_script, env_key, chdir: worktree_path)
|
|
188
|
+
if status2.success?
|
|
189
|
+
deploy_to_environment(env_key, worktree_path: worktree_path, deployed_by: "#{agent_name} [deploy]")
|
|
190
|
+
LOG.info "[Deploy] Auto-deploy to #{env_key} succeeded (after terraform lock fix) for card ##{card_number}"
|
|
191
|
+
else
|
|
192
|
+
record_deploy_failure(env_key, worktree_path: worktree_path, stdout: stdout2, stderr: stderr2)
|
|
193
|
+
LOG.error "[Deploy] Auto-deploy to #{env_key} failed (after retry) for card ##{card_number}"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Detect Terraform provider lock file checksum mismatch errors.
|
|
198
|
+
def terraform_lock_error?(stdout, stderr)
|
|
199
|
+
combined = "#{stdout}\n#{stderr}"
|
|
200
|
+
combined.include?("checksums previously recorded in the dependency lock file")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Clear all environments occupied by a given card number (called on PR merge).
|
|
204
|
+
def clear_deployment_for_card(card_number)
|
|
205
|
+
state = load_deployment_state
|
|
206
|
+
cleared = []
|
|
207
|
+
|
|
208
|
+
state.each do |env_key, info|
|
|
209
|
+
next unless info["card_number"] == card_number && info["status"] == "occupied"
|
|
210
|
+
|
|
211
|
+
state[env_key] = { "status" => "available", "cleared_at" => Time.now.iso8601, "last_card" => card_number }
|
|
212
|
+
cleared << env_key
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if cleared.any?
|
|
216
|
+
save_deployment_state(state)
|
|
217
|
+
DEPLOYMENT_STATE.replace(state)
|
|
218
|
+
LOG.info "[Deploy] Cleared #{cleared.join(", ")} — card ##{card_number} merged"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
cleared
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Return environments with status "available", optionally filtered by project.
|
|
225
|
+
def available_environments(project: nil)
|
|
226
|
+
config = DEPLOYMENTS_CONFIG["environments"] || {}
|
|
227
|
+
state = load_deployment_state
|
|
228
|
+
|
|
229
|
+
config.select do |env_key, env_config|
|
|
230
|
+
next false if project && env_config["project"] != project
|
|
231
|
+
|
|
232
|
+
info = state[env_key]
|
|
233
|
+
info.nil? || info["status"] == "available"
|
|
234
|
+
end.keys
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Full deployment status for API / waybar.
|
|
238
|
+
def deployment_status
|
|
239
|
+
config = DEPLOYMENTS_CONFIG["environments"] || {}
|
|
240
|
+
state = load_deployment_state
|
|
241
|
+
|
|
242
|
+
config.map do |env_key, env_config|
|
|
243
|
+
info = state[env_key] || { "status" => "available" }
|
|
244
|
+
url = resolve_deployment_url(env_config, info["card_tags"])
|
|
245
|
+
{ "env" => env_key, "label" => env_config["label"], "url" => url, "project" => env_config["project"] }.merge(info)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Resolve the correct URL for an environment based on card tags.
|
|
250
|
+
# If the card has a tag matching a key in the environment's "urls" map, use that URL.
|
|
251
|
+
# Otherwise fall back to the default "url".
|
|
252
|
+
def resolve_deployment_url(env_config, card_tags)
|
|
253
|
+
urls = env_config["urls"] || {}
|
|
254
|
+
if card_tags && urls.any?
|
|
255
|
+
card_tags.each { |tag| return urls[tag] if urls[tag] }
|
|
256
|
+
end
|
|
257
|
+
env_config["url"]
|
|
258
|
+
end
|