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