aidp 0.7.0 → 0.8.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 +4 -4
- data/README.md +60 -214
- data/bin/aidp +1 -1
- data/lib/aidp/analysis/kb_inspector.rb +38 -23
- data/lib/aidp/analysis/seams.rb +2 -31
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +1 -13
- data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
- data/lib/aidp/analyze/error_handler.rb +2 -75
- data/lib/aidp/analyze/json_file_storage.rb +292 -0
- data/lib/aidp/analyze/progress.rb +12 -0
- data/lib/aidp/analyze/progress_visualizer.rb +12 -17
- data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
- data/lib/aidp/analyze/runner.rb +256 -87
- data/lib/aidp/cli/jobs_command.rb +100 -432
- data/lib/aidp/cli.rb +309 -239
- data/lib/aidp/config.rb +298 -10
- data/lib/aidp/debug_logger.rb +195 -0
- data/lib/aidp/debug_mixin.rb +187 -0
- data/lib/aidp/execute/progress.rb +9 -0
- data/lib/aidp/execute/runner.rb +221 -40
- data/lib/aidp/execute/steps.rb +17 -7
- data/lib/aidp/execute/workflow_selector.rb +211 -0
- data/lib/aidp/harness/completion_checker.rb +268 -0
- data/lib/aidp/harness/condition_detector.rb +1526 -0
- data/lib/aidp/harness/config_loader.rb +373 -0
- data/lib/aidp/harness/config_manager.rb +382 -0
- data/lib/aidp/harness/config_schema.rb +1006 -0
- data/lib/aidp/harness/config_validator.rb +355 -0
- data/lib/aidp/harness/configuration.rb +477 -0
- data/lib/aidp/harness/enhanced_runner.rb +494 -0
- data/lib/aidp/harness/error_handler.rb +616 -0
- data/lib/aidp/harness/provider_config.rb +423 -0
- data/lib/aidp/harness/provider_factory.rb +306 -0
- data/lib/aidp/harness/provider_manager.rb +1269 -0
- data/lib/aidp/harness/provider_type_checker.rb +88 -0
- data/lib/aidp/harness/runner.rb +411 -0
- data/lib/aidp/harness/state/errors.rb +28 -0
- data/lib/aidp/harness/state/metrics.rb +219 -0
- data/lib/aidp/harness/state/persistence.rb +128 -0
- data/lib/aidp/harness/state/provider_state.rb +132 -0
- data/lib/aidp/harness/state/ui_state.rb +68 -0
- data/lib/aidp/harness/state/workflow_state.rb +123 -0
- data/lib/aidp/harness/state_manager.rb +586 -0
- data/lib/aidp/harness/status_display.rb +888 -0
- data/lib/aidp/harness/ui/base.rb +16 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
- data/lib/aidp/harness/ui/error_handler.rb +132 -0
- data/lib/aidp/harness/ui/frame_manager.rb +361 -0
- data/lib/aidp/harness/ui/job_monitor.rb +500 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
- data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
- data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
- data/lib/aidp/harness/ui/progress_display.rb +280 -0
- data/lib/aidp/harness/ui/question_collector.rb +141 -0
- data/lib/aidp/harness/ui/spinner_group.rb +184 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
- data/lib/aidp/harness/ui/status_manager.rb +312 -0
- data/lib/aidp/harness/ui/status_widget.rb +280 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
- data/lib/aidp/harness/user_interface.rb +2381 -0
- data/lib/aidp/provider_manager.rb +131 -7
- data/lib/aidp/providers/anthropic.rb +28 -103
- data/lib/aidp/providers/base.rb +170 -0
- data/lib/aidp/providers/cursor.rb +52 -181
- data/lib/aidp/providers/gemini.rb +24 -107
- data/lib/aidp/providers/macos_ui.rb +99 -5
- data/lib/aidp/providers/opencode.rb +194 -0
- data/lib/aidp/storage/csv_storage.rb +172 -0
- data/lib/aidp/storage/file_manager.rb +214 -0
- data/lib/aidp/storage/json_storage.rb +140 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +54 -39
- data/templates/COMMON/AGENT_BASE.md +11 -0
- data/templates/EXECUTE/00_PRD.md +4 -4
- data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
- data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
- data/templates/EXECUTE/08_TASKS.md +4 -4
- data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
- data/templates/README.md +279 -0
- data/templates/aidp-development.yml.example +373 -0
- data/templates/aidp-minimal.yml.example +48 -0
- data/templates/aidp-production.yml.example +475 -0
- data/templates/aidp.yml.example +598 -0
- metadata +93 -69
- data/lib/aidp/analyze/agent_personas.rb +0 -71
- data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
- data/lib/aidp/analyze/data_retention_manager.rb +0 -421
- data/lib/aidp/analyze/database.rb +0 -260
- data/lib/aidp/analyze/dependencies.rb +0 -335
- data/lib/aidp/analyze/export_manager.rb +0 -418
- data/lib/aidp/analyze/focus_guidance.rb +0 -517
- data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
- data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
- data/lib/aidp/analyze/memory_manager.rb +0 -339
- data/lib/aidp/analyze/metrics_storage.rb +0 -336
- data/lib/aidp/analyze/parallel_processor.rb +0 -454
- data/lib/aidp/analyze/performance_optimizer.rb +0 -691
- data/lib/aidp/analyze/repository_chunker.rb +0 -697
- data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
- data/lib/aidp/analyze/storage.rb +0 -655
- data/lib/aidp/analyze/tool_configuration.rb +0 -441
- data/lib/aidp/analyze/tool_modernization.rb +0 -750
- data/lib/aidp/database/pg_adapter.rb +0 -148
- data/lib/aidp/database_config.rb +0 -69
- data/lib/aidp/database_connection.rb +0 -72
- data/lib/aidp/job_manager.rb +0 -41
- data/lib/aidp/jobs/base_job.rb +0 -45
- data/lib/aidp/jobs/provider_execution_job.rb +0 -83
- data/lib/aidp/project_detector.rb +0 -117
- data/lib/aidp/providers/agent_supervisor.rb +0 -348
- data/lib/aidp/providers/supervised_base.rb +0 -317
- data/lib/aidp/providers/supervised_cursor.rb +0 -22
- data/lib/aidp/sync.rb +0 -13
- 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
|