openclacky 0.7.0 → 0.7.2

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/commit/SKILL.md +29 -4
  3. data/.clackyrules +3 -1
  4. data/CHANGELOG.md +103 -2
  5. data/README.md +70 -161
  6. data/bin/clarky +11 -0
  7. data/docs/HOW-TO-USE-CN.md +96 -0
  8. data/docs/HOW-TO-USE.md +94 -0
  9. data/docs/config.example.yml +27 -0
  10. data/docs/deploy_subagent_design.md +540 -0
  11. data/docs/time_machine_design.md +247 -0
  12. data/docs/why-openclacky.md +0 -1
  13. data/lib/clacky/agent/cost_tracker.rb +180 -0
  14. data/lib/clacky/agent/llm_caller.rb +54 -0
  15. data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
  16. data/lib/clacky/agent/message_compressor_helper.rb +534 -0
  17. data/lib/clacky/agent/session_serializer.rb +152 -0
  18. data/lib/clacky/agent/skill_manager.rb +138 -0
  19. data/lib/clacky/agent/system_prompt_builder.rb +96 -0
  20. data/lib/clacky/agent/time_machine.rb +199 -0
  21. data/lib/clacky/agent/tool_executor.rb +434 -0
  22. data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
  23. data/lib/clacky/agent.rb +260 -1370
  24. data/lib/clacky/agent_config.rb +447 -10
  25. data/lib/clacky/cli.rb +275 -98
  26. data/lib/clacky/client.rb +12 -2
  27. data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
  28. data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
  29. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
  30. data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
  31. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
  32. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
  33. data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
  34. data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
  35. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
  36. data/lib/clacky/default_skills/new/SKILL.md +2 -2
  37. data/lib/clacky/json_ui_controller.rb +195 -0
  38. data/lib/clacky/providers.rb +107 -0
  39. data/lib/clacky/skill.rb +48 -7
  40. data/lib/clacky/skill_loader.rb +7 -0
  41. data/lib/clacky/tools/edit.rb +105 -48
  42. data/lib/clacky/tools/file_reader.rb +44 -73
  43. data/lib/clacky/tools/invoke_skill.rb +89 -0
  44. data/lib/clacky/tools/list_tasks.rb +54 -0
  45. data/lib/clacky/tools/redo_task.rb +41 -0
  46. data/lib/clacky/tools/safe_shell.rb +1 -1
  47. data/lib/clacky/tools/shell.rb +74 -62
  48. data/lib/clacky/tools/trash_manager.rb +1 -1
  49. data/lib/clacky/tools/undo_task.rb +32 -0
  50. data/lib/clacky/tools/web_fetch.rb +2 -1
  51. data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
  52. data/lib/clacky/ui2/components/inline_input.rb +23 -2
  53. data/lib/clacky/ui2/components/input_area.rb +65 -21
  54. data/lib/clacky/ui2/components/modal_component.rb +199 -62
  55. data/lib/clacky/ui2/layout_manager.rb +75 -25
  56. data/lib/clacky/ui2/line_editor.rb +23 -2
  57. data/lib/clacky/ui2/markdown_renderer.rb +31 -10
  58. data/lib/clacky/ui2/screen_buffer.rb +2 -0
  59. data/lib/clacky/ui2/ui_controller.rb +316 -37
  60. data/lib/clacky/ui2.rb +2 -0
  61. data/lib/clacky/ui_interface.rb +50 -0
  62. data/lib/clacky/utils/arguments_parser.rb +31 -3
  63. data/lib/clacky/utils/file_processor.rb +13 -18
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky.rb +19 -9
  66. data/scripts/install.sh +274 -97
  67. data/scripts/uninstall.sh +12 -12
  68. metadata +40 -13
  69. data/.clacky/skills/test-skill/SKILL.md +0 -15
  70. data/lib/clacky/compression/base.rb +0 -231
  71. data/lib/clacky/compression/standard.rb +0 -339
  72. data/lib/clacky/config.rb +0 -117
  73. /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
  74. /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
  75. /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
  76. /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
  77. /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
  78. /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
@@ -0,0 +1,383 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../tools/list_services'
4
+ require_relative '../tools/report_deploy_status'
5
+ require_relative '../tools/execute_deployment'
6
+ require_relative '../tools/set_deploy_variables'
7
+ require_relative '../tools/fetch_runtime_logs'
8
+ require_relative '../tools/check_health'
9
+
10
+ module Clacky
11
+ module DeployTemplates
12
+ # Rails deployment template - Fixed 8-step deployment process
13
+ # No AI decision-making, pure automation
14
+ class RailsDeploy
15
+ # Execute the Rails deployment workflow
16
+ #
17
+ # @return [Hash] Deployment result
18
+ def self.execute
19
+ # CRITICAL: Check environment requirements
20
+ unless environment_valid?
21
+ return {
22
+ success: false,
23
+ error: "Railway deployment is only available in the Clacky cloud environment with Rails template projects.",
24
+ details: environment_check_details
25
+ }
26
+ end
27
+
28
+ puts "\n" + "="*60
29
+ puts "🚂 Rails Deployment Template (8-Step Process)"
30
+ puts "="*60 + "\n"
31
+
32
+ # Step 1: List services
33
+ step1_result = step1_list_services
34
+ return step1_result unless step1_result[:success]
35
+
36
+ services = step1_result[:services]
37
+ main_service = step1_result[:main_service]
38
+ db_service = step1_result[:db_service]
39
+
40
+ # Step 2: Check first deployment
41
+ step2_result = step2_check_first_deployment(main_service)
42
+ return step2_result unless step2_result[:success]
43
+
44
+ is_first = step2_result[:is_first_deployment]
45
+
46
+ # Step 3: Set Rails environment variables
47
+ step3_result = step3_set_rails_variables(main_service, db_service, is_first)
48
+ return step3_result unless step3_result[:success]
49
+
50
+ # Step 4: Execute deployment
51
+ step4_result = step4_execute_deployment(main_service)
52
+ return step4_result unless step4_result[:success]
53
+
54
+ # Step 5: Run database migrations
55
+ step5_result = step5_run_migrations(main_service)
56
+ return step5_result unless step5_result[:success]
57
+
58
+ # Step 6: Run database seeds (first deployment only)
59
+ if is_first
60
+ step6_result = step6_run_seeds(main_service)
61
+ return step6_result unless step6_result[:success]
62
+ else
63
+ puts "\n[Step 6] Skipping database seeds (not first deployment)\n"
64
+ end
65
+
66
+ # Step 7: Health check
67
+ step7_result = step7_health_check(main_service)
68
+ return step7_result unless step7_result[:success]
69
+
70
+ # Step 8: Report success
71
+ step8_report_success(main_service)
72
+ end
73
+
74
+ # Check if environment is valid for deployment
75
+ #
76
+ # @return [Boolean] true if both required environment variables are "true"
77
+ def self.environment_valid?
78
+ ENV['IS_RAILS_TEMPLATE'] == 'true' && ENV['IS_CLACKY_CDE'] == 'true'
79
+ end
80
+
81
+ # Get environment check details for error reporting
82
+ #
83
+ # @return [Hash] Details about environment check
84
+ def self.environment_check_details
85
+ {
86
+ is_rails_template: ENV['IS_RAILS_TEMPLATE'],
87
+ is_clacky_cde: ENV['IS_CLACKY_CDE'],
88
+ required: {
89
+ is_rails_template: 'true',
90
+ is_clacky_cde: 'true'
91
+ }
92
+ }
93
+ end
94
+
95
+ # Step 1: List Railway services
96
+ def self.step1_list_services
97
+ puts "\n[Step 1] Listing Railway services..."
98
+ DeployTools::ReportDeployStatus.execute(
99
+ status: 'analyzing',
100
+ message: 'Listing Railway services'
101
+ )
102
+
103
+ result = DeployTools::ListServices.execute
104
+
105
+ unless result[:success]
106
+ return {
107
+ success: false,
108
+ error: "Failed to list services",
109
+ details: result
110
+ }
111
+ end
112
+
113
+ services = result[:services]
114
+
115
+ # Find main web service
116
+ main_service = services.find { |s| s['type'] == 'web' || s['type'] == 'service' }
117
+
118
+ if main_service.nil?
119
+ return {
120
+ success: false,
121
+ error: "No web service found",
122
+ details: "Please create a web service in Railway first"
123
+ }
124
+ end
125
+
126
+ # Find database service
127
+ db_service = services.find { |s| s['type'] == 'postgres' || s['type'] == 'mysql' }
128
+
129
+ puts "✅ Found #{services.length} service(s)"
130
+ puts " Main service: #{main_service['name']}"
131
+ puts " Database: #{db_service ? db_service['name'] : 'None'}"
132
+
133
+ {
134
+ success: true,
135
+ services: services,
136
+ main_service: main_service,
137
+ db_service: db_service
138
+ }
139
+ end
140
+
141
+ # Step 2: Check if this is first deployment
142
+ def self.step2_check_first_deployment(main_service)
143
+ puts "\n[Step 2] Checking deployment history..."
144
+
145
+ deployments = main_service['deployments'] || []
146
+ is_first = deployments.empty?
147
+
148
+ if is_first
149
+ puts "📦 This is the FIRST deployment"
150
+ else
151
+ puts "📦 This is a SUBSEQUENT deployment (#{deployments.length} previous deployment(s))"
152
+ end
153
+
154
+ {
155
+ success: true,
156
+ is_first_deployment: is_first,
157
+ previous_deployments: deployments.length
158
+ }
159
+ end
160
+
161
+ # Step 3: Set Rails environment variables
162
+ def self.step3_set_rails_variables(main_service, db_service, is_first)
163
+ puts "\n[Step 3] Setting Rails environment variables..."
164
+ DeployTools::ReportDeployStatus.execute(
165
+ status: 'analyzing',
166
+ message: 'Configuring Rails environment variables'
167
+ )
168
+
169
+ service_name = main_service['name']
170
+
171
+ # Build simple variables
172
+ variables = {
173
+ 'RAILS_ENV' => 'production',
174
+ 'RAILS_SERVE_STATIC_FILES' => 'true',
175
+ 'RAILS_LOG_TO_STDOUT' => 'true'
176
+ }
177
+
178
+ # Get or prompt for SECRET_KEY_BASE
179
+ secret_key_base = ENV['SECRET_KEY_BASE']
180
+ if secret_key_base.nil? || secret_key_base.empty?
181
+ puts "⚠️ SECRET_KEY_BASE not found. Please generate one:"
182
+ puts " Run: rails secret"
183
+ print "Enter SECRET_KEY_BASE: "
184
+ secret_key_base = $stdin.gets.chomp
185
+ end
186
+ variables['SECRET_KEY_BASE'] = secret_key_base
187
+
188
+ # Build reference variables
189
+ ref_variables = {}
190
+ if db_service
191
+ ref_variables['DATABASE_URL'] = "#{db_service['name']}.DATABASE_URL"
192
+ end
193
+
194
+ # Set variables
195
+ result = DeployTools::SetDeployVariables.execute(
196
+ service_name: service_name,
197
+ variables: variables,
198
+ ref_variables: ref_variables
199
+ )
200
+
201
+ unless result[:success]
202
+ return {
203
+ success: false,
204
+ error: "Failed to set environment variables",
205
+ details: result
206
+ }
207
+ end
208
+
209
+ puts "✅ Set #{result[:set_variables].length} variable(s)"
210
+ if result[:skipped_variables].any?
211
+ puts "⚠️ Skipped #{result[:skipped_variables].length} protected variable(s)"
212
+ end
213
+
214
+ {
215
+ success: true,
216
+ set_count: result[:set_variables].length
217
+ }
218
+ end
219
+
220
+ # Step 4: Execute deployment
221
+ def self.step4_execute_deployment(main_service)
222
+ puts "\n[Step 4] Executing deployment..."
223
+ DeployTools::ReportDeployStatus.execute(
224
+ status: 'deploying',
225
+ message: 'Starting deployment to Railway'
226
+ )
227
+
228
+ service_name = main_service['name']
229
+ result = DeployTools::ExecuteDeployment.execute(service_name: service_name)
230
+
231
+ unless result[:success]
232
+ # Fetch logs for debugging
233
+ puts "\n❌ Deployment failed. Fetching logs..."
234
+ log_result = DeployTools::FetchRuntimeLogs.execute(
235
+ service_name: service_name,
236
+ lines: 100
237
+ )
238
+
239
+ if log_result[:success]
240
+ puts "\n📋 Last 100 lines of logs:"
241
+ puts log_result[:logs]
242
+ end
243
+
244
+ return {
245
+ success: false,
246
+ error: "Deployment failed",
247
+ details: result
248
+ }
249
+ end
250
+
251
+ puts "✅ Deployment completed in #{result[:elapsed].round(1)}s"
252
+
253
+ {
254
+ success: true,
255
+ elapsed: result[:elapsed]
256
+ }
257
+ end
258
+
259
+ # Step 5: Run database migrations
260
+ def self.step5_run_migrations(main_service)
261
+ puts "\n[Step 5] Running database migrations..."
262
+
263
+ service_name = main_service['name']
264
+ command = "clackycli run -s '#{service_name}' rake db:migrate"
265
+
266
+ puts "Running: rake db:migrate"
267
+ output = `#{command} 2>&1`
268
+ exit_code = $?.exitstatus
269
+
270
+ if exit_code != 0
271
+ return {
272
+ success: false,
273
+ error: "Database migration failed",
274
+ details: output
275
+ }
276
+ end
277
+
278
+ puts "✅ Database migrations completed"
279
+ puts output if output && !output.empty?
280
+
281
+ {
282
+ success: true
283
+ }
284
+ end
285
+
286
+ # Step 6: Run database seeds
287
+ def self.step6_run_seeds(main_service)
288
+ puts "\n[Step 6] Running database seeds..."
289
+
290
+ service_name = main_service['name']
291
+ command = "clackycli run -s '#{service_name}' rake db:seed"
292
+
293
+ puts "Running: rake db:seed"
294
+ output = `#{command} 2>&1`
295
+ exit_code = $?.exitstatus
296
+
297
+ if exit_code != 0
298
+ puts "⚠️ Warning: Database seeding failed (this may be expected)"
299
+ puts output if output && !output.empty?
300
+ else
301
+ puts "✅ Database seeding completed"
302
+ puts output if output && !output.empty?
303
+ end
304
+
305
+ # Don't fail deployment if seeding fails
306
+ {
307
+ success: true
308
+ }
309
+ end
310
+
311
+ # Step 7: Health check
312
+ def self.step7_health_check(main_service)
313
+ puts "\n[Step 7] Performing health check..."
314
+ DeployTools::ReportDeployStatus.execute(
315
+ status: 'checking',
316
+ message: 'Verifying application health'
317
+ )
318
+
319
+ public_url = main_service['public_url']
320
+
321
+ if public_url.nil? || public_url.empty?
322
+ puts "⚠️ Warning: No public URL found. Skipping health check."
323
+ return { success: true }
324
+ end
325
+
326
+ # Wait a bit for service to be fully ready
327
+ puts "⏳ Waiting 10 seconds for service to be ready..."
328
+ sleep 10
329
+
330
+ result = DeployTools::CheckHealth.execute(
331
+ url: public_url,
332
+ path: '/',
333
+ timeout: 30
334
+ )
335
+
336
+ unless result[:success]
337
+ puts "⚠️ Warning: Health check failed (service may still be starting)"
338
+ puts " Error: #{result[:error]}"
339
+ puts " You can manually check: #{public_url}"
340
+ # Don't fail deployment on health check failure
341
+ else
342
+ puts "✅ Health check passed (#{result[:status_code]} - #{result[:elapsed]}s)"
343
+ end
344
+
345
+ {
346
+ success: true,
347
+ health_check_result: result
348
+ }
349
+ end
350
+
351
+ # Step 8: Report success
352
+ def self.step8_report_success(main_service)
353
+ puts "\n[Step 8] Deployment completed successfully! 🎉"
354
+
355
+ public_url = main_service['public_url']
356
+
357
+ DeployTools::ReportDeployStatus.execute(
358
+ status: 'success',
359
+ message: "Rails app deployed successfully"
360
+ )
361
+
362
+ puts "\n" + "="*60
363
+ puts "✅ DEPLOYMENT SUCCESSFUL"
364
+ puts "="*60
365
+ puts "Service: #{main_service['name']}"
366
+ puts "URL: #{public_url || 'Not available yet'}"
367
+ puts "="*60 + "\n"
368
+
369
+ {
370
+ success: true,
371
+ service: main_service['name'],
372
+ url: public_url
373
+ }
374
+ end
375
+ end
376
+ end
377
+ end
378
+
379
+ # Run deployment if executed directly
380
+ if __FILE__ == $0
381
+ result = Clacky::DeployTemplates::RailsDeploy.execute
382
+ exit(result[:success] ? 0 : 1)
383
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module Clacky
8
+ module DeployTools
9
+ # Perform HTTP health check on deployed application
10
+ class CheckHealth
11
+ DEFAULT_PATH = '/'
12
+ DEFAULT_TIMEOUT = 30
13
+ MAX_TIMEOUT = 120
14
+
15
+ # Perform health check
16
+ #
17
+ # @param url [String] Optional URL (defaults to RAILWAY_PUBLIC_DOMAIN env var)
18
+ # @param path [String] Health check path (default: "/")
19
+ # @param timeout [Integer] Request timeout in seconds (default: 30)
20
+ # @return [Hash] Result of health check
21
+ def self.execute(url: nil, path: DEFAULT_PATH, timeout: DEFAULT_TIMEOUT)
22
+ # Get URL from parameter or environment
23
+ target_url = url || ENV['RAILWAY_PUBLIC_DOMAIN']
24
+
25
+ if target_url.nil? || target_url.empty?
26
+ return {
27
+ error: "No URL provided",
28
+ details: "Please provide a URL or set RAILWAY_PUBLIC_DOMAIN environment variable"
29
+ }
30
+ end
31
+
32
+ # Ensure URL has protocol
33
+ target_url = "https://#{target_url}" unless target_url.start_with?('http://', 'https://')
34
+
35
+ # Build full URL with path
36
+ full_url = "#{target_url.chomp('/')}#{path}"
37
+
38
+ # Validate timeout
39
+ timeout = timeout.to_i
40
+ if timeout <= 0 || timeout > MAX_TIMEOUT
41
+ timeout = DEFAULT_TIMEOUT
42
+ end
43
+
44
+ puts "🏥 Checking health: #{full_url} (timeout: #{timeout}s)"
45
+
46
+ begin
47
+ uri = URI.parse(full_url)
48
+ result = perform_request(uri, timeout)
49
+
50
+ if result[:success]
51
+ puts "✅ Health check passed: #{result[:status_code]}"
52
+ else
53
+ puts "❌ Health check failed: #{result[:error]}"
54
+ end
55
+
56
+ result.merge(url: full_url, path: path)
57
+ rescue URI::InvalidURIError => e
58
+ {
59
+ success: false,
60
+ error: "Invalid URL",
61
+ details: e.message,
62
+ url: full_url
63
+ }
64
+ end
65
+ end
66
+
67
+ # Perform HTTP request
68
+ #
69
+ # @param uri [URI] Target URI
70
+ # @param timeout [Integer] Timeout in seconds
71
+ # @return [Hash] Request result
72
+ def self.perform_request(uri, timeout)
73
+ start_time = Time.now
74
+
75
+ http = Net::HTTP.new(uri.host, uri.port)
76
+ http.use_ssl = (uri.scheme == 'https')
77
+ http.open_timeout = timeout
78
+ http.read_timeout = timeout
79
+
80
+ request = Net::HTTP::Get.new(uri.request_uri)
81
+ request['User-Agent'] = 'Clacky-Deploy-Health-Check/1.0'
82
+
83
+ response = http.request(request)
84
+ elapsed = Time.now - start_time
85
+
86
+ {
87
+ success: response.is_a?(Net::HTTPSuccess),
88
+ status_code: response.code.to_i,
89
+ status_message: response.message,
90
+ elapsed: elapsed.round(2),
91
+ headers: response.to_hash,
92
+ body_preview: response.body&.slice(0, 500) # First 500 chars
93
+ }
94
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
95
+ {
96
+ success: false,
97
+ error: "Request timeout",
98
+ details: e.message,
99
+ elapsed: timeout
100
+ }
101
+ rescue SocketError => e
102
+ {
103
+ success: false,
104
+ error: "Network error",
105
+ details: e.message
106
+ }
107
+ rescue StandardError => e
108
+ {
109
+ success: false,
110
+ error: "Health check failed",
111
+ details: "#{e.class}: #{e.message}"
112
+ }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Clacky
6
+ module DeployTools
7
+ # Execute deployment and monitor until completion
8
+ class ExecuteDeployment
9
+ MAX_WAIT_TIME = 600 # 10 minutes
10
+ POLL_INTERVAL = 5 # 5 seconds
11
+
12
+ # Execute deployment for a service
13
+ #
14
+ # @param service_name [String] Service to deploy
15
+ # @return [Hash] Result of the deployment
16
+ def self.execute(service_name:)
17
+ if service_name.nil? || service_name.empty?
18
+ return {
19
+ error: "Service name is required",
20
+ details: "Please provide a valid service name"
21
+ }
22
+ end
23
+
24
+ puts "🚀 Starting deployment for service: #{service_name}"
25
+
26
+ # Trigger deployment
27
+ command = "clackycli up -s #{shell_escape(service_name)} -d"
28
+ output = `#{command} 2>&1`
29
+ exit_code = $?.exitstatus
30
+
31
+ if exit_code != 0
32
+ return {
33
+ error: "Failed to trigger deployment",
34
+ details: output,
35
+ exit_code: exit_code,
36
+ service: service_name
37
+ }
38
+ end
39
+
40
+ puts "✅ Deployment triggered successfully"
41
+ puts "⏳ Monitoring deployment progress..."
42
+
43
+ # Monitor deployment status
44
+ result = monitor_deployment(service_name)
45
+ result[:service] = service_name
46
+ result
47
+ end
48
+
49
+ # Monitor deployment status until completion or timeout
50
+ #
51
+ # @param service_name [String] Service name
52
+ # @return [Hash] Deployment result
53
+ def self.monitor_deployment(service_name)
54
+ start_time = Time.now
55
+ last_status = nil
56
+
57
+ loop do
58
+ elapsed = Time.now - start_time
59
+
60
+ if elapsed > MAX_WAIT_TIME
61
+ return {
62
+ success: false,
63
+ error: "Deployment timeout",
64
+ details: "Deployment exceeded maximum wait time of #{MAX_WAIT_TIME} seconds",
65
+ elapsed: elapsed
66
+ }
67
+ end
68
+
69
+ # Get deployment status
70
+ status = get_deployment_status(service_name)
71
+
72
+ # Print status update if changed
73
+ if status[:current_status] != last_status
74
+ puts "📊 Status: #{status[:current_status]}"
75
+ last_status = status[:current_status]
76
+ end
77
+
78
+ # Check if deployment completed
79
+ if status[:completed]
80
+ if status[:success]
81
+ return {
82
+ success: true,
83
+ message: "Deployment completed successfully",
84
+ elapsed: elapsed,
85
+ final_status: status[:current_status]
86
+ }
87
+ else
88
+ return {
89
+ success: false,
90
+ error: "Deployment failed",
91
+ details: status[:error_message],
92
+ elapsed: elapsed,
93
+ final_status: status[:current_status]
94
+ }
95
+ end
96
+ end
97
+
98
+ # Wait before next poll
99
+ sleep POLL_INTERVAL
100
+ end
101
+ end
102
+
103
+ # Get current deployment status
104
+ #
105
+ # @param service_name [String] Service name
106
+ # @return [Hash] Status information
107
+ def self.get_deployment_status(service_name)
108
+ command = "clackycli service list --json"
109
+ output = `#{command} 2>&1`
110
+
111
+ begin
112
+ services = JSON.parse(output)
113
+ service = services.find { |s| s['name'] == service_name }
114
+
115
+ if service.nil?
116
+ return {
117
+ completed: true,
118
+ success: false,
119
+ error_message: "Service not found: #{service_name}"
120
+ }
121
+ end
122
+
123
+ # Get latest deployment
124
+ deployments = service['deployments'] || []
125
+ latest = deployments.first
126
+
127
+ if latest.nil?
128
+ return {
129
+ completed: false,
130
+ current_status: 'waiting'
131
+ }
132
+ end
133
+
134
+ status = latest['status']
135
+
136
+ case status
137
+ when 'SUCCESS', 'ACTIVE'
138
+ {
139
+ completed: true,
140
+ success: true,
141
+ current_status: status
142
+ }
143
+ when 'FAILED', 'CRASHED'
144
+ {
145
+ completed: true,
146
+ success: false,
147
+ current_status: status,
148
+ error_message: latest['error'] || 'Deployment failed'
149
+ }
150
+ else
151
+ {
152
+ completed: false,
153
+ current_status: status
154
+ }
155
+ end
156
+ rescue JSON::ParserError
157
+ {
158
+ completed: true,
159
+ success: false,
160
+ error_message: "Failed to parse deployment status"
161
+ }
162
+ end
163
+ end
164
+
165
+ # Escape shell arguments
166
+ #
167
+ # @param str [String] String to escape
168
+ # @return [String] Escaped string
169
+ def self.shell_escape(str)
170
+ "'#{str.gsub("'", "'\\\\''")}'"
171
+ end
172
+ end
173
+ end
174
+ end