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,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
require_relative "base"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
module Harness
|
8
|
+
module UI
|
9
|
+
# Handles interactive question collection using CLI UI prompts
|
10
|
+
class QuestionCollector < Base
|
11
|
+
class QuestionError < StandardError; end
|
12
|
+
class ValidationError < QuestionError; end
|
13
|
+
class CollectionError < QuestionError; end
|
14
|
+
|
15
|
+
def initialize(ui_components = {})
|
16
|
+
super()
|
17
|
+
@prompt = ui_components[:prompt] || TTY::Prompt.new
|
18
|
+
@validator = ui_components[:validator] || QuestionValidator.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def collect_questions(questions)
|
22
|
+
validate_questions_input(questions)
|
23
|
+
|
24
|
+
# Validate all questions first
|
25
|
+
errors = get_validation_errors(questions)
|
26
|
+
raise ValidationError, errors.join("\n") unless errors.empty?
|
27
|
+
|
28
|
+
responses = {}
|
29
|
+
questions.each_with_index do |question, index|
|
30
|
+
question_key = question[:key] || "question_#{index + 1}"
|
31
|
+
responses[question_key] = collect_single_question(question, index + 1)
|
32
|
+
end
|
33
|
+
responses
|
34
|
+
rescue ValidationError => e
|
35
|
+
raise e
|
36
|
+
rescue => e
|
37
|
+
raise CollectionError, "Failed to collect questions: #{e.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def collect_single_question(question, number)
|
41
|
+
validate_question_format(question)
|
42
|
+
|
43
|
+
question_text = format_question_text(question, number)
|
44
|
+
response = prompt_for_response(question_text, question)
|
45
|
+
|
46
|
+
validate_response(response, question)
|
47
|
+
response
|
48
|
+
rescue => e
|
49
|
+
raise QuestionError, "Failed to collect question #{number}: #{e.message}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_questions(questions)
|
53
|
+
return true if questions.empty?
|
54
|
+
|
55
|
+
questions.all? do |question|
|
56
|
+
validate_question_format(question)
|
57
|
+
true
|
58
|
+
rescue ValidationError
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_validation_errors(questions)
|
64
|
+
errors = []
|
65
|
+
|
66
|
+
questions.each_with_index do |question, index|
|
67
|
+
validate_question_format(question)
|
68
|
+
rescue ValidationError => e
|
69
|
+
errors << "Question #{index + 1}: #{e.message}"
|
70
|
+
end
|
71
|
+
|
72
|
+
errors
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_question_format(question)
|
76
|
+
raise ValidationError, "Question must be a hash" unless question.is_a?(Hash)
|
77
|
+
raise ValidationError, "Question must have :text key" unless question.key?(:text)
|
78
|
+
raise ValidationError, "Question text cannot be empty" if question[:text].to_s.strip.empty?
|
79
|
+
unless question.key?(:type) && question.key?(:required)
|
80
|
+
raise ValidationError, "Question missing required fields"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_questions_input(questions)
|
85
|
+
raise ValidationError, "Questions must be an array" unless questions.is_a?(Array)
|
86
|
+
# Allow empty array - return empty hash
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def validate_response(response, question)
|
92
|
+
@validator.validate(response, question)
|
93
|
+
end
|
94
|
+
|
95
|
+
def format_question_text(question, number)
|
96
|
+
"Question #{number}: #{question[:text]}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def prompt_for_response(question_text, question)
|
100
|
+
@prompt.ask(question_text) do |handler|
|
101
|
+
add_question_options(handler, question)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_question_options(handler, question)
|
106
|
+
return unless question[:options]
|
107
|
+
|
108
|
+
question[:options].each { |option| handler.option(option) }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Validates question responses
|
113
|
+
class QuestionValidator
|
114
|
+
def validate(response, question)
|
115
|
+
validate_required(response, question)
|
116
|
+
validate_format(response, question)
|
117
|
+
validate_options(response, question)
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def validate_required(response, question)
|
123
|
+
return unless question[:required]
|
124
|
+
raise ValidationError, "Response is required" if response.nil? || response.to_s.strip.empty?
|
125
|
+
end
|
126
|
+
|
127
|
+
def validate_format(response, question)
|
128
|
+
return unless question[:format]
|
129
|
+
return if response.to_s.match?(question[:format])
|
130
|
+
raise ValidationError, "Response format is invalid"
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_options(response, question)
|
134
|
+
return unless question[:options]
|
135
|
+
return if question[:options].include?(response)
|
136
|
+
raise ValidationError, "Response must be one of: #{question[:options].join(", ")}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-spinner"
|
4
|
+
require_relative "base"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
module Harness
|
8
|
+
module UI
|
9
|
+
# Handles concurrent operations using CLI UI spinner groups
|
10
|
+
class SpinnerGroup < Base
|
11
|
+
class SpinnerGroupError < StandardError; end
|
12
|
+
class InvalidOperationError < SpinnerGroupError; end
|
13
|
+
class ExecutionError < SpinnerGroupError; end
|
14
|
+
|
15
|
+
def initialize(ui_components = {})
|
16
|
+
super()
|
17
|
+
@spinner_class = ui_components[:spinner_class] || TTY::Spinner
|
18
|
+
@formatter = ui_components[:formatter] || SpinnerGroupFormatter.new
|
19
|
+
@spinners = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def run_concurrent_operations(operations)
|
23
|
+
validate_operations(operations)
|
24
|
+
|
25
|
+
# Create individual spinners for each operation
|
26
|
+
operations.each do |operation|
|
27
|
+
spinner = @spinner_class.new(
|
28
|
+
"#{operation[:name]}...",
|
29
|
+
format: :dots,
|
30
|
+
success_mark: "✓",
|
31
|
+
error_mark: "✗"
|
32
|
+
)
|
33
|
+
@spinners[operation[:id]] = spinner
|
34
|
+
spinner.start
|
35
|
+
end
|
36
|
+
|
37
|
+
# Execute operations
|
38
|
+
operations.each do |operation|
|
39
|
+
operation[:block].call
|
40
|
+
@spinners[operation[:id]].success(operation[:name])
|
41
|
+
rescue => e
|
42
|
+
@spinners[operation[:id]].error(operation[:name])
|
43
|
+
raise e
|
44
|
+
end
|
45
|
+
rescue => e
|
46
|
+
raise ExecutionError, "Failed to run concurrent operations: #{e.message}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def run_workflow_steps(steps)
|
50
|
+
validate_steps(steps)
|
51
|
+
|
52
|
+
operations = convert_steps_to_operations(steps)
|
53
|
+
run_concurrent_operations(operations)
|
54
|
+
rescue => e
|
55
|
+
raise ExecutionError, "Failed to run workflow steps: #{e.message}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_analysis_tasks(tasks)
|
59
|
+
validate_tasks(tasks)
|
60
|
+
|
61
|
+
operations = convert_tasks_to_operations(tasks)
|
62
|
+
run_concurrent_operations(operations)
|
63
|
+
rescue => e
|
64
|
+
raise ExecutionError, "Failed to run analysis tasks: #{e.message}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def run_provider_operations(provider_operations)
|
68
|
+
validate_provider_operations(provider_operations)
|
69
|
+
|
70
|
+
operations = convert_provider_operations(provider_operations)
|
71
|
+
run_concurrent_operations(operations)
|
72
|
+
rescue => e
|
73
|
+
raise ExecutionError, "Failed to run provider operations: #{e.message}"
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def validate_operations(operations)
|
79
|
+
raise InvalidOperationError, "Operations must be an array" unless operations.is_a?(Array)
|
80
|
+
raise InvalidOperationError, "Operations array cannot be empty" if operations.empty?
|
81
|
+
|
82
|
+
operations.each_with_index do |operation, index|
|
83
|
+
validate_operation(operation, index)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_operation(operation, index)
|
88
|
+
raise InvalidOperationError, "Operation #{index} must be a hash" unless operation.is_a?(Hash)
|
89
|
+
raise InvalidOperationError, "Operation #{index} must have :title" unless operation.key?(:title)
|
90
|
+
raise InvalidOperationError, "Operation #{index} must have :block" unless operation.key?(:block)
|
91
|
+
raise InvalidOperationError, "Operation #{index} title cannot be empty" if operation[:title].to_s.strip.empty?
|
92
|
+
raise InvalidOperationError, "Operation #{index} block must be callable" unless operation[:block].respond_to?(:call)
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_steps(steps)
|
96
|
+
raise InvalidOperationError, "Steps must be an array" unless steps.is_a?(Array)
|
97
|
+
raise InvalidOperationError, "Steps array cannot be empty" if steps.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_tasks(tasks)
|
101
|
+
raise InvalidOperationError, "Tasks must be an array" unless tasks.is_a?(Array)
|
102
|
+
raise InvalidOperationError, "Tasks array cannot be empty" if tasks.empty?
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate_provider_operations(provider_operations)
|
106
|
+
raise InvalidOperationError, "Provider operations must be an array" unless provider_operations.is_a?(Array)
|
107
|
+
raise InvalidOperationError, "Provider operations array cannot be empty" if provider_operations.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_operation(spin_group, operation)
|
111
|
+
formatted_title = @formatter.format_operation_title(operation[:title])
|
112
|
+
spin_group.add(formatted_title) do |spinner|
|
113
|
+
execute_operation_with_error_handling(operation, spinner)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def execute_operation_with_error_handling(operation, spinner)
|
118
|
+
operation[:block].call(spinner)
|
119
|
+
rescue => e
|
120
|
+
spinner.update_title(@formatter.format_error_title(operation[:title], e.message))
|
121
|
+
raise
|
122
|
+
end
|
123
|
+
|
124
|
+
def convert_steps_to_operations(steps)
|
125
|
+
steps.map do |step|
|
126
|
+
{
|
127
|
+
title: @formatter.format_step_title(step[:name]),
|
128
|
+
block: step[:block]
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def convert_tasks_to_operations(tasks)
|
134
|
+
tasks.map do |task|
|
135
|
+
{
|
136
|
+
title: @formatter.format_task_title(task[:name]),
|
137
|
+
block: task[:block]
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def convert_provider_operations(provider_operations)
|
143
|
+
provider_operations.map do |op|
|
144
|
+
{
|
145
|
+
title: @formatter.format_provider_title(op[:provider], op[:operation]),
|
146
|
+
block: op[:block]
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Formats spinner group display text
|
153
|
+
class SpinnerGroupFormatter
|
154
|
+
def format_operation_title(title)
|
155
|
+
"🔄 #{title}"
|
156
|
+
end
|
157
|
+
|
158
|
+
def format_step_title(step_name)
|
159
|
+
"⚡ #{step_name}"
|
160
|
+
end
|
161
|
+
|
162
|
+
def format_task_title(task_name)
|
163
|
+
"📋 #{task_name}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def format_provider_title(provider_name, operation)
|
167
|
+
"🤖 #{provider_name}: #{operation}"
|
168
|
+
end
|
169
|
+
|
170
|
+
def format_error_title(original_title, error_message)
|
171
|
+
"❌ #{original_title} (Error: #{error_message})"
|
172
|
+
end
|
173
|
+
|
174
|
+
def format_success_title(original_title)
|
175
|
+
"✅ #{original_title}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def format_progress_title(title, current, total)
|
179
|
+
"📊 #{title} (#{current}/#{total})"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-spinner"
|
4
|
+
require "pastel"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
module Harness
|
8
|
+
module UI
|
9
|
+
# Unified spinner helper that automatically manages TTY::Spinner lifecycle
|
10
|
+
# Usage: with_spinner("Loading...") { some_operation }
|
11
|
+
class SpinnerHelper
|
12
|
+
class SpinnerError < StandardError; end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@pastel = Pastel.new
|
16
|
+
@active_spinners = []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Main method: automatically manages spinner around a block
|
20
|
+
def with_spinner(message, format: :dots, success_message: nil, error_message: nil, &block)
|
21
|
+
raise ArgumentError, "Block required for with_spinner" unless block_given?
|
22
|
+
|
23
|
+
spinner = create_spinner(message, format)
|
24
|
+
start_spinner(spinner)
|
25
|
+
|
26
|
+
begin
|
27
|
+
result = yield(spinner)
|
28
|
+
success_spinner(spinner, success_message || message)
|
29
|
+
result
|
30
|
+
rescue => e
|
31
|
+
error_spinner(spinner, error_message || "Failed: #{e.message}")
|
32
|
+
raise e
|
33
|
+
ensure
|
34
|
+
cleanup_spinner(spinner)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convenience methods for common patterns
|
39
|
+
def with_loading_spinner(message, &block)
|
40
|
+
with_spinner("⏳ #{message}", format: :dots, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_processing_spinner(message, &block)
|
44
|
+
with_spinner("🔄 #{message}", format: :pulse, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_saving_spinner(message, &block)
|
48
|
+
with_spinner("💾 #{message}", format: :dots, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def with_analyzing_spinner(message, &block)
|
52
|
+
with_spinner("🔍 #{message}", format: :dots, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def with_building_spinner(message, &block)
|
56
|
+
with_spinner("🏗️ #{message}", format: :dots, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# For operations that might take a while
|
60
|
+
def with_long_operation_spinner(message, &block)
|
61
|
+
with_spinner("⏳ #{message}", format: :pulse, &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
# For quick operations
|
65
|
+
def with_quick_spinner(message, &block)
|
66
|
+
with_spinner("⚡ #{message}", format: :dots, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Update spinner message during operation
|
70
|
+
def update_spinner_message(spinner, new_message)
|
71
|
+
spinner.update_title(new_message)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Check if any spinners are active
|
75
|
+
def any_active?
|
76
|
+
@active_spinners.any?(&:spinning?)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get count of active spinners
|
80
|
+
def active_count
|
81
|
+
@active_spinners.count(&:spinning?)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Force stop all spinners (emergency cleanup)
|
85
|
+
def stop_all
|
86
|
+
@active_spinners.each do |spinner|
|
87
|
+
spinner.stop if spinner.spinning?
|
88
|
+
end
|
89
|
+
@active_spinners.clear
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def create_spinner(message, format)
|
95
|
+
TTY::Spinner.new(
|
96
|
+
"#{message} :spinner",
|
97
|
+
format: format,
|
98
|
+
success_mark: @pastel.green("✓"),
|
99
|
+
error_mark: @pastel.red("✗"),
|
100
|
+
hide_cursor: true
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
def start_spinner(spinner)
|
105
|
+
@active_spinners << spinner
|
106
|
+
spinner.start
|
107
|
+
end
|
108
|
+
|
109
|
+
def success_spinner(spinner, message)
|
110
|
+
spinner.success(@pastel.green("✓ #{message}"))
|
111
|
+
end
|
112
|
+
|
113
|
+
def error_spinner(spinner, message)
|
114
|
+
spinner.error(@pastel.red("✗ #{message}"))
|
115
|
+
end
|
116
|
+
|
117
|
+
def cleanup_spinner(spinner)
|
118
|
+
@active_spinners.delete(spinner)
|
119
|
+
# TTY::Spinner handles its own cleanup
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Global instance for easy access
|
124
|
+
SPINNER = SpinnerHelper.new
|
125
|
+
|
126
|
+
# Convenience methods for global access
|
127
|
+
def self.with_spinner(message, **options, &block)
|
128
|
+
SPINNER.with_spinner(message, **options, &block)
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.with_loading_spinner(message, &block)
|
132
|
+
SPINNER.with_loading_spinner(message, &block)
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.with_processing_spinner(message, &block)
|
136
|
+
SPINNER.with_processing_spinner(message, &block)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.with_saving_spinner(message, &block)
|
140
|
+
SPINNER.with_saving_spinner(message, &block)
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.with_analyzing_spinner(message, &block)
|
144
|
+
SPINNER.with_analyzing_spinner(message, &block)
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.with_building_spinner(message, &block)
|
148
|
+
SPINNER.with_building_spinner(message, &block)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|