aidp 0.5.0 → 0.8.0

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -151
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +471 -0
  5. data/lib/aidp/analysis/seams.rb +159 -0
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
  8. data/lib/aidp/analyze/error_handler.rb +2 -78
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/analyze/steps.rb +6 -0
  15. data/lib/aidp/cli/jobs_command.rb +103 -435
  16. data/lib/aidp/cli.rb +317 -191
  17. data/lib/aidp/config.rb +298 -10
  18. data/lib/aidp/debug_logger.rb +195 -0
  19. data/lib/aidp/debug_mixin.rb +187 -0
  20. data/lib/aidp/execute/progress.rb +9 -0
  21. data/lib/aidp/execute/runner.rb +221 -40
  22. data/lib/aidp/execute/steps.rb +17 -7
  23. data/lib/aidp/execute/workflow_selector.rb +211 -0
  24. data/lib/aidp/harness/completion_checker.rb +268 -0
  25. data/lib/aidp/harness/condition_detector.rb +1526 -0
  26. data/lib/aidp/harness/config_loader.rb +373 -0
  27. data/lib/aidp/harness/config_manager.rb +382 -0
  28. data/lib/aidp/harness/config_schema.rb +1006 -0
  29. data/lib/aidp/harness/config_validator.rb +355 -0
  30. data/lib/aidp/harness/configuration.rb +477 -0
  31. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  32. data/lib/aidp/harness/error_handler.rb +616 -0
  33. data/lib/aidp/harness/provider_config.rb +423 -0
  34. data/lib/aidp/harness/provider_factory.rb +306 -0
  35. data/lib/aidp/harness/provider_manager.rb +1269 -0
  36. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  37. data/lib/aidp/harness/runner.rb +411 -0
  38. data/lib/aidp/harness/state/errors.rb +28 -0
  39. data/lib/aidp/harness/state/metrics.rb +219 -0
  40. data/lib/aidp/harness/state/persistence.rb +128 -0
  41. data/lib/aidp/harness/state/provider_state.rb +132 -0
  42. data/lib/aidp/harness/state/ui_state.rb +68 -0
  43. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  44. data/lib/aidp/harness/state_manager.rb +586 -0
  45. data/lib/aidp/harness/status_display.rb +888 -0
  46. data/lib/aidp/harness/ui/base.rb +16 -0
  47. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  48. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  49. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  50. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  51. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  52. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  53. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  54. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  55. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  56. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  57. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  58. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  59. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  60. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  61. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  62. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  63. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  64. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  65. data/lib/aidp/harness/user_interface.rb +2381 -0
  66. data/lib/aidp/provider_manager.rb +131 -7
  67. data/lib/aidp/providers/anthropic.rb +28 -109
  68. data/lib/aidp/providers/base.rb +170 -0
  69. data/lib/aidp/providers/cursor.rb +52 -183
  70. data/lib/aidp/providers/gemini.rb +24 -109
  71. data/lib/aidp/providers/macos_ui.rb +99 -5
  72. data/lib/aidp/providers/opencode.rb +194 -0
  73. data/lib/aidp/storage/csv_storage.rb +172 -0
  74. data/lib/aidp/storage/file_manager.rb +214 -0
  75. data/lib/aidp/storage/json_storage.rb +140 -0
  76. data/lib/aidp/version.rb +1 -1
  77. data/lib/aidp.rb +56 -35
  78. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  79. data/templates/COMMON/AGENT_BASE.md +11 -0
  80. data/templates/EXECUTE/00_PRD.md +4 -4
  81. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  82. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  83. data/templates/EXECUTE/08_TASKS.md +4 -4
  84. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  85. data/templates/README.md +279 -0
  86. data/templates/aidp-development.yml.example +373 -0
  87. data/templates/aidp-minimal.yml.example +48 -0
  88. data/templates/aidp-production.yml.example +475 -0
  89. data/templates/aidp.yml.example +598 -0
  90. metadata +106 -64
  91. data/lib/aidp/analyze/agent_personas.rb +0 -71
  92. data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
  93. data/lib/aidp/analyze/data_retention_manager.rb +0 -426
  94. data/lib/aidp/analyze/database.rb +0 -260
  95. data/lib/aidp/analyze/dependencies.rb +0 -335
  96. data/lib/aidp/analyze/export_manager.rb +0 -425
  97. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  98. data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
  99. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  100. data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
  101. data/lib/aidp/analyze/memory_manager.rb +0 -365
  102. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  103. data/lib/aidp/analyze/parallel_processor.rb +0 -460
  104. data/lib/aidp/analyze/performance_optimizer.rb +0 -694
  105. data/lib/aidp/analyze/repository_chunker.rb +0 -704
  106. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  107. data/lib/aidp/analyze/storage.rb +0 -662
  108. data/lib/aidp/analyze/tool_configuration.rb +0 -456
  109. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  110. data/lib/aidp/database/pg_adapter.rb +0 -148
  111. data/lib/aidp/database_config.rb +0 -69
  112. data/lib/aidp/database_connection.rb +0 -72
  113. data/lib/aidp/database_migration.rb +0 -158
  114. data/lib/aidp/job_manager.rb +0 -41
  115. data/lib/aidp/jobs/base_job.rb +0 -47
  116. data/lib/aidp/jobs/provider_execution_job.rb +0 -96
  117. data/lib/aidp/project_detector.rb +0 -117
  118. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  119. data/lib/aidp/providers/supervised_base.rb +0 -317
  120. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  121. data/lib/aidp/sync.rb +0 -13
  122. data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+ require_relative "base"
5
+ require_relative "status_widget"
6
+ require_relative "spinner_group"
7
+ require_relative "frame_manager"
8
+
9
+ module Aidp
10
+ module Harness
11
+ module UI
12
+ # Real-time status updates using CLI UI spinners
13
+ class StatusManager < Base
14
+ class StatusError < StandardError; end
15
+ class InvalidStatusError < StatusError; end
16
+ class UpdateError < StatusError; end
17
+
18
+ def initialize(ui_components = {})
19
+ super()
20
+ @status_widget = ui_components[:status_widget] || StatusWidget.new
21
+ @spinner_group = ui_components[:spinner_group] || SpinnerGroup.new
22
+ @frame_manager = ui_components[:frame_manager] || FrameManager.new
23
+ @formatter = ui_components[:formatter] || StatusManagerFormatter.new
24
+
25
+ @active_statuses = {}
26
+ @status_history = []
27
+ end
28
+
29
+ def show_workflow_status(workflow_name, &block)
30
+ validate_workflow_name(workflow_name)
31
+
32
+ @frame_manager.workflow_frame(workflow_name) do
33
+ @status_widget.show_loading_status("Starting #{workflow_name}") do |spinner|
34
+ track_workflow_status(workflow_name, spinner, &block)
35
+ end
36
+ end
37
+ rescue => e
38
+ raise UpdateError, "Failed to show workflow status: #{e.message}"
39
+ end
40
+
41
+ def show_step_status(step_name, &block)
42
+ validate_step_name(step_name)
43
+
44
+ @frame_manager.step_frame(step_name, 1, 1) do
45
+ @status_widget.show_loading_status("Processing #{step_name}") do |spinner|
46
+ track_step_status(step_name, spinner, &block)
47
+ end
48
+ end
49
+ rescue => e
50
+ raise UpdateError, "Failed to show step status: #{e.message}"
51
+ end
52
+
53
+ def show_concurrent_statuses(operations, &block)
54
+ validate_operations(operations)
55
+
56
+ @frame_manager.section("Concurrent Operations") do
57
+ @spinner_group.run_concurrent_operations(operations, &block)
58
+ end
59
+ rescue => e
60
+ raise UpdateError, "Failed to show concurrent statuses: #{e.message}"
61
+ end
62
+
63
+ def update_status(status_id, message, type = :info)
64
+ validate_status_id(status_id)
65
+ validate_message(message)
66
+ validate_status_type(type)
67
+
68
+ status = @active_statuses[status_id]
69
+ raise InvalidStatusError, "Status #{status_id} not found" unless status
70
+
71
+ update_status_display(status, message, type)
72
+ record_status_update(status_id, message, type)
73
+ end
74
+
75
+ def create_status_tracker(name, initial_message = "Initializing...")
76
+ validate_tracker_name(name)
77
+ validate_message(initial_message)
78
+
79
+ status_id = generate_status_id(name)
80
+ status_tracker = create_status_instance(name, initial_message)
81
+ @active_statuses[status_id] = status_tracker
82
+
83
+ record_status_creation(status_id, name, initial_message)
84
+ status_id
85
+ end
86
+
87
+ def complete_status(status_id, final_message = "Completed")
88
+ validate_status_id(status_id)
89
+
90
+ status = @active_statuses[status_id]
91
+ raise InvalidStatusError, "Status #{status_id} not found" unless status
92
+
93
+ complete_status_display(status, final_message)
94
+ @active_statuses.delete(status_id)
95
+
96
+ record_status_completion(status_id, final_message)
97
+ end
98
+
99
+ def show_success_status(message)
100
+ @status_widget.show_success_status(message)
101
+ record_status_event(:success, message)
102
+ end
103
+
104
+ def show_error_status(message)
105
+ @status_widget.show_error_status(message)
106
+ record_status_event(:error, message)
107
+ end
108
+
109
+ def show_warning_status(message)
110
+ @status_widget.show_warning_status(message)
111
+ record_status_event(:warning, message)
112
+ end
113
+
114
+ def show_info_status(message)
115
+ @status_widget.show_info_status(message)
116
+ record_status_event(:info, message)
117
+ end
118
+
119
+ def get_status_summary
120
+ {
121
+ active_statuses: @active_statuses.size,
122
+ completed_statuses: @status_history.count { |h| h[:status] == "completed" },
123
+ total_statuses: @status_history.size,
124
+ status_history: @status_history.dup
125
+ }
126
+ end
127
+
128
+ def clear_status_history
129
+ @status_history.clear
130
+ end
131
+
132
+ private
133
+
134
+ def validate_workflow_name(workflow_name)
135
+ raise InvalidStatusError, "Workflow name cannot be empty" if workflow_name.to_s.strip.empty?
136
+ end
137
+
138
+ def validate_step_name(step_name)
139
+ raise InvalidStatusError, "Step name cannot be empty" if step_name.to_s.strip.empty?
140
+ end
141
+
142
+ def validate_operations(operations)
143
+ raise InvalidStatusError, "Operations must be an array" unless operations.is_a?(Array)
144
+ raise InvalidStatusError, "Operations array cannot be empty" if operations.empty?
145
+ end
146
+
147
+ def validate_status_id(status_id)
148
+ raise InvalidStatusError, "Status ID cannot be empty" if status_id.to_s.strip.empty?
149
+ end
150
+
151
+ def validate_message(message)
152
+ raise InvalidStatusError, "Message cannot be empty" if message.to_s.strip.empty?
153
+ end
154
+
155
+ def validate_status_type(type)
156
+ valid_types = [:info, :success, :warning, :error, :loading]
157
+ unless valid_types.include?(type)
158
+ raise InvalidStatusError, "Invalid status type: #{type}. Must be one of: #{valid_types.join(", ")}"
159
+ end
160
+ end
161
+
162
+ def validate_tracker_name(name)
163
+ raise InvalidStatusError, "Tracker name cannot be empty" if name.to_s.strip.empty?
164
+ end
165
+
166
+ def track_workflow_status(workflow_name, spinner, &block) # Will be updated dynamically
167
+ yield(spinner) if block_given?
168
+ @status_widget.show_success_status("Completed #{workflow_name}")
169
+ rescue => e
170
+ @status_widget.show_error_status("Failed #{workflow_name}: #{e.message}")
171
+ raise
172
+ end
173
+
174
+ def track_step_status(step_name, spinner, &block)
175
+ yield(spinner) if block_given?
176
+ @status_widget.show_success_status("Completed #{step_name}")
177
+ rescue => e
178
+ @status_widget.show_error_status("Failed #{step_name}: #{e.message}")
179
+ raise
180
+ end
181
+
182
+ def update_status_display(status, message, type)
183
+ status[:message] = message
184
+ status[:type] = type
185
+ status[:last_updated] = Time.now
186
+
187
+ # Update the actual status display if it exists
188
+ if status[:spinner]
189
+ @status_widget.update_status(status[:spinner], message)
190
+ end
191
+ end
192
+
193
+ def complete_status_display(status, final_message)
194
+ status[:message] = final_message
195
+ status[:status] = "completed"
196
+ status[:completed_at] = Time.now
197
+
198
+ # Show final status
199
+ case status[:type]
200
+ when :success
201
+ @status_widget.show_success_status(final_message)
202
+ when :error
203
+ @status_widget.show_error_status(final_message)
204
+ when :warning
205
+ @status_widget.show_warning_status(final_message)
206
+ else
207
+ @status_widget.show_info_status(final_message)
208
+ end
209
+ end
210
+
211
+ def create_status_instance(name, initial_message)
212
+ {
213
+ name: name,
214
+ message: initial_message,
215
+ type: :info,
216
+ created_at: Time.now,
217
+ last_updated: Time.now,
218
+ status: "active"
219
+ }
220
+ end
221
+
222
+ def generate_status_id(name)
223
+ "#{name.downcase.gsub(/\s+/, "_")}_#{Time.now.to_i}"
224
+ end
225
+
226
+ def record_status_creation(status_id, name, initial_message)
227
+ @status_history << {
228
+ status_id: status_id,
229
+ name: name,
230
+ message: initial_message,
231
+ status: "created",
232
+ timestamp: Time.now
233
+ }
234
+ end
235
+
236
+ def record_status_update(status_id, message, type)
237
+ @status_history << {
238
+ status_id: status_id,
239
+ message: message,
240
+ type: type,
241
+ status: "updated",
242
+ timestamp: Time.now
243
+ }
244
+ end
245
+
246
+ def record_status_completion(status_id, final_message)
247
+ @status_history << {
248
+ status_id: status_id,
249
+ message: final_message,
250
+ status: "completed",
251
+ timestamp: Time.now
252
+ }
253
+ end
254
+
255
+ def record_status_event(type, message)
256
+ @status_history << {
257
+ type: type,
258
+ message: message,
259
+ status: "event",
260
+ timestamp: Time.now
261
+ }
262
+ end
263
+ end
264
+
265
+ # Formats status management display
266
+ class StatusManagerFormatter
267
+ def initialize
268
+ @pastel = Pastel.new
269
+ end
270
+
271
+ def format_workflow_status(workflow_name)
272
+ @pastel.bold(@pastel.blue("🔄 #{workflow_name} Workflow"))
273
+ end
274
+
275
+ def format_step_status(step_name)
276
+ @pastel.bold(@pastel.green("⚡ #{step_name}"))
277
+ end
278
+
279
+ def format_status_message(message, type)
280
+ case type
281
+ when :success
282
+ @pastel.green("✅ #{message}")
283
+ when :error
284
+ @pastel.red("❌ #{message}")
285
+ when :warning
286
+ @pastel.yellow("⚠️ #{message}")
287
+ when :info
288
+ @pastel.blue("ℹ️ #{message}")
289
+ when :loading
290
+ @pastel.dim("⏳ #{message}")
291
+ else
292
+ @pastel.dim(message)
293
+ end
294
+ end
295
+
296
+ def format_status_summary(summary)
297
+ result = []
298
+ result << @pastel.bold(@pastel.blue("📊 Status Summary"))
299
+ result << "Active statuses: #{@pastel.bold(summary[:active_statuses])}"
300
+ result << "Completed statuses: #{@pastel.bold(summary[:completed_statuses])}"
301
+ result << "Total statuses: #{@pastel.bold(summary[:total_statuses])}"
302
+ result.join("\n")
303
+ end
304
+
305
+ def format_status_tracker(tracker)
306
+ status_emoji = (tracker[:status] == "completed") ? "✅" : "🔄"
307
+ "#{status_emoji} #{@pastel.bold(tracker[:name])} - #{@pastel.dim(tracker[:message])}"
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-spinner"
4
+ require "pastel"
5
+ require_relative "base"
6
+
7
+ module Aidp
8
+ module Harness
9
+ module UI
10
+ # Handles status display using CLI UI spinners
11
+ class StatusWidget < Base
12
+ class StatusError < StandardError; end
13
+ class InvalidStatusError < StatusError; end
14
+ class DisplayError < StatusError; end
15
+
16
+ def initialize(ui_components = {})
17
+ super()
18
+ @spinner = ui_components[:spinner] || TTY::Spinner
19
+ @pastel = Pastel.new
20
+ @formatter = ui_components[:formatter] || StatusFormatter.new
21
+ @status_history = []
22
+ @current_spinner = nil
23
+ @spinner_active = false
24
+ end
25
+
26
+ def show_status(message, &block)
27
+ validate_message(message)
28
+
29
+ formatted_message = @formatter.format_status_message(message)
30
+ @spinner.spin(formatted_message) do |spinner|
31
+ yield(spinner) if block_given?
32
+ end
33
+ rescue => e
34
+ raise DisplayError, "Failed to show status: #{e.message}"
35
+ end
36
+
37
+ def update_status(spinner, message)
38
+ validate_spinner_and_message(spinner, message)
39
+
40
+ formatted_message = @formatter.format_status_message(message)
41
+ spinner.update_title(formatted_message)
42
+ rescue => e
43
+ raise DisplayError, "Failed to update status: #{e.message}"
44
+ end
45
+
46
+ def show_loading_status(operation_name, &block)
47
+ validate_operation_name(operation_name)
48
+
49
+ message = @formatter.format_loading_message(operation_name)
50
+ show_status(message, &block)
51
+ rescue => e
52
+ raise DisplayError, "Failed to show loading status: #{e.message}"
53
+ end
54
+
55
+ def show_success_status(message)
56
+ validate_message(message)
57
+
58
+ formatted_message = @formatter.format_success_message(message)
59
+ puts(formatted_message)
60
+ rescue => e
61
+ raise DisplayError, "Failed to show success status: #{e.message}"
62
+ end
63
+
64
+ def show_error_status(message)
65
+ validate_message(message)
66
+
67
+ formatted_message = @formatter.format_error_message(message)
68
+ puts(formatted_message)
69
+ rescue => e
70
+ raise DisplayError, "Failed to show error status: #{e.message}"
71
+ end
72
+
73
+ def show_warning_status(message)
74
+ validate_message(message)
75
+
76
+ formatted_message = @formatter.format_warning_message(message)
77
+ puts(formatted_message)
78
+ rescue => e
79
+ raise DisplayError, "Failed to show warning status: #{e.message}"
80
+ end
81
+
82
+ # Methods expected by tests
83
+ def display_status(status_type, message, error_data = nil)
84
+ validate_status_type(status_type)
85
+ validate_message(message)
86
+
87
+ case status_type
88
+ when :loading
89
+ display_loading_status(message)
90
+ when :success
91
+ display_success_status(message)
92
+ when :error
93
+ display_error_status(message, error_data)
94
+ when :warning
95
+ display_warning_status(message)
96
+ end
97
+
98
+ record_status_history(status_type, message, error_data)
99
+ rescue InvalidStatusError => e
100
+ raise e
101
+ rescue => e
102
+ raise DisplayError, "Failed to display status: #{e.message}"
103
+ end
104
+
105
+ def start_spinner(message)
106
+ validate_message(message)
107
+ stop_spinner if @spinner_active
108
+
109
+ @spinner_active = true
110
+ @current_spinner = @spinner.new("⏳ #{message} :spinner", format: :pulse)
111
+ @current_spinner.start
112
+ end
113
+
114
+ def stop_spinner
115
+ return unless @spinner_active
116
+
117
+ @current_spinner&.stop
118
+ @spinner_active = false
119
+ @current_spinner = nil
120
+ end
121
+
122
+ def update_spinner_message(message)
123
+ validate_message(message)
124
+ raise DisplayError, "No active spinner to update" unless @spinner_active
125
+
126
+ @current_spinner&.stop
127
+ @current_spinner = @spinner.new("⏳ #{message} :spinner", format: :pulse)
128
+ @current_spinner.start
129
+ end
130
+
131
+ def display_status_with_duration(status_type, message, duration)
132
+ validate_status_type(status_type)
133
+ validate_message(message)
134
+
135
+ formatted_duration = format_duration(duration)
136
+ message_with_duration = "#{message} (#{formatted_duration})"
137
+ display_status(status_type, message_with_duration)
138
+ end
139
+
140
+ def display_multiple_status(status_items)
141
+ return if status_items.empty?
142
+
143
+ status_items.each do |item|
144
+ display_status(item[:type], item[:message], item[:error_data])
145
+ end
146
+ end
147
+
148
+ def get_status_history
149
+ @status_history.dup
150
+ end
151
+
152
+ def clear_status_history
153
+ @status_history.clear
154
+ end
155
+
156
+ def format_duration(seconds)
157
+ return "0s" if seconds <= 0
158
+
159
+ if seconds < 60
160
+ "#{seconds.round(1)}s"
161
+ elsif seconds < 3600
162
+ minutes = (seconds / 60).floor
163
+ remaining_seconds = (seconds % 60).floor
164
+ "#{minutes}m #{remaining_seconds}s"
165
+ else
166
+ hours = (seconds / 3600).floor
167
+ minutes = ((seconds % 3600) / 60).floor
168
+ remaining_seconds = (seconds % 60).floor
169
+ "#{hours}h #{minutes}m #{remaining_seconds}s"
170
+ end
171
+ end
172
+
173
+ def spinner_active?
174
+ @spinner_active
175
+ end
176
+
177
+ def current_spinner_message
178
+ return nil unless @spinner_active
179
+ @current_spinner&.message&.to_s&.gsub(/⏳\s+|\s+:spinner/, "")
180
+ end
181
+
182
+ private
183
+
184
+ def validate_message(message)
185
+ raise InvalidStatusError, "Message cannot be empty" if message.to_s.strip.empty?
186
+ end
187
+
188
+ def validate_spinner_and_message(spinner, message)
189
+ raise InvalidStatusError, "Spinner cannot be nil" if spinner.nil?
190
+ validate_message(message)
191
+ end
192
+
193
+ def validate_operation_name(operation_name)
194
+ raise InvalidStatusError, "Operation name cannot be empty" if operation_name.to_s.strip.empty?
195
+ end
196
+
197
+ def validate_status_type(status_type)
198
+ valid_types = [:loading, :success, :error, :warning]
199
+ unless valid_types.include?(status_type)
200
+ raise InvalidStatusError, "Invalid status type: #{status_type}. Must be one of: #{valid_types.join(", ")}"
201
+ end
202
+ end
203
+
204
+ def record_status_history(status_type, message, error_data)
205
+ @status_history << {
206
+ type: status_type,
207
+ message: message,
208
+ error_data: error_data,
209
+ timestamp: Time.now
210
+ }
211
+ end
212
+
213
+ def display_loading_status(message)
214
+ puts("⏳ #{message}")
215
+ end
216
+
217
+ def display_success_status(message)
218
+ puts("✅ #{message}")
219
+ end
220
+
221
+ def display_error_status(message, error_data)
222
+ puts("❌ #{message}")
223
+ if error_data && error_data[:message]
224
+ puts(" #{error_data[:message]}")
225
+ end
226
+ end
227
+
228
+ def display_warning_status(message)
229
+ puts("⚠️ #{message}")
230
+ end
231
+ end
232
+
233
+ # Formats status display messages
234
+ class StatusFormatter
235
+ def initialize
236
+ @pastel = Pastel.new
237
+ end
238
+
239
+ def format_status_message(message)
240
+ "⏳ #{message}"
241
+ end
242
+
243
+ def format_loading_message(operation_name)
244
+ "Loading #{operation_name}..."
245
+ end
246
+
247
+ def format_success_message(message)
248
+ "#{@pastel.green("✓")} #{message}"
249
+ end
250
+
251
+ def format_error_message(message)
252
+ "#{@pastel.red("✗")} #{message}"
253
+ end
254
+
255
+ def format_warning_message(message)
256
+ "#{@pastel.yellow("⚠")} #{message}"
257
+ end
258
+
259
+ def format_info_message(message)
260
+ "#{@pastel.blue("ℹ")} #{message}"
261
+ end
262
+
263
+ def format_step_message(step_name, status)
264
+ case status
265
+ when :starting
266
+ "Starting #{step_name}..."
267
+ when :in_progress
268
+ "Processing #{step_name}..."
269
+ when :completed
270
+ "Completed #{step_name}"
271
+ when :failed
272
+ "Failed #{step_name}"
273
+ else
274
+ "#{step_name}: #{status}"
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end