aidp 0.7.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.
- 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 +0 -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,2381 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "readline"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Harness
|
7
|
+
# Handles user interaction and feedback collection
|
8
|
+
class UserInterface
|
9
|
+
def initialize
|
10
|
+
@input_history = []
|
11
|
+
@file_selection_enabled = false
|
12
|
+
@control_interface_enabled = true
|
13
|
+
@pause_requested = false
|
14
|
+
@stop_requested = false
|
15
|
+
@resume_requested = false
|
16
|
+
@control_thread = nil
|
17
|
+
@control_mutex = Mutex.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Collect user feedback for a list of questions
|
21
|
+
def collect_feedback(questions, context = nil)
|
22
|
+
responses = {}
|
23
|
+
|
24
|
+
# Display context if provided
|
25
|
+
if context
|
26
|
+
display_feedback_context(context)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Display question presentation header
|
30
|
+
display_question_presentation_header(questions, context)
|
31
|
+
|
32
|
+
# Process questions with advanced presentation
|
33
|
+
questions.each_with_index do |question_data, index|
|
34
|
+
question_number = question_data[:number] || (index + 1)
|
35
|
+
|
36
|
+
# Display question with advanced formatting
|
37
|
+
display_numbered_question(question_data, question_number, index + 1, questions.length)
|
38
|
+
|
39
|
+
# Get user response based on question type
|
40
|
+
response = get_question_response(question_data, question_number)
|
41
|
+
|
42
|
+
# Validate response if required
|
43
|
+
if question_data[:required] != false && (response.nil? || response.to_s.strip.empty?)
|
44
|
+
puts "ā This question is required. Please provide a response."
|
45
|
+
redo
|
46
|
+
end
|
47
|
+
|
48
|
+
responses["question_#{question_number}"] = response
|
49
|
+
|
50
|
+
# Show progress indicator
|
51
|
+
display_question_progress(index + 1, questions.length)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Display completion summary
|
55
|
+
display_question_completion_summary(responses, questions)
|
56
|
+
responses
|
57
|
+
end
|
58
|
+
|
59
|
+
# Display feedback context
|
60
|
+
def display_feedback_context(context)
|
61
|
+
puts "\nš Context:"
|
62
|
+
puts "-" * 30
|
63
|
+
|
64
|
+
if context[:type]
|
65
|
+
puts "Type: #{context[:type]}"
|
66
|
+
end
|
67
|
+
|
68
|
+
if context[:urgency]
|
69
|
+
urgency_emojis = {
|
70
|
+
"high" => "š“",
|
71
|
+
"medium" => "š”",
|
72
|
+
"low" => "š¢"
|
73
|
+
}
|
74
|
+
urgency_emoji = urgency_emojis[context[:urgency]] || "ā¹ļø"
|
75
|
+
puts "Urgency: #{urgency_emoji} #{context[:urgency].capitalize}"
|
76
|
+
end
|
77
|
+
|
78
|
+
if context[:description]
|
79
|
+
puts "Description: #{context[:description]}"
|
80
|
+
end
|
81
|
+
|
82
|
+
if context[:agent_output]
|
83
|
+
puts "\nAgent Output:"
|
84
|
+
puts context[:agent_output]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Display question presentation header
|
89
|
+
def display_question_presentation_header(questions, context)
|
90
|
+
puts "\nš¤ Agent needs your feedback:"
|
91
|
+
puts "=" * 60
|
92
|
+
|
93
|
+
# Display question overview
|
94
|
+
display_question_overview(questions)
|
95
|
+
|
96
|
+
# Display context summary if available
|
97
|
+
if context
|
98
|
+
display_context_summary(context)
|
99
|
+
end
|
100
|
+
|
101
|
+
puts "\nš Questions to answer:"
|
102
|
+
puts "-" * 40
|
103
|
+
end
|
104
|
+
|
105
|
+
# Display question overview
|
106
|
+
def display_question_overview(questions)
|
107
|
+
total_questions = questions.length
|
108
|
+
required_questions = questions.count { |q| q[:required] != false }
|
109
|
+
optional_questions = total_questions - required_questions
|
110
|
+
|
111
|
+
question_types = questions.map { |q| q[:type] || "text" }.uniq
|
112
|
+
|
113
|
+
puts "š Overview:"
|
114
|
+
puts " Total questions: #{total_questions}"
|
115
|
+
puts " Required: #{required_questions}"
|
116
|
+
puts " Optional: #{optional_questions}"
|
117
|
+
puts " Question types: #{question_types.join(", ")}"
|
118
|
+
|
119
|
+
# Estimate completion time
|
120
|
+
estimated_time = estimate_completion_time(questions)
|
121
|
+
puts " Estimated time: #{estimated_time}"
|
122
|
+
end
|
123
|
+
|
124
|
+
# Display context summary
|
125
|
+
def display_context_summary(context)
|
126
|
+
puts "\nš Context Summary:"
|
127
|
+
|
128
|
+
if context[:type]
|
129
|
+
puts " Type: #{context[:type]}"
|
130
|
+
end
|
131
|
+
|
132
|
+
if context[:urgency]
|
133
|
+
urgency_emojis = {
|
134
|
+
"high" => "š“",
|
135
|
+
"medium" => "š”",
|
136
|
+
"low" => "š¢"
|
137
|
+
}
|
138
|
+
urgency_emoji = urgency_emojis[context[:urgency]] || "ā¹ļø"
|
139
|
+
puts " Urgency: #{urgency_emoji} #{context[:urgency].capitalize}"
|
140
|
+
end
|
141
|
+
|
142
|
+
if context[:description]
|
143
|
+
puts " Description: #{context[:description]}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Estimate completion time for questions
|
148
|
+
def estimate_completion_time(questions)
|
149
|
+
total_time = 0
|
150
|
+
|
151
|
+
questions.each do |question|
|
152
|
+
question_type = question[:type] || "text"
|
153
|
+
|
154
|
+
total_time += case question_type
|
155
|
+
when "text"
|
156
|
+
30 # 30 seconds for text input
|
157
|
+
when "choice"
|
158
|
+
15 # 15 seconds for choice selection
|
159
|
+
when "confirmation"
|
160
|
+
10 # 10 seconds for yes/no
|
161
|
+
when "file"
|
162
|
+
45 # 45 seconds for file selection
|
163
|
+
when "number"
|
164
|
+
20 # 20 seconds for number input
|
165
|
+
when "email"
|
166
|
+
25 # 25 seconds for email input
|
167
|
+
when "url"
|
168
|
+
30 # 30 seconds for URL input
|
169
|
+
else
|
170
|
+
30 # Default 30 seconds
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
if total_time < 60
|
175
|
+
"#{total_time} seconds"
|
176
|
+
else
|
177
|
+
minutes = (total_time / 60.0).round(1)
|
178
|
+
"#{minutes} minutes"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Display numbered question with advanced formatting
|
183
|
+
def display_numbered_question(question_data, question_number, _current_index, total_questions)
|
184
|
+
question_text = question_data[:question]
|
185
|
+
question_type = question_data[:type] || "text"
|
186
|
+
expected_input = question_data[:expected_input] || "text"
|
187
|
+
options = question_data[:options]
|
188
|
+
default_value = question_data[:default]
|
189
|
+
required = question_data[:required] != false
|
190
|
+
|
191
|
+
# Display question header
|
192
|
+
puts "\n" + "=" * 60
|
193
|
+
puts "š Question #{question_number} of #{total_questions}"
|
194
|
+
puts "=" * 60
|
195
|
+
|
196
|
+
# Display question text with formatting
|
197
|
+
display_question_text(question_text, question_type)
|
198
|
+
|
199
|
+
# Display question metadata
|
200
|
+
display_question_metadata(question_type, expected_input, options, default_value, required)
|
201
|
+
|
202
|
+
# Display question instructions
|
203
|
+
display_question_instructions(question_type, options, default_value, required)
|
204
|
+
|
205
|
+
puts "\n" + "-" * 60
|
206
|
+
end
|
207
|
+
|
208
|
+
# Display question text with formatting
|
209
|
+
def display_question_text(question_text, question_type)
|
210
|
+
# Get question type emoji
|
211
|
+
type_emojis = {
|
212
|
+
"text" => "š",
|
213
|
+
"choice" => "š",
|
214
|
+
"confirmation" => "ā
",
|
215
|
+
"file" => "š",
|
216
|
+
"number" => "š¢",
|
217
|
+
"email" => "š§",
|
218
|
+
"url" => "š"
|
219
|
+
}
|
220
|
+
type_emoji = type_emojis[question_type] || "ā"
|
221
|
+
|
222
|
+
puts "#{type_emoji} #{question_text}"
|
223
|
+
end
|
224
|
+
|
225
|
+
# Display question metadata
|
226
|
+
def display_question_metadata(question_type, expected_input, options, default_value, required)
|
227
|
+
puts "\nš Question Details:"
|
228
|
+
|
229
|
+
# Question type
|
230
|
+
puts " Type: #{question_type.capitalize}"
|
231
|
+
|
232
|
+
# Expected input
|
233
|
+
if expected_input != "text"
|
234
|
+
puts " Expected input: #{expected_input}"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Options
|
238
|
+
if options && !options.empty?
|
239
|
+
puts " Options: #{options.length} available"
|
240
|
+
end
|
241
|
+
|
242
|
+
# Default value
|
243
|
+
if default_value
|
244
|
+
puts " Default: #{default_value}"
|
245
|
+
end
|
246
|
+
|
247
|
+
# Required status
|
248
|
+
status = required ? "Required" : "Optional"
|
249
|
+
status_emoji = required ? "š“" : "š¢"
|
250
|
+
puts " Status: #{status_emoji} #{status}"
|
251
|
+
end
|
252
|
+
|
253
|
+
# Display question instructions
|
254
|
+
def display_question_instructions(question_type, options, default_value, required)
|
255
|
+
puts "\nš” Instructions:"
|
256
|
+
|
257
|
+
case question_type
|
258
|
+
when "text"
|
259
|
+
puts " ⢠Enter your text response"
|
260
|
+
puts " ⢠Use @ for file selection if needed"
|
261
|
+
puts " ⢠Press Enter when done"
|
262
|
+
when "choice"
|
263
|
+
puts " ⢠Select from the numbered options below"
|
264
|
+
puts " ⢠Enter the number of your choice"
|
265
|
+
puts " ⢠Press Enter to confirm"
|
266
|
+
when "confirmation"
|
267
|
+
puts " ⢠Enter 'y' or 'yes' for Yes"
|
268
|
+
puts " ⢠Enter 'n' or 'no' for No"
|
269
|
+
puts " ⢠Press Enter for default"
|
270
|
+
when "file"
|
271
|
+
puts " ⢠Enter file path directly"
|
272
|
+
puts " ⢠Use @ to browse and select files"
|
273
|
+
puts " ⢠File must exist and be readable"
|
274
|
+
when "number"
|
275
|
+
puts " ⢠Enter a valid number"
|
276
|
+
puts " ⢠Use decimal point for decimals"
|
277
|
+
puts " ⢠Press Enter when done"
|
278
|
+
when "email"
|
279
|
+
puts " ⢠Enter a valid email address"
|
280
|
+
puts " ⢠Format: user@domain.com"
|
281
|
+
puts " ⢠Press Enter when done"
|
282
|
+
when "url"
|
283
|
+
puts " ⢠Enter a valid URL"
|
284
|
+
puts " ⢠Format: https://example.com"
|
285
|
+
puts " ⢠Press Enter when done"
|
286
|
+
end
|
287
|
+
|
288
|
+
# Additional instructions based on options
|
289
|
+
if options && !options.empty?
|
290
|
+
puts "\nš Available Options:"
|
291
|
+
options.each_with_index do |option, index|
|
292
|
+
marker = (default_value && option == default_value) ? " (default)" : ""
|
293
|
+
puts " #{index + 1}. #{option}#{marker}"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Default value instructions
|
298
|
+
if default_value
|
299
|
+
puts "\nā” Quick Answer:"
|
300
|
+
puts " ⢠Press Enter to use default: #{default_value}"
|
301
|
+
end
|
302
|
+
|
303
|
+
# Required field instructions
|
304
|
+
if required
|
305
|
+
puts "\nā ļø Required Field:"
|
306
|
+
puts " ⢠This question must be answered"
|
307
|
+
puts " ⢠Cannot be left blank"
|
308
|
+
else
|
309
|
+
puts "\nā
Optional Field:"
|
310
|
+
puts " ⢠This question can be skipped"
|
311
|
+
puts " ⢠Press Enter to leave blank"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Display question progress
|
316
|
+
def display_question_progress(current_index, total_questions)
|
317
|
+
progress_percentage = (current_index.to_f / total_questions * 100).round(1)
|
318
|
+
progress_bar = generate_progress_bar(progress_percentage)
|
319
|
+
|
320
|
+
puts "\nš Progress: #{progress_bar} #{progress_percentage}% (#{current_index}/#{total_questions})"
|
321
|
+
|
322
|
+
# Show estimated time remaining
|
323
|
+
if current_index < total_questions
|
324
|
+
remaining_questions = total_questions - current_index
|
325
|
+
estimated_remaining = estimate_remaining_time(remaining_questions)
|
326
|
+
puts "ā±ļø Estimated time remaining: #{estimated_remaining}"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Generate progress bar
|
331
|
+
def generate_progress_bar(percentage, width = 20)
|
332
|
+
filled = (percentage / 100.0 * width).round
|
333
|
+
empty = width - filled
|
334
|
+
|
335
|
+
"[" + "ā" * filled + "ā" * empty + "]"
|
336
|
+
end
|
337
|
+
|
338
|
+
# Estimate remaining time
|
339
|
+
def estimate_remaining_time(remaining_questions)
|
340
|
+
# Assume average 25 seconds per question
|
341
|
+
total_seconds = remaining_questions * 25
|
342
|
+
|
343
|
+
if total_seconds < 60
|
344
|
+
"#{total_seconds} seconds"
|
345
|
+
else
|
346
|
+
minutes = (total_seconds / 60.0).round(1)
|
347
|
+
"#{minutes} minutes"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Display question completion summary
|
352
|
+
def display_question_completion_summary(responses, questions)
|
353
|
+
puts "\n" + "=" * 60
|
354
|
+
puts "ā
Question Completion Summary"
|
355
|
+
puts "=" * 60
|
356
|
+
|
357
|
+
# Show completion statistics
|
358
|
+
total_questions = questions.length
|
359
|
+
answered_questions = responses.values.count { |v| !v.nil? && !v.to_s.strip.empty? }
|
360
|
+
skipped_questions = total_questions - answered_questions
|
361
|
+
|
362
|
+
puts "š Statistics:"
|
363
|
+
puts " Total questions: #{total_questions}"
|
364
|
+
puts " Answered: #{answered_questions}"
|
365
|
+
puts " Skipped: #{skipped_questions}"
|
366
|
+
puts " Completion rate: #{(answered_questions.to_f / total_questions * 100).round(1)}%"
|
367
|
+
|
368
|
+
# Show response summary
|
369
|
+
puts "\nš Response Summary:"
|
370
|
+
responses.each do |key, value|
|
371
|
+
question_number = key.gsub("question_", "")
|
372
|
+
if value.nil? || value.to_s.strip.empty?
|
373
|
+
puts " #{question_number}. [Skipped]"
|
374
|
+
else
|
375
|
+
display_value = (value.to_s.length > 50) ? "#{value.to_s[0..47]}..." : value.to_s
|
376
|
+
puts " #{question_number}. #{display_value}"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
puts "\nš Continuing execution..."
|
381
|
+
end
|
382
|
+
|
383
|
+
# Display question information (legacy method for compatibility)
|
384
|
+
def display_question_info(question_type, expected_input, options, default_value, required)
|
385
|
+
info_parts = []
|
386
|
+
|
387
|
+
# Question type
|
388
|
+
type_emojis = {
|
389
|
+
"text" => "š",
|
390
|
+
"choice" => "š",
|
391
|
+
"confirmation" => "ā
",
|
392
|
+
"file" => "š",
|
393
|
+
"number" => "š¢",
|
394
|
+
"email" => "š§",
|
395
|
+
"url" => "š"
|
396
|
+
}
|
397
|
+
type_emoji = type_emojis[question_type] || "ā"
|
398
|
+
info_parts << "#{type_emoji} #{question_type.capitalize}"
|
399
|
+
|
400
|
+
# Expected input type
|
401
|
+
if expected_input != "text"
|
402
|
+
info_parts << "Expected: #{expected_input}"
|
403
|
+
end
|
404
|
+
|
405
|
+
# Options
|
406
|
+
if options && !options.empty?
|
407
|
+
info_parts << "Options: #{options.join(", ")}"
|
408
|
+
end
|
409
|
+
|
410
|
+
# Default value
|
411
|
+
if default_value
|
412
|
+
info_parts << "Default: #{default_value}"
|
413
|
+
end
|
414
|
+
|
415
|
+
# Required status
|
416
|
+
info_parts << if required
|
417
|
+
"Required: Yes"
|
418
|
+
else
|
419
|
+
"Required: No"
|
420
|
+
end
|
421
|
+
|
422
|
+
puts " #{info_parts.join(" | ")}"
|
423
|
+
end
|
424
|
+
|
425
|
+
# Get response for a specific question with enhanced validation
|
426
|
+
def get_question_response(question_data, _question_number)
|
427
|
+
question_type = question_data[:type] || "text"
|
428
|
+
expected_input = question_data[:expected_input] || "text"
|
429
|
+
options = question_data[:options]
|
430
|
+
default_value = question_data[:default]
|
431
|
+
required = question_data[:required] != false
|
432
|
+
validation_options = question_data[:validation_options] || {}
|
433
|
+
|
434
|
+
case question_type
|
435
|
+
when "text"
|
436
|
+
get_text_response(expected_input, default_value, required, validation_options)
|
437
|
+
when "choice"
|
438
|
+
get_choice_response(options, default_value, required)
|
439
|
+
when "confirmation"
|
440
|
+
get_confirmation_response(default_value, required)
|
441
|
+
when "file"
|
442
|
+
get_file_response(expected_input, default_value, required, validation_options)
|
443
|
+
when "number"
|
444
|
+
get_number_response(expected_input, default_value, required, validation_options)
|
445
|
+
when "email"
|
446
|
+
get_email_response(default_value, required, validation_options)
|
447
|
+
when "url"
|
448
|
+
get_url_response(default_value, required, validation_options)
|
449
|
+
else
|
450
|
+
get_text_response(expected_input, default_value, required, validation_options)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Comprehensive error recovery system
|
455
|
+
def handle_input_error(error, question_data, retry_count = 0)
|
456
|
+
max_retries = 3
|
457
|
+
|
458
|
+
puts "\nšØ Input Error:"
|
459
|
+
puts " #{error.message}"
|
460
|
+
|
461
|
+
if retry_count < max_retries
|
462
|
+
puts "\nš Retry Options:"
|
463
|
+
puts " 1. Try again"
|
464
|
+
puts " 2. Skip this question"
|
465
|
+
puts " 3. Get help"
|
466
|
+
puts " 4. Cancel all questions"
|
467
|
+
|
468
|
+
choice = Readline.readline("Your choice (1-4): ", true)
|
469
|
+
|
470
|
+
case choice&.strip
|
471
|
+
when "1"
|
472
|
+
puts "š Retrying..."
|
473
|
+
:retry
|
474
|
+
when "2"
|
475
|
+
puts "āļø Skipping question..."
|
476
|
+
:skip
|
477
|
+
when "3"
|
478
|
+
show_question_help(question_data)
|
479
|
+
:retry
|
480
|
+
when "4"
|
481
|
+
puts "ā Cancelling all questions..."
|
482
|
+
:cancel
|
483
|
+
else
|
484
|
+
puts "ā Invalid choice. Retrying..."
|
485
|
+
:retry
|
486
|
+
end
|
487
|
+
else
|
488
|
+
puts "\nā Maximum retries exceeded. Skipping question..."
|
489
|
+
:skip
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# Show help for specific question
|
494
|
+
def show_question_help(question_data)
|
495
|
+
question_type = question_data[:type] || "text"
|
496
|
+
|
497
|
+
puts "\nš Help for #{question_type.capitalize} Question:"
|
498
|
+
puts "=" * 50
|
499
|
+
|
500
|
+
case question_type
|
501
|
+
when "text"
|
502
|
+
puts "⢠Enter any text response"
|
503
|
+
puts "⢠Use @ for file selection if needed"
|
504
|
+
puts "⢠Press Enter when done"
|
505
|
+
when "choice"
|
506
|
+
puts "⢠Select from the numbered options"
|
507
|
+
puts "⢠Enter the number of your choice"
|
508
|
+
puts "⢠Or type the option text directly"
|
509
|
+
when "confirmation"
|
510
|
+
puts "⢠Enter 'y' or 'yes' for Yes"
|
511
|
+
puts "⢠Enter 'n' or 'no' for No"
|
512
|
+
puts "⢠Press Enter for default"
|
513
|
+
when "file"
|
514
|
+
puts "⢠Enter file path directly"
|
515
|
+
puts "⢠Use @ to browse and select files"
|
516
|
+
puts "⢠File must exist and be readable"
|
517
|
+
when "number"
|
518
|
+
puts "⢠Enter a valid number"
|
519
|
+
puts "⢠Use decimal point for decimals"
|
520
|
+
puts "⢠Check range requirements"
|
521
|
+
when "email"
|
522
|
+
puts "⢠Enter a valid email address"
|
523
|
+
puts "⢠Format: user@domain.com"
|
524
|
+
puts "⢠Check for typos"
|
525
|
+
when "url"
|
526
|
+
puts "⢠Enter a valid URL"
|
527
|
+
puts "⢠Format: https://example.com"
|
528
|
+
puts "⢠Include protocol (http:// or https://)"
|
529
|
+
end
|
530
|
+
|
531
|
+
puts "\nPress Enter to continue..."
|
532
|
+
Readline.readline
|
533
|
+
end
|
534
|
+
|
535
|
+
# Enhanced error handling and validation display
|
536
|
+
def display_validation_error(validation_result, _input_type)
|
537
|
+
puts "\nā Validation Error:"
|
538
|
+
puts " #{validation_result[:error_message]}"
|
539
|
+
|
540
|
+
if validation_result[:suggestions].any?
|
541
|
+
puts "\nš” Suggestions:"
|
542
|
+
validation_result[:suggestions].each do |suggestion|
|
543
|
+
puts " ⢠#{suggestion}"
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
if validation_result[:warnings].any?
|
548
|
+
puts "\nā ļø Warnings:"
|
549
|
+
validation_result[:warnings].each do |warning|
|
550
|
+
puts " ⢠#{warning}"
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
puts "\nš Please try again..."
|
555
|
+
end
|
556
|
+
|
557
|
+
# Display validation warnings
|
558
|
+
def display_validation_warnings(validation_result)
|
559
|
+
if validation_result[:warnings].any?
|
560
|
+
puts "\nā ļø Warnings:"
|
561
|
+
validation_result[:warnings].each do |warning|
|
562
|
+
puts " ⢠#{warning}"
|
563
|
+
end
|
564
|
+
puts "\nPress Enter to continue or type 'fix' to correct..."
|
565
|
+
|
566
|
+
input = Readline.readline("", true)
|
567
|
+
return input&.strip&.downcase == "fix"
|
568
|
+
end
|
569
|
+
false
|
570
|
+
end
|
571
|
+
|
572
|
+
# Get text response with enhanced validation
|
573
|
+
def get_text_response(expected_input, default_value, required, options = {})
|
574
|
+
prompt = "Your response"
|
575
|
+
prompt += " (default: #{default_value})" if default_value
|
576
|
+
prompt += required ? ": " : " (optional): "
|
577
|
+
|
578
|
+
loop do
|
579
|
+
input = Readline.readline(prompt, true)
|
580
|
+
|
581
|
+
# Handle empty input
|
582
|
+
if input.nil? || input.strip.empty?
|
583
|
+
if default_value
|
584
|
+
return default_value
|
585
|
+
elsif required
|
586
|
+
puts "ā This field is required. Please provide a response."
|
587
|
+
next
|
588
|
+
else
|
589
|
+
return nil
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
# Enhanced validation
|
594
|
+
validation_result = validate_input_type(input.strip, expected_input, options)
|
595
|
+
|
596
|
+
unless validation_result[:valid]
|
597
|
+
display_validation_error(validation_result, expected_input)
|
598
|
+
next
|
599
|
+
end
|
600
|
+
|
601
|
+
# Check for warnings
|
602
|
+
if display_validation_warnings(validation_result)
|
603
|
+
next
|
604
|
+
end
|
605
|
+
|
606
|
+
return input.strip
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
# Get choice response with enhanced validation
|
611
|
+
def get_choice_response(options, default_value, required)
|
612
|
+
return nil if options.nil? || options.empty?
|
613
|
+
|
614
|
+
puts "\n Available options:"
|
615
|
+
options.each_with_index do |option, index|
|
616
|
+
marker = (default_value && option == default_value) ? " (default)" : ""
|
617
|
+
puts " #{index + 1}. #{option}#{marker}"
|
618
|
+
end
|
619
|
+
|
620
|
+
loop do
|
621
|
+
prompt = "Your choice (1-#{options.size})"
|
622
|
+
prompt += " (default: #{default_value})" if default_value
|
623
|
+
prompt += required ? ": " : " (optional): "
|
624
|
+
|
625
|
+
input = Readline.readline(prompt, true)
|
626
|
+
|
627
|
+
if input.nil? || input.strip.empty?
|
628
|
+
if default_value
|
629
|
+
return default_value
|
630
|
+
elsif required
|
631
|
+
puts "ā Please make a selection."
|
632
|
+
next
|
633
|
+
else
|
634
|
+
return nil
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
# Enhanced validation for choice
|
639
|
+
validation_result = validate_input_type(input.strip, "choice", {choices: options})
|
640
|
+
|
641
|
+
unless validation_result[:valid]
|
642
|
+
display_validation_error(validation_result, "choice")
|
643
|
+
next
|
644
|
+
end
|
645
|
+
|
646
|
+
# Check for warnings
|
647
|
+
if display_validation_warnings(validation_result)
|
648
|
+
next
|
649
|
+
end
|
650
|
+
|
651
|
+
# Parse the choice
|
652
|
+
choice = input.strip
|
653
|
+
if choice.match?(/\A\d+\z/)
|
654
|
+
return options[choice.to_i - 1]
|
655
|
+
else
|
656
|
+
return choice
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
# Get confirmation response with enhanced validation
|
662
|
+
def get_confirmation_response(default_value, required)
|
663
|
+
default = default_value.nil? || default_value
|
664
|
+
default_text = default ? "Y/n" : "y/N"
|
665
|
+
prompt = "Your response [#{default_text}]"
|
666
|
+
prompt += required ? ": " : " (optional): "
|
667
|
+
|
668
|
+
loop do
|
669
|
+
input = Readline.readline(prompt, true)
|
670
|
+
|
671
|
+
if input.nil? || input.strip.empty?
|
672
|
+
return default
|
673
|
+
end
|
674
|
+
|
675
|
+
# Enhanced validation for boolean
|
676
|
+
validation_result = validate_input_type(input.strip, "boolean")
|
677
|
+
|
678
|
+
unless validation_result[:valid]
|
679
|
+
display_validation_error(validation_result, "boolean")
|
680
|
+
next
|
681
|
+
end
|
682
|
+
|
683
|
+
# Check for warnings
|
684
|
+
if display_validation_warnings(validation_result)
|
685
|
+
next
|
686
|
+
end
|
687
|
+
|
688
|
+
response = input.strip.downcase
|
689
|
+
case response
|
690
|
+
when "y", "yes", "true", "1"
|
691
|
+
return true
|
692
|
+
when "n", "no", "false", "0"
|
693
|
+
return false
|
694
|
+
end
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
# Get file response with enhanced validation
|
699
|
+
def get_file_response(_expected_input, default_value, required, options = {})
|
700
|
+
prompt = "File path"
|
701
|
+
prompt += " (default: #{default_value})" if default_value
|
702
|
+
prompt += required ? ": " : " (optional): "
|
703
|
+
|
704
|
+
loop do
|
705
|
+
input = Readline.readline(prompt, true)
|
706
|
+
|
707
|
+
if input.nil? || input.strip.empty?
|
708
|
+
if default_value
|
709
|
+
return default_value
|
710
|
+
elsif required
|
711
|
+
puts "ā Please provide a file path."
|
712
|
+
next
|
713
|
+
else
|
714
|
+
return nil
|
715
|
+
end
|
716
|
+
end
|
717
|
+
|
718
|
+
# Handle file selection with @ character
|
719
|
+
if input.strip.start_with?("@")
|
720
|
+
file_path = handle_file_selection(input.strip)
|
721
|
+
return file_path if file_path
|
722
|
+
else
|
723
|
+
# Enhanced validation for file path
|
724
|
+
validation_result = validate_input_type(input.strip, "file", options)
|
725
|
+
|
726
|
+
unless validation_result[:valid]
|
727
|
+
display_validation_error(validation_result, "file")
|
728
|
+
next
|
729
|
+
end
|
730
|
+
|
731
|
+
# Check for warnings
|
732
|
+
if display_validation_warnings(validation_result)
|
733
|
+
next
|
734
|
+
end
|
735
|
+
|
736
|
+
return input.strip
|
737
|
+
end
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
# Get number response with enhanced validation
|
742
|
+
def get_number_response(expected_input, default_value, required, options = {})
|
743
|
+
prompt = "Number"
|
744
|
+
prompt += " (default: #{default_value})" if default_value
|
745
|
+
prompt += required ? ": " : " (optional): "
|
746
|
+
|
747
|
+
loop do
|
748
|
+
input = Readline.readline(prompt, true)
|
749
|
+
|
750
|
+
if input.nil? || input.strip.empty?
|
751
|
+
if default_value
|
752
|
+
return default_value
|
753
|
+
elsif required
|
754
|
+
puts "ā Please provide a number."
|
755
|
+
next
|
756
|
+
else
|
757
|
+
return nil
|
758
|
+
end
|
759
|
+
end
|
760
|
+
|
761
|
+
# Enhanced validation for numbers
|
762
|
+
validation_result = validate_input_type(input.strip, expected_input, options)
|
763
|
+
|
764
|
+
unless validation_result[:valid]
|
765
|
+
display_validation_error(validation_result, expected_input)
|
766
|
+
next
|
767
|
+
end
|
768
|
+
|
769
|
+
# Check for warnings
|
770
|
+
if display_validation_warnings(validation_result)
|
771
|
+
next
|
772
|
+
end
|
773
|
+
|
774
|
+
# Parse the number
|
775
|
+
begin
|
776
|
+
if expected_input == "integer"
|
777
|
+
return Integer(input.strip)
|
778
|
+
else
|
779
|
+
return Float(input.strip)
|
780
|
+
end
|
781
|
+
rescue ArgumentError
|
782
|
+
puts "ā Please enter a valid #{expected_input}."
|
783
|
+
next
|
784
|
+
end
|
785
|
+
end
|
786
|
+
end
|
787
|
+
|
788
|
+
# Get email response with enhanced validation
|
789
|
+
def get_email_response(default_value, required, options = {})
|
790
|
+
prompt = "Email address"
|
791
|
+
prompt += " (default: #{default_value})" if default_value
|
792
|
+
prompt += required ? ": " : " (optional): "
|
793
|
+
|
794
|
+
loop do
|
795
|
+
input = Readline.readline(prompt, true)
|
796
|
+
|
797
|
+
if input.nil? || input.strip.empty?
|
798
|
+
if default_value
|
799
|
+
return default_value
|
800
|
+
elsif required
|
801
|
+
puts "ā Please provide an email address."
|
802
|
+
next
|
803
|
+
else
|
804
|
+
return nil
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
# Enhanced validation for email
|
809
|
+
validation_result = validate_input_type(input.strip, "email", options)
|
810
|
+
|
811
|
+
unless validation_result[:valid]
|
812
|
+
display_validation_error(validation_result, "email")
|
813
|
+
next
|
814
|
+
end
|
815
|
+
|
816
|
+
# Check for warnings
|
817
|
+
if display_validation_warnings(validation_result)
|
818
|
+
next
|
819
|
+
end
|
820
|
+
|
821
|
+
return input.strip
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
# Get URL response with enhanced validation
|
826
|
+
def get_url_response(default_value, required, options = {})
|
827
|
+
prompt = "URL"
|
828
|
+
prompt += " (default: #{default_value})" if default_value
|
829
|
+
prompt += required ? ": " : " (optional): "
|
830
|
+
|
831
|
+
loop do
|
832
|
+
input = Readline.readline(prompt, true)
|
833
|
+
|
834
|
+
if input.nil? || input.strip.empty?
|
835
|
+
if default_value
|
836
|
+
return default_value
|
837
|
+
elsif required
|
838
|
+
puts "ā Please provide a URL."
|
839
|
+
next
|
840
|
+
else
|
841
|
+
return nil
|
842
|
+
end
|
843
|
+
end
|
844
|
+
|
845
|
+
# Enhanced validation for URL
|
846
|
+
validation_result = validate_input_type(input.strip, "url", options)
|
847
|
+
|
848
|
+
unless validation_result[:valid]
|
849
|
+
display_validation_error(validation_result, "url")
|
850
|
+
next
|
851
|
+
end
|
852
|
+
|
853
|
+
# Check for warnings
|
854
|
+
if display_validation_warnings(validation_result)
|
855
|
+
next
|
856
|
+
end
|
857
|
+
|
858
|
+
return input.strip
|
859
|
+
end
|
860
|
+
end
|
861
|
+
|
862
|
+
# Comprehensive input validation system
|
863
|
+
def validate_input_type(input, expected_type, options = {})
|
864
|
+
case expected_type
|
865
|
+
when "email"
|
866
|
+
validate_email(input, options)
|
867
|
+
when "url"
|
868
|
+
validate_url(input, options)
|
869
|
+
when "number", "integer"
|
870
|
+
validate_number(input, options)
|
871
|
+
when "float", "decimal"
|
872
|
+
validate_float(input, options)
|
873
|
+
when "boolean"
|
874
|
+
validate_boolean(input, options)
|
875
|
+
when "file", "path"
|
876
|
+
validate_file_path(input, options)
|
877
|
+
when "text"
|
878
|
+
validate_text(input, options)
|
879
|
+
when "choice"
|
880
|
+
validate_choice(input, options)
|
881
|
+
else
|
882
|
+
validate_generic(input, options)
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
# Validate email input
|
887
|
+
def validate_email(input, options = {})
|
888
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
889
|
+
|
890
|
+
# Basic email regex
|
891
|
+
email_regex = /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i
|
892
|
+
|
893
|
+
if input.nil? || input.strip.empty?
|
894
|
+
if options[:required]
|
895
|
+
result[:error_message] = "Email address cannot be empty"
|
896
|
+
else
|
897
|
+
result[:valid] = true
|
898
|
+
end
|
899
|
+
return result
|
900
|
+
end
|
901
|
+
|
902
|
+
if !email_regex.match?(input.strip)
|
903
|
+
result[:error_message] = "Invalid email format"
|
904
|
+
result[:suggestions] = [
|
905
|
+
"Use format: user@domain.com",
|
906
|
+
"Check for typos in domain name",
|
907
|
+
"Ensure @ symbol is present"
|
908
|
+
]
|
909
|
+
return result
|
910
|
+
end
|
911
|
+
|
912
|
+
# Additional validations
|
913
|
+
email = input.strip.downcase
|
914
|
+
local_part, domain = email.split("@")
|
915
|
+
|
916
|
+
# Check local part length
|
917
|
+
if local_part.length > 64
|
918
|
+
result[:warnings] << "Local part is very long (#{local_part.length} characters)"
|
919
|
+
end
|
920
|
+
|
921
|
+
# Check domain length
|
922
|
+
if domain.length > 253
|
923
|
+
result[:warnings] << "Domain is very long (#{domain.length} characters)"
|
924
|
+
end
|
925
|
+
|
926
|
+
# Check for common typos
|
927
|
+
common_domains = %w[gmail.com yahoo.com hotmail.com outlook.com]
|
928
|
+
if common_domains.any? { |d| domain.include?(d) && domain != d }
|
929
|
+
result[:suggestions] << "Did you mean #{domain.gsub(/[^a-z.]/, "")}?"
|
930
|
+
end
|
931
|
+
|
932
|
+
result[:valid] = true
|
933
|
+
result
|
934
|
+
end
|
935
|
+
|
936
|
+
# Validate URL input
|
937
|
+
def validate_url(input, options = {})
|
938
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
939
|
+
|
940
|
+
if input.nil? || input.strip.empty?
|
941
|
+
if options[:required]
|
942
|
+
result[:error_message] = "URL cannot be empty"
|
943
|
+
else
|
944
|
+
result[:valid] = true
|
945
|
+
end
|
946
|
+
return result
|
947
|
+
end
|
948
|
+
|
949
|
+
url = input.strip
|
950
|
+
|
951
|
+
# Basic URL regex
|
952
|
+
url_regex = /\Ahttps?:\/\/.+/i
|
953
|
+
|
954
|
+
if !url_regex.match?(url)
|
955
|
+
result[:error_message] = "Invalid URL format"
|
956
|
+
result[:suggestions] = [
|
957
|
+
"Use format: https://example.com",
|
958
|
+
"Include http:// or https:// protocol",
|
959
|
+
"Check for typos in domain name"
|
960
|
+
]
|
961
|
+
return result
|
962
|
+
end
|
963
|
+
|
964
|
+
# Additional validations
|
965
|
+
begin
|
966
|
+
uri = URI.parse(url)
|
967
|
+
|
968
|
+
# Check for valid hostname
|
969
|
+
if uri.host.nil? || uri.host.empty?
|
970
|
+
result[:error_message] = "Invalid hostname in URL"
|
971
|
+
return result
|
972
|
+
end
|
973
|
+
|
974
|
+
# Check for common typos
|
975
|
+
if uri.host.include?("www.") && !uri.host.start_with?("www.")
|
976
|
+
result[:suggestions] << "Consider using www.#{uri.host}"
|
977
|
+
end
|
978
|
+
|
979
|
+
# Check for HTTP vs HTTPS
|
980
|
+
if uri.scheme == "http" && !uri.host.include?("localhost")
|
981
|
+
result[:warnings] << "Consider using HTTPS for security"
|
982
|
+
end
|
983
|
+
rescue URI::InvalidURIError
|
984
|
+
result[:error_message] = "Invalid URL format"
|
985
|
+
result[:suggestions] = [
|
986
|
+
"Check for special characters",
|
987
|
+
"Ensure proper URL encoding",
|
988
|
+
"Verify domain name spelling"
|
989
|
+
]
|
990
|
+
return result
|
991
|
+
end
|
992
|
+
|
993
|
+
result[:valid] = true
|
994
|
+
result
|
995
|
+
end
|
996
|
+
|
997
|
+
# Validate number input
|
998
|
+
def validate_number(input, options = {})
|
999
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
1000
|
+
|
1001
|
+
if input.nil? || input.strip.empty?
|
1002
|
+
result[:error_message] = "Number cannot be empty"
|
1003
|
+
return result
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
number_str = input.strip
|
1007
|
+
|
1008
|
+
# Check for valid integer format
|
1009
|
+
if !number_str.match?(/\A-?\d+\z/)
|
1010
|
+
result[:error_message] = "Invalid number format"
|
1011
|
+
result[:suggestions] = [
|
1012
|
+
"Enter a whole number (e.g., 25, -10, 0)",
|
1013
|
+
"Remove any decimal points or letters",
|
1014
|
+
"Check for typos"
|
1015
|
+
]
|
1016
|
+
return result
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
number = number_str.to_i
|
1020
|
+
|
1021
|
+
# Range validation
|
1022
|
+
if options[:min] && number < options[:min]
|
1023
|
+
result[:error_message] = "Number must be at least #{options[:min]}"
|
1024
|
+
return result
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
if options[:max] && number > options[:max]
|
1028
|
+
result[:error_message] = "Number must be at most #{options[:max]}"
|
1029
|
+
return result
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
# Warning for very large numbers
|
1033
|
+
if number.abs > 1_000_000
|
1034
|
+
result[:warnings] << "Very large number (#{number})"
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
result[:valid] = true
|
1038
|
+
result
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
# Validate float input
|
1042
|
+
def validate_float(input, options = {})
|
1043
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
1044
|
+
|
1045
|
+
if input.nil? || input.strip.empty?
|
1046
|
+
result[:error_message] = "Number cannot be empty"
|
1047
|
+
return result
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
number_str = input.strip
|
1051
|
+
|
1052
|
+
# Check for valid float format
|
1053
|
+
if !number_str.match?(/\A-?\d+\.?\d*\z/)
|
1054
|
+
result[:error_message] = "Invalid number format"
|
1055
|
+
result[:suggestions] = [
|
1056
|
+
"Enter a number (e.g., 25, 3.14, -10.5)",
|
1057
|
+
"Use decimal point for decimals",
|
1058
|
+
"Remove any letters or special characters"
|
1059
|
+
]
|
1060
|
+
return result
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
number = number_str.to_f
|
1064
|
+
|
1065
|
+
# Range validation
|
1066
|
+
if options[:min] && number < options[:min]
|
1067
|
+
result[:error_message] = "Number must be at least #{options[:min]}"
|
1068
|
+
return result
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
if options[:max] && number > options[:max]
|
1072
|
+
result[:error_message] = "Number must be at most #{options[:max]}"
|
1073
|
+
return result
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
# Precision validation
|
1077
|
+
if options[:precision] && number_str.include?(".")
|
1078
|
+
decimal_places = number_str.split(".")[1]&.length || 0
|
1079
|
+
if decimal_places > options[:precision]
|
1080
|
+
result[:warnings] << "Number has more decimal places than expected (#{decimal_places} > #{options[:precision]})"
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
result[:valid] = true
|
1085
|
+
result
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
# Validate boolean input
|
1089
|
+
def validate_boolean(input, options = {})
|
1090
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
1091
|
+
|
1092
|
+
if input.nil? || input.strip.empty?
|
1093
|
+
if options[:required]
|
1094
|
+
result[:error_message] = "Please enter a yes/no response"
|
1095
|
+
else
|
1096
|
+
result[:valid] = true
|
1097
|
+
end
|
1098
|
+
return result
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
response = input.strip.downcase
|
1102
|
+
valid_responses = %w[y yes n no true false 1 0]
|
1103
|
+
|
1104
|
+
if !valid_responses.include?(response)
|
1105
|
+
result[:error_message] = "Invalid response"
|
1106
|
+
result[:suggestions] = [
|
1107
|
+
"Enter 'y' or 'yes' for Yes",
|
1108
|
+
"Enter 'n' or 'no' for No",
|
1109
|
+
"Enter 'true' or 'false'",
|
1110
|
+
"Enter '1' for Yes or '0' for No"
|
1111
|
+
]
|
1112
|
+
return result
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
result[:valid] = true
|
1116
|
+
result
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
# Validate file path input
|
1120
|
+
def validate_file_path(input, options = {})
|
1121
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
1122
|
+
|
1123
|
+
if input.nil? || input.strip.empty?
|
1124
|
+
result[:error_message] = "File path cannot be empty"
|
1125
|
+
return result
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
file_path = input.strip
|
1129
|
+
|
1130
|
+
# Check if file exists
|
1131
|
+
if !File.exist?(file_path)
|
1132
|
+
result[:error_message] = "File does not exist: #{file_path}"
|
1133
|
+
result[:suggestions] = [
|
1134
|
+
"Check the file path for typos",
|
1135
|
+
"Use @ to browse and select files",
|
1136
|
+
"Ensure the file exists in the specified location"
|
1137
|
+
]
|
1138
|
+
return result
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
# Check if it's actually a file (not a directory)
|
1142
|
+
if !File.file?(file_path)
|
1143
|
+
result[:error_message] = "Path is not a file: #{file_path}"
|
1144
|
+
result[:suggestions] = [
|
1145
|
+
"Select a file, not a directory",
|
1146
|
+
"Use @ to browse files"
|
1147
|
+
]
|
1148
|
+
return result
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
# Check file permissions
|
1152
|
+
if !File.readable?(file_path)
|
1153
|
+
result[:error_message] = "File is not readable: #{file_path}"
|
1154
|
+
result[:suggestions] = [
|
1155
|
+
"Check file permissions",
|
1156
|
+
"Ensure you have read access to the file"
|
1157
|
+
]
|
1158
|
+
return result
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
# File size warning
|
1162
|
+
file_size = File.size(file_path)
|
1163
|
+
if file_size > 10 * 1024 * 1024 # 10MB
|
1164
|
+
result[:warnings] << "Large file size (#{format_file_size(file_size)})"
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
# File extension validation
|
1168
|
+
if options[:allowed_extensions]
|
1169
|
+
ext = File.extname(file_path).downcase
|
1170
|
+
if !options[:allowed_extensions].include?(ext)
|
1171
|
+
result[:warnings] << "Unexpected file extension: #{ext}"
|
1172
|
+
result[:suggestions] << "Expected extensions: #{options[:allowed_extensions].join(", ")}"
|
1173
|
+
end
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
result[:valid] = true
|
1177
|
+
result
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
# Validate text input
|
1181
|
+
def validate_text(input, options = {})
|
1182
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
1183
|
+
|
1184
|
+
if input.nil? || input.strip.empty?
|
1185
|
+
if options[:required]
|
1186
|
+
result[:error_message] = "Text input is required"
|
1187
|
+
else
|
1188
|
+
result[:valid] = true
|
1189
|
+
end
|
1190
|
+
return result
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
text = input.strip
|
1194
|
+
|
1195
|
+
# Length validation
|
1196
|
+
if options[:min_length] && text.length < options[:min_length]
|
1197
|
+
result[:error_message] = "Text must be at least #{options[:min_length]} characters"
|
1198
|
+
return result
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
if options[:max_length] && text.length > options[:max_length]
|
1202
|
+
result[:error_message] = "Text must be at most #{options[:max_length]} characters"
|
1203
|
+
return result
|
1204
|
+
end
|
1205
|
+
|
1206
|
+
# Pattern validation
|
1207
|
+
if options[:pattern] && !text.match?(options[:pattern])
|
1208
|
+
result[:error_message] = "Text does not match required pattern"
|
1209
|
+
result[:suggestions] = [
|
1210
|
+
"Check the format requirements",
|
1211
|
+
"Ensure all required characters are present"
|
1212
|
+
]
|
1213
|
+
return result
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
# Content validation
|
1217
|
+
if options[:forbidden_words]&.any? { |word| text.downcase.include?(word.downcase) }
|
1218
|
+
result[:warnings] << "Text contains potentially inappropriate content"
|
1219
|
+
end
|
1220
|
+
|
1221
|
+
result[:valid] = true
|
1222
|
+
result
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
# Validate choice input
|
1226
|
+
def validate_choice(input, options = {})
|
1227
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
1228
|
+
|
1229
|
+
if input.nil? || input.strip.empty?
|
1230
|
+
result[:error_message] = "Please make a selection"
|
1231
|
+
return result
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
choice = input.strip
|
1235
|
+
|
1236
|
+
# Check if it's a number selection
|
1237
|
+
if choice.match?(/\A\d+\z/)
|
1238
|
+
choice_num = choice.to_i
|
1239
|
+
if options[:choices] && (choice_num < 1 || choice_num > options[:choices].length)
|
1240
|
+
result[:error_message] = "Invalid selection number"
|
1241
|
+
result[:suggestions] = [
|
1242
|
+
"Enter a number between 1 and #{options[:choices].length}",
|
1243
|
+
"Available options: #{options[:choices].join(", ")}"
|
1244
|
+
]
|
1245
|
+
return result
|
1246
|
+
end
|
1247
|
+
elsif options[:choices] && !options[:choices].include?(choice)
|
1248
|
+
# Check if it's a direct choice
|
1249
|
+
result[:error_message] = "Invalid choice"
|
1250
|
+
result[:suggestions] = [
|
1251
|
+
"Available options: #{options[:choices].join(", ")}",
|
1252
|
+
"Or enter the number of your choice"
|
1253
|
+
]
|
1254
|
+
return result
|
1255
|
+
end
|
1256
|
+
|
1257
|
+
result[:valid] = true
|
1258
|
+
result
|
1259
|
+
end
|
1260
|
+
|
1261
|
+
# Validate generic input
|
1262
|
+
def validate_generic(input, options = {})
|
1263
|
+
result = {valid: false, error_message: nil, suggestions: [], warnings: []}
|
1264
|
+
|
1265
|
+
if input.nil? || input.strip.empty?
|
1266
|
+
if options[:required]
|
1267
|
+
result[:error_message] = "Input is required"
|
1268
|
+
else
|
1269
|
+
result[:valid] = true
|
1270
|
+
end
|
1271
|
+
return result
|
1272
|
+
end
|
1273
|
+
|
1274
|
+
result[:valid] = true
|
1275
|
+
result
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
# Get user input with support for file selection
|
1279
|
+
def get_user_input(prompt)
|
1280
|
+
loop do
|
1281
|
+
input = Readline.readline(prompt, true)
|
1282
|
+
|
1283
|
+
# Handle empty input
|
1284
|
+
if input.nil? || input.strip.empty?
|
1285
|
+
puts "Please provide a response."
|
1286
|
+
next
|
1287
|
+
end
|
1288
|
+
|
1289
|
+
# Handle file selection with @ character
|
1290
|
+
if input.strip.start_with?("@")
|
1291
|
+
file_path = handle_file_selection(input.strip)
|
1292
|
+
return file_path if file_path
|
1293
|
+
else
|
1294
|
+
# Add to history and return
|
1295
|
+
@input_history << input.strip
|
1296
|
+
return input.strip
|
1297
|
+
end
|
1298
|
+
end
|
1299
|
+
end
|
1300
|
+
|
1301
|
+
# Handle file selection interface
|
1302
|
+
def handle_file_selection(input)
|
1303
|
+
# Remove @ character and any following text
|
1304
|
+
search_term = input[1..].strip
|
1305
|
+
|
1306
|
+
# Parse search options
|
1307
|
+
search_options = parse_file_search_options(search_term)
|
1308
|
+
|
1309
|
+
# Get available files with advanced search
|
1310
|
+
available_files = find_files_advanced(search_options)
|
1311
|
+
|
1312
|
+
if available_files.empty?
|
1313
|
+
puts "No files found matching '#{search_options[:term]}'. Please try again."
|
1314
|
+
puts "š” Try: @ (all files), @.rb (Ruby files), @config (files with 'config'), @lib/ (files in lib directory)"
|
1315
|
+
return nil
|
1316
|
+
end
|
1317
|
+
|
1318
|
+
# Display file selection menu with advanced features
|
1319
|
+
display_advanced_file_menu(available_files, search_options)
|
1320
|
+
|
1321
|
+
# Get user selection with advanced options
|
1322
|
+
selection = get_advanced_file_selection(available_files.size, search_options)
|
1323
|
+
|
1324
|
+
if selection && selection >= 0 && selection < available_files.size
|
1325
|
+
selected_file = available_files[selection]
|
1326
|
+
puts "ā
Selected: #{selected_file}"
|
1327
|
+
|
1328
|
+
# Show file preview if requested
|
1329
|
+
if search_options[:preview]
|
1330
|
+
show_file_preview(selected_file)
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
selected_file
|
1334
|
+
elsif selection == -1
|
1335
|
+
# User wants to refine search
|
1336
|
+
handle_file_selection("@#{search_term}")
|
1337
|
+
else
|
1338
|
+
puts "ā Invalid selection. Please try again."
|
1339
|
+
nil
|
1340
|
+
end
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
# Parse file search options from search term
|
1344
|
+
def parse_file_search_options(search_term)
|
1345
|
+
options = {
|
1346
|
+
term: search_term,
|
1347
|
+
extensions: [],
|
1348
|
+
directories: [],
|
1349
|
+
patterns: [],
|
1350
|
+
preview: false,
|
1351
|
+
case_sensitive: false,
|
1352
|
+
max_results: 50
|
1353
|
+
}
|
1354
|
+
|
1355
|
+
# Parse extension filters (e.g., .rb, .js, .py)
|
1356
|
+
if search_term.match?(/\.\w+$/)
|
1357
|
+
options[:extensions] = [search_term]
|
1358
|
+
options[:term] = ""
|
1359
|
+
end
|
1360
|
+
|
1361
|
+
# Parse directory filters (e.g., lib/, spec/, app/)
|
1362
|
+
if search_term.match?(/^[^\/]+\/$/)
|
1363
|
+
options[:directories] = [search_term.chomp("/")]
|
1364
|
+
options[:term] = ""
|
1365
|
+
end
|
1366
|
+
|
1367
|
+
# Parse pattern filters (e.g., config, test, spec)
|
1368
|
+
if search_term.match?(/^[a-zA-Z_][a-zA-Z0-9_]*$/)
|
1369
|
+
options[:patterns] = [search_term]
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
# Parse special options
|
1373
|
+
if search_term.include?("preview")
|
1374
|
+
options[:preview] = true
|
1375
|
+
options[:term] = options[:term].gsub("preview", "").strip
|
1376
|
+
end
|
1377
|
+
|
1378
|
+
if search_term.include?("case")
|
1379
|
+
options[:case_sensitive] = true
|
1380
|
+
options[:term] = options[:term].gsub("case", "").strip
|
1381
|
+
end
|
1382
|
+
|
1383
|
+
# Clean up multiple spaces
|
1384
|
+
options[:term] = options[:term].gsub(/\s+/, " ").strip
|
1385
|
+
|
1386
|
+
options
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
# Find files with advanced search options
|
1390
|
+
def find_files_advanced(search_options)
|
1391
|
+
files = []
|
1392
|
+
|
1393
|
+
# Determine search paths
|
1394
|
+
search_paths = determine_search_paths(search_options)
|
1395
|
+
|
1396
|
+
search_paths.each do |path|
|
1397
|
+
next unless Dir.exist?(path)
|
1398
|
+
|
1399
|
+
# Use appropriate glob pattern
|
1400
|
+
glob_pattern = build_glob_pattern(path, search_options)
|
1401
|
+
|
1402
|
+
Dir.glob(glob_pattern).each do |file|
|
1403
|
+
next unless File.file?(file)
|
1404
|
+
|
1405
|
+
# Apply filters
|
1406
|
+
if matches_filters?(file, search_options)
|
1407
|
+
files << file
|
1408
|
+
end
|
1409
|
+
end
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
# Sort and limit results
|
1413
|
+
files = sort_files(files, search_options)
|
1414
|
+
files.first(search_options[:max_results])
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
# Determine search paths based on options
|
1418
|
+
def determine_search_paths(search_options)
|
1419
|
+
if search_options[:directories].any?
|
1420
|
+
search_options[:directories]
|
1421
|
+
else
|
1422
|
+
[
|
1423
|
+
".",
|
1424
|
+
"lib",
|
1425
|
+
"spec",
|
1426
|
+
"app",
|
1427
|
+
"src",
|
1428
|
+
"docs",
|
1429
|
+
"templates",
|
1430
|
+
"config",
|
1431
|
+
"test",
|
1432
|
+
"tests"
|
1433
|
+
]
|
1434
|
+
end
|
1435
|
+
end
|
1436
|
+
|
1437
|
+
# Build glob pattern for file search
|
1438
|
+
def build_glob_pattern(base_path, search_options)
|
1439
|
+
if search_options[:extensions].any?
|
1440
|
+
# Search for specific extensions
|
1441
|
+
extensions = search_options[:extensions].join(",")
|
1442
|
+
File.join(base_path, "**", "*{#{extensions}}")
|
1443
|
+
else
|
1444
|
+
# Search for all files
|
1445
|
+
File.join(base_path, "**", "*")
|
1446
|
+
end
|
1447
|
+
end
|
1448
|
+
|
1449
|
+
# Check if file matches search filters
|
1450
|
+
def matches_filters?(file, search_options)
|
1451
|
+
filename = File.basename(file)
|
1452
|
+
filepath = file
|
1453
|
+
|
1454
|
+
# Apply case sensitivity
|
1455
|
+
if search_options[:case_sensitive]
|
1456
|
+
filename_to_check = filename
|
1457
|
+
term_to_check = search_options[:term]
|
1458
|
+
else
|
1459
|
+
filename_to_check = filename.downcase
|
1460
|
+
term_to_check = search_options[:term]&.downcase
|
1461
|
+
end
|
1462
|
+
|
1463
|
+
# Check term match
|
1464
|
+
if search_options[:term] && search_options[:term].empty?
|
1465
|
+
true
|
1466
|
+
elsif search_options[:patterns]&.any?
|
1467
|
+
# Check if any pattern matches
|
1468
|
+
search_options[:patterns].any? do |pattern|
|
1469
|
+
pattern_to_check = search_options[:case_sensitive] ? pattern : pattern.downcase
|
1470
|
+
filename_to_check.include?(pattern_to_check) || filepath.include?(pattern_to_check)
|
1471
|
+
end
|
1472
|
+
else
|
1473
|
+
# Simple term matching
|
1474
|
+
filename_to_check.include?(term_to_check) || filepath.include?(term_to_check)
|
1475
|
+
end
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
# Sort files by relevance and type
|
1479
|
+
def sort_files(files, search_options)
|
1480
|
+
files.sort_by do |file|
|
1481
|
+
filename = File.basename(file)
|
1482
|
+
ext = File.extname(file)
|
1483
|
+
|
1484
|
+
# Priority scoring
|
1485
|
+
score = 0
|
1486
|
+
|
1487
|
+
# Exact filename match gets highest priority
|
1488
|
+
if filename.downcase == search_options[:term].downcase
|
1489
|
+
score += 1000
|
1490
|
+
end
|
1491
|
+
|
1492
|
+
# Filename starts with search term
|
1493
|
+
if filename.downcase.start_with?(search_options[:term].downcase)
|
1494
|
+
score += 500
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
# Filename contains search term
|
1498
|
+
if filename.downcase.include?(search_options[:term].downcase)
|
1499
|
+
score += 100
|
1500
|
+
end
|
1501
|
+
|
1502
|
+
# File type priority
|
1503
|
+
case ext
|
1504
|
+
when ".rb"
|
1505
|
+
score += 50
|
1506
|
+
when ".js", ".ts"
|
1507
|
+
score += 40
|
1508
|
+
when ".py"
|
1509
|
+
score += 40
|
1510
|
+
when ".md"
|
1511
|
+
score += 30
|
1512
|
+
when ".yml", ".yaml"
|
1513
|
+
score += 30
|
1514
|
+
when ".json"
|
1515
|
+
score += 20
|
1516
|
+
end
|
1517
|
+
|
1518
|
+
# Directory priority
|
1519
|
+
if file.include?("lib/")
|
1520
|
+
score += 25
|
1521
|
+
elsif file.include?("spec/") || file.include?("test/")
|
1522
|
+
score += 20
|
1523
|
+
elsif file.include?("config/")
|
1524
|
+
score += 15
|
1525
|
+
end
|
1526
|
+
|
1527
|
+
# Shorter paths get slight priority
|
1528
|
+
score += (100 - file.length)
|
1529
|
+
|
1530
|
+
[-score, file] # Negative for descending order
|
1531
|
+
end
|
1532
|
+
end
|
1533
|
+
|
1534
|
+
# Find files matching search term (legacy method for compatibility)
|
1535
|
+
def find_files(search_term)
|
1536
|
+
search_options = parse_file_search_options(search_term)
|
1537
|
+
find_files_advanced(search_options)
|
1538
|
+
end
|
1539
|
+
|
1540
|
+
# Display advanced file selection menu
|
1541
|
+
def display_advanced_file_menu(files, search_options)
|
1542
|
+
puts "\nš Available files:"
|
1543
|
+
puts "Search: #{search_options[:term]} | Extensions: #{search_options[:extensions].join(", ")} | Directories: #{search_options[:directories].join(", ")}"
|
1544
|
+
puts "-" * 80
|
1545
|
+
|
1546
|
+
files.each_with_index do |file, index|
|
1547
|
+
file_info = get_file_info(file)
|
1548
|
+
puts " #{index + 1}. #{file_info[:display_name]}"
|
1549
|
+
puts " š #{file_info[:size]} | š
#{file_info[:modified]} | š·ļø #{file_info[:type]}"
|
1550
|
+
end
|
1551
|
+
|
1552
|
+
puts "\nOptions:"
|
1553
|
+
puts " 0. Cancel"
|
1554
|
+
puts " -1. Refine search"
|
1555
|
+
puts " p. Preview selected file"
|
1556
|
+
puts " h. Show help"
|
1557
|
+
end
|
1558
|
+
|
1559
|
+
# Get file information for display
|
1560
|
+
def get_file_info(file)
|
1561
|
+
{
|
1562
|
+
display_name: file,
|
1563
|
+
size: format_file_size(File.size(file)),
|
1564
|
+
modified: File.mtime(file).strftime("%Y-%m-%d %H:%M"),
|
1565
|
+
type: get_file_type(file)
|
1566
|
+
}
|
1567
|
+
end
|
1568
|
+
|
1569
|
+
# Format file size for display
|
1570
|
+
def format_file_size(size)
|
1571
|
+
if size < 1024
|
1572
|
+
"#{size} B"
|
1573
|
+
elsif size < 1024 * 1024
|
1574
|
+
"#{(size / 1024.0).round(1)} KB"
|
1575
|
+
else
|
1576
|
+
"#{(size / (1024.0 * 1024.0)).round(1)} MB"
|
1577
|
+
end
|
1578
|
+
end
|
1579
|
+
|
1580
|
+
# Get file type for display
|
1581
|
+
def get_file_type(file)
|
1582
|
+
ext = File.extname(file)
|
1583
|
+
case ext
|
1584
|
+
when ".rb"
|
1585
|
+
"Ruby"
|
1586
|
+
when ".js"
|
1587
|
+
"JavaScript"
|
1588
|
+
when ".ts"
|
1589
|
+
"TypeScript"
|
1590
|
+
when ".py"
|
1591
|
+
"Python"
|
1592
|
+
when ".md"
|
1593
|
+
"Markdown"
|
1594
|
+
when ".yml", ".yaml"
|
1595
|
+
"YAML"
|
1596
|
+
when ".json"
|
1597
|
+
"JSON"
|
1598
|
+
when ".xml"
|
1599
|
+
"XML"
|
1600
|
+
when ".html", ".htm"
|
1601
|
+
"HTML"
|
1602
|
+
when ".css"
|
1603
|
+
"CSS"
|
1604
|
+
when ".scss", ".sass"
|
1605
|
+
"Sass"
|
1606
|
+
when ".sql"
|
1607
|
+
"SQL"
|
1608
|
+
when ".sh"
|
1609
|
+
"Shell"
|
1610
|
+
when ".txt"
|
1611
|
+
"Text"
|
1612
|
+
else
|
1613
|
+
ext.empty? ? "File" : ext[1..].upcase
|
1614
|
+
end
|
1615
|
+
end
|
1616
|
+
|
1617
|
+
# Display file selection menu (legacy method for compatibility)
|
1618
|
+
def display_file_menu(files)
|
1619
|
+
display_advanced_file_menu(files, {term: "", extensions: [], directories: []})
|
1620
|
+
end
|
1621
|
+
|
1622
|
+
# Get advanced file selection from user
|
1623
|
+
def get_advanced_file_selection(max_files, _search_options)
|
1624
|
+
loop do
|
1625
|
+
input = Readline.readline("Select file (0-#{max_files}, -1=refine, p=preview, h=help): ", true)
|
1626
|
+
|
1627
|
+
if input.nil? || input.strip.empty?
|
1628
|
+
puts "Please enter a selection."
|
1629
|
+
next
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
input = input.strip.downcase
|
1633
|
+
|
1634
|
+
# Handle special commands
|
1635
|
+
case input
|
1636
|
+
when "h", "help"
|
1637
|
+
show_file_selection_help
|
1638
|
+
next
|
1639
|
+
when "p", "preview"
|
1640
|
+
puts "š” Select a file number first, then use 'p' to preview it."
|
1641
|
+
next
|
1642
|
+
end
|
1643
|
+
|
1644
|
+
begin
|
1645
|
+
selection = input.to_i
|
1646
|
+
if selection == 0
|
1647
|
+
return nil # Cancel
|
1648
|
+
elsif selection == -1
|
1649
|
+
return -1 # Refine search
|
1650
|
+
elsif selection.between?(1, max_files)
|
1651
|
+
return selection - 1 # Convert to 0-based index
|
1652
|
+
else
|
1653
|
+
puts "Please enter a number between 0 and #{max_files}, or use -1, p, h."
|
1654
|
+
end
|
1655
|
+
rescue ArgumentError
|
1656
|
+
puts "Please enter a valid number or command (0-#{max_files}, -1, p, h)."
|
1657
|
+
end
|
1658
|
+
end
|
1659
|
+
end
|
1660
|
+
|
1661
|
+
# Show file selection help
|
1662
|
+
def show_file_selection_help
|
1663
|
+
puts "\nš File Selection Help:"
|
1664
|
+
puts "=" * 40
|
1665
|
+
|
1666
|
+
puts "\nš Search Examples:"
|
1667
|
+
puts " @ - Show all files"
|
1668
|
+
puts " @.rb - Show Ruby files only"
|
1669
|
+
puts " @config - Show files with 'config' in name"
|
1670
|
+
puts " @lib/ - Show files in lib directory"
|
1671
|
+
puts " @spec preview - Show spec files with preview option"
|
1672
|
+
puts " @.js case - Show JavaScript files (case sensitive)"
|
1673
|
+
|
1674
|
+
puts "\nāØļø Selection Commands:"
|
1675
|
+
puts " 1-50 - Select file by number"
|
1676
|
+
puts " 0 - Cancel selection"
|
1677
|
+
puts " -1 - Refine search"
|
1678
|
+
puts " p - Preview selected file"
|
1679
|
+
puts " h - Show this help"
|
1680
|
+
|
1681
|
+
puts "\nš” Tips:"
|
1682
|
+
puts " ⢠Files are sorted by relevance and type"
|
1683
|
+
puts " ⢠Use extension filters for specific file types"
|
1684
|
+
puts " ⢠Use directory filters to limit search scope"
|
1685
|
+
puts " ⢠Preview option shows file content before selection"
|
1686
|
+
end
|
1687
|
+
|
1688
|
+
# Show file preview
|
1689
|
+
def show_file_preview(file_path)
|
1690
|
+
puts "\nš File Preview: #{file_path}"
|
1691
|
+
puts "=" * 60
|
1692
|
+
|
1693
|
+
begin
|
1694
|
+
content = File.read(file_path)
|
1695
|
+
lines = content.lines
|
1696
|
+
|
1697
|
+
puts "š File Info:"
|
1698
|
+
puts " Size: #{format_file_size(File.size(file_path))}"
|
1699
|
+
puts " Lines: #{lines.count}"
|
1700
|
+
puts " Modified: #{File.mtime(file_path).strftime("%Y-%m-%d %H:%M:%S")}"
|
1701
|
+
puts " Type: #{get_file_type(file_path)}"
|
1702
|
+
|
1703
|
+
puts "\nš Content Preview (first 20 lines):"
|
1704
|
+
puts "-" * 40
|
1705
|
+
|
1706
|
+
lines.first(20).each_with_index do |line, index|
|
1707
|
+
puts "#{(index + 1).to_s.rjust(3)}: #{line.chomp}"
|
1708
|
+
end
|
1709
|
+
|
1710
|
+
if lines.count > 20
|
1711
|
+
puts "... (#{lines.count - 20} more lines)"
|
1712
|
+
end
|
1713
|
+
rescue => e
|
1714
|
+
puts "ā Error reading file: #{e.message}"
|
1715
|
+
end
|
1716
|
+
|
1717
|
+
puts "\nPress Enter to continue..."
|
1718
|
+
Readline.readline
|
1719
|
+
end
|
1720
|
+
|
1721
|
+
# Get file selection from user (legacy method for compatibility)
|
1722
|
+
def get_file_selection(max_files)
|
1723
|
+
get_advanced_file_selection(max_files, {term: "", extensions: [], directories: []})
|
1724
|
+
end
|
1725
|
+
|
1726
|
+
# Get confirmation from user
|
1727
|
+
def get_confirmation(message, default: true)
|
1728
|
+
default_text = default ? "Y/n" : "y/N"
|
1729
|
+
prompt = "#{message} [#{default_text}]: "
|
1730
|
+
|
1731
|
+
loop do
|
1732
|
+
input = Readline.readline(prompt, true)
|
1733
|
+
|
1734
|
+
if input.nil? || input.strip.empty?
|
1735
|
+
return default
|
1736
|
+
end
|
1737
|
+
|
1738
|
+
response = input.strip.downcase
|
1739
|
+
case response
|
1740
|
+
when "y", "yes"
|
1741
|
+
return true
|
1742
|
+
when "n", "no"
|
1743
|
+
return false
|
1744
|
+
else
|
1745
|
+
puts "Please enter 'y' or 'n'."
|
1746
|
+
end
|
1747
|
+
end
|
1748
|
+
end
|
1749
|
+
|
1750
|
+
# Get choice from multiple options
|
1751
|
+
def get_choice(message, options, default: nil)
|
1752
|
+
puts "\n#{message}"
|
1753
|
+
options.each_with_index do |option, index|
|
1754
|
+
marker = (default && index == default) ? " (default)" : ""
|
1755
|
+
puts " #{index + 1}. #{option}#{marker}"
|
1756
|
+
end
|
1757
|
+
|
1758
|
+
loop do
|
1759
|
+
input = Readline.readline("Your choice (1-#{options.size}): ", true)
|
1760
|
+
|
1761
|
+
if input.nil? || input.strip.empty?
|
1762
|
+
return default if default
|
1763
|
+
puts "Please make a selection."
|
1764
|
+
next
|
1765
|
+
end
|
1766
|
+
|
1767
|
+
begin
|
1768
|
+
choice = input.strip.to_i
|
1769
|
+
if choice.between?(1, options.size)
|
1770
|
+
return choice - 1 # Convert to 0-based index
|
1771
|
+
else
|
1772
|
+
puts "Please enter a number between 1 and #{options.size}."
|
1773
|
+
end
|
1774
|
+
rescue ArgumentError
|
1775
|
+
puts "Please enter a valid number."
|
1776
|
+
end
|
1777
|
+
end
|
1778
|
+
end
|
1779
|
+
|
1780
|
+
# Display progress message
|
1781
|
+
def show_progress(message)
|
1782
|
+
print "\r#{message}".ljust(80)
|
1783
|
+
$stdout.flush
|
1784
|
+
end
|
1785
|
+
|
1786
|
+
# Clear progress message
|
1787
|
+
def clear_progress
|
1788
|
+
print "\r" + " " * 80 + "\r"
|
1789
|
+
$stdout.flush
|
1790
|
+
end
|
1791
|
+
|
1792
|
+
# Get input history
|
1793
|
+
def input_history
|
1794
|
+
@input_history.dup
|
1795
|
+
end
|
1796
|
+
|
1797
|
+
# Clear input history
|
1798
|
+
def clear_history
|
1799
|
+
@input_history.clear
|
1800
|
+
end
|
1801
|
+
|
1802
|
+
# Display interactive help
|
1803
|
+
def show_help
|
1804
|
+
puts "\nš Interactive Prompt Help:"
|
1805
|
+
puts "=" * 40
|
1806
|
+
|
1807
|
+
puts "\nš¤ Input Types:"
|
1808
|
+
puts " ⢠Text: Free-form text input"
|
1809
|
+
puts " ⢠Choice: Select from predefined options"
|
1810
|
+
puts " ⢠Confirmation: Yes/No questions"
|
1811
|
+
puts " ⢠File: File path with @ browsing"
|
1812
|
+
puts " ⢠Number: Integer or decimal numbers"
|
1813
|
+
puts " ⢠Email: Email address format"
|
1814
|
+
puts " ⢠URL: Web URL format"
|
1815
|
+
|
1816
|
+
puts "\nāØļø Special Commands:"
|
1817
|
+
puts " ⢠@: Browse and select files"
|
1818
|
+
puts " ⢠Enter: Use default value (if available)"
|
1819
|
+
puts " ⢠Ctrl+C: Cancel operation"
|
1820
|
+
|
1821
|
+
puts "\nš File Selection:"
|
1822
|
+
puts " ⢠Type @ to browse files"
|
1823
|
+
puts " ⢠Type @search to filter files"
|
1824
|
+
puts " ⢠Select by number or type 0 to cancel"
|
1825
|
+
|
1826
|
+
puts "\nā
Validation:"
|
1827
|
+
puts " ⢠Required fields must be filled"
|
1828
|
+
puts " ⢠Input format is validated automatically"
|
1829
|
+
puts " ⢠Invalid input shows error and retries"
|
1830
|
+
|
1831
|
+
puts "\nš” Tips:"
|
1832
|
+
puts " ⢠Use Tab for auto-completion"
|
1833
|
+
puts " ⢠Arrow keys for history navigation"
|
1834
|
+
puts " ⢠Default values are shown in prompts"
|
1835
|
+
end
|
1836
|
+
|
1837
|
+
# Display question summary
|
1838
|
+
def display_question_summary(questions)
|
1839
|
+
puts "\nš Question Summary:"
|
1840
|
+
puts "-" * 30
|
1841
|
+
|
1842
|
+
questions.each_with_index do |question_data, index|
|
1843
|
+
question_number = question_data[:number] || (index + 1)
|
1844
|
+
question_text = question_data[:question]
|
1845
|
+
question_type = question_data[:type] || "text"
|
1846
|
+
required = question_data[:required] != false
|
1847
|
+
|
1848
|
+
status = required ? "Required" : "Optional"
|
1849
|
+
type_emojis = {
|
1850
|
+
"text" => "š",
|
1851
|
+
"choice" => "š",
|
1852
|
+
"confirmation" => "ā
",
|
1853
|
+
"file" => "š",
|
1854
|
+
"number" => "š¢",
|
1855
|
+
"email" => "š§",
|
1856
|
+
"url" => "š"
|
1857
|
+
}
|
1858
|
+
type_emoji = type_emojis[question_type] || "ā"
|
1859
|
+
|
1860
|
+
puts " #{question_number}. #{type_emoji} #{question_text} (#{status})"
|
1861
|
+
end
|
1862
|
+
end
|
1863
|
+
|
1864
|
+
# Get user preferences for feedback collection
|
1865
|
+
def get_user_preferences
|
1866
|
+
puts "\nāļø User Preferences:"
|
1867
|
+
puts "-" * 25
|
1868
|
+
|
1869
|
+
preferences = {}
|
1870
|
+
|
1871
|
+
# Auto-confirm defaults
|
1872
|
+
preferences[:auto_confirm_defaults] = get_confirmation(
|
1873
|
+
"Auto-confirm default values without prompting?",
|
1874
|
+
default: false
|
1875
|
+
)
|
1876
|
+
|
1877
|
+
# Show help automatically
|
1878
|
+
preferences[:show_help_automatically] = get_confirmation(
|
1879
|
+
"Show help automatically for new question types?",
|
1880
|
+
default: false
|
1881
|
+
)
|
1882
|
+
|
1883
|
+
# Verbose mode
|
1884
|
+
preferences[:verbose_mode] = get_confirmation(
|
1885
|
+
"Enable verbose mode with detailed information?",
|
1886
|
+
default: true
|
1887
|
+
)
|
1888
|
+
|
1889
|
+
# File browsing enabled
|
1890
|
+
preferences[:file_browsing_enabled] = get_confirmation(
|
1891
|
+
"Enable file browsing with @ character?",
|
1892
|
+
default: true
|
1893
|
+
)
|
1894
|
+
|
1895
|
+
preferences
|
1896
|
+
end
|
1897
|
+
|
1898
|
+
# Apply user preferences
|
1899
|
+
def apply_preferences(preferences)
|
1900
|
+
@auto_confirm_defaults = preferences[:auto_confirm_defaults] || false
|
1901
|
+
@show_help_automatically = preferences[:show_help_automatically] || false
|
1902
|
+
@verbose_mode = preferences[:verbose_mode] != false
|
1903
|
+
@file_selection_enabled = preferences[:file_browsing_enabled] != false
|
1904
|
+
end
|
1905
|
+
|
1906
|
+
# Check if help should be shown
|
1907
|
+
def should_show_help?(question_type, seen_types)
|
1908
|
+
return false unless @show_help_automatically
|
1909
|
+
|
1910
|
+
!seen_types.include?(question_type)
|
1911
|
+
end
|
1912
|
+
|
1913
|
+
# Mark question type as seen
|
1914
|
+
def mark_question_type_seen(question_type, seen_types)
|
1915
|
+
seen_types << question_type
|
1916
|
+
end
|
1917
|
+
|
1918
|
+
# Get feedback with preferences
|
1919
|
+
def collect_feedback_with_preferences(questions, context = nil, preferences = {})
|
1920
|
+
# Apply preferences
|
1921
|
+
apply_preferences(preferences)
|
1922
|
+
|
1923
|
+
# Track seen question types
|
1924
|
+
seen_types = Set.new
|
1925
|
+
|
1926
|
+
# Show help if needed
|
1927
|
+
if should_show_help?(questions.first&.dig(:type), seen_types)
|
1928
|
+
show_help
|
1929
|
+
puts "\nPress Enter to continue..."
|
1930
|
+
Readline.readline
|
1931
|
+
end
|
1932
|
+
|
1933
|
+
# Display question summary if verbose
|
1934
|
+
if @verbose_mode
|
1935
|
+
display_question_summary(questions)
|
1936
|
+
puts "\nPress Enter to start answering questions..."
|
1937
|
+
Readline.readline
|
1938
|
+
end
|
1939
|
+
|
1940
|
+
# Collect feedback
|
1941
|
+
responses = collect_feedback(questions, context)
|
1942
|
+
|
1943
|
+
# Mark question types as seen
|
1944
|
+
questions.each do |question_data|
|
1945
|
+
mark_question_type_seen(question_data[:type] || "text", seen_types)
|
1946
|
+
end
|
1947
|
+
|
1948
|
+
responses
|
1949
|
+
end
|
1950
|
+
|
1951
|
+
# Get quick feedback for simple questions
|
1952
|
+
def get_quick_feedback(question, options = {})
|
1953
|
+
question_type = options[:type] || "text"
|
1954
|
+
default_value = options[:default]
|
1955
|
+
required = options[:required] != false
|
1956
|
+
|
1957
|
+
puts "\nā #{question}"
|
1958
|
+
|
1959
|
+
case question_type
|
1960
|
+
when "text"
|
1961
|
+
get_text_response("text", default_value, required)
|
1962
|
+
when "confirmation"
|
1963
|
+
get_confirmation_response(default_value, required)
|
1964
|
+
when "choice"
|
1965
|
+
get_choice_response(options[:options], default_value, required)
|
1966
|
+
else
|
1967
|
+
get_text_response("text", default_value, required)
|
1968
|
+
end
|
1969
|
+
end
|
1970
|
+
|
1971
|
+
# Batch collect feedback for multiple simple questions
|
1972
|
+
def collect_batch_feedback(questions)
|
1973
|
+
responses = {}
|
1974
|
+
|
1975
|
+
puts "\nš Quick Feedback Collection:"
|
1976
|
+
puts "=" * 35
|
1977
|
+
|
1978
|
+
questions.each_with_index do |question_data, index|
|
1979
|
+
question_number = index + 1
|
1980
|
+
question_text = question_data[:question]
|
1981
|
+
question_type = question_data[:type] || "text"
|
1982
|
+
default_value = question_data[:default]
|
1983
|
+
required = question_data[:required] != false
|
1984
|
+
|
1985
|
+
puts "\n#{question_number}. #{question_text}"
|
1986
|
+
|
1987
|
+
response = get_quick_feedback(question_text, {
|
1988
|
+
type: question_type,
|
1989
|
+
default: default_value,
|
1990
|
+
required: required,
|
1991
|
+
options: question_data[:options]
|
1992
|
+
})
|
1993
|
+
|
1994
|
+
responses["question_#{question_number}"] = response
|
1995
|
+
end
|
1996
|
+
|
1997
|
+
puts "\nā
Batch feedback collected."
|
1998
|
+
responses
|
1999
|
+
end
|
2000
|
+
|
2001
|
+
# ============================================================================
|
2002
|
+
# PAUSE/RESUME/STOP CONTROL INTERFACE
|
2003
|
+
# ============================================================================
|
2004
|
+
|
2005
|
+
# Start the control interface
|
2006
|
+
def start_control_interface
|
2007
|
+
return unless @control_interface_enabled
|
2008
|
+
|
2009
|
+
@control_mutex.synchronize do
|
2010
|
+
return if @control_thread&.alive?
|
2011
|
+
|
2012
|
+
# Start control interface using Async (skip in test mode)
|
2013
|
+
unless ENV["RACK_ENV"] == "test" || defined?(RSpec)
|
2014
|
+
require "async"
|
2015
|
+
Async do |task|
|
2016
|
+
task.async { control_interface_loop }
|
2017
|
+
end
|
2018
|
+
end
|
2019
|
+
end
|
2020
|
+
|
2021
|
+
puts "\nš® Control Interface Started"
|
2022
|
+
puts " Press 'p' + Enter to pause"
|
2023
|
+
puts " Press 'r' + Enter to resume"
|
2024
|
+
puts " Press 's' + Enter to stop"
|
2025
|
+
puts " Press 'h' + Enter for help"
|
2026
|
+
puts " Press 'q' + Enter to quit control interface"
|
2027
|
+
puts "=" * 50
|
2028
|
+
end
|
2029
|
+
|
2030
|
+
# Stop the control interface
|
2031
|
+
def stop_control_interface
|
2032
|
+
@control_mutex.synchronize do
|
2033
|
+
if @control_thread&.alive?
|
2034
|
+
@control_thread.kill
|
2035
|
+
@control_thread = nil
|
2036
|
+
end
|
2037
|
+
end
|
2038
|
+
|
2039
|
+
puts "\nš Control Interface Stopped"
|
2040
|
+
end
|
2041
|
+
|
2042
|
+
# Check if pause is requested
|
2043
|
+
def pause_requested?
|
2044
|
+
@control_mutex.synchronize { @pause_requested }
|
2045
|
+
end
|
2046
|
+
|
2047
|
+
# Check if stop is requested
|
2048
|
+
def stop_requested?
|
2049
|
+
@control_mutex.synchronize { @stop_requested }
|
2050
|
+
end
|
2051
|
+
|
2052
|
+
# Check if resume is requested
|
2053
|
+
def resume_requested?
|
2054
|
+
@control_mutex.synchronize { @resume_requested }
|
2055
|
+
end
|
2056
|
+
|
2057
|
+
# Request pause
|
2058
|
+
def request_pause
|
2059
|
+
@control_mutex.synchronize do
|
2060
|
+
@pause_requested = true
|
2061
|
+
@resume_requested = false
|
2062
|
+
end
|
2063
|
+
puts "\nāøļø Pause requested..."
|
2064
|
+
end
|
2065
|
+
|
2066
|
+
# Request stop
|
2067
|
+
def request_stop
|
2068
|
+
@control_mutex.synchronize do
|
2069
|
+
@stop_requested = true
|
2070
|
+
@pause_requested = false
|
2071
|
+
@resume_requested = false
|
2072
|
+
end
|
2073
|
+
puts "\nš Stop requested..."
|
2074
|
+
end
|
2075
|
+
|
2076
|
+
# Request resume
|
2077
|
+
def request_resume
|
2078
|
+
@control_mutex.synchronize do
|
2079
|
+
@resume_requested = true
|
2080
|
+
@pause_requested = false
|
2081
|
+
end
|
2082
|
+
puts "\nā¶ļø Resume requested..."
|
2083
|
+
end
|
2084
|
+
|
2085
|
+
# Clear all control requests
|
2086
|
+
def clear_control_requests
|
2087
|
+
@control_mutex.synchronize do
|
2088
|
+
@pause_requested = false
|
2089
|
+
@stop_requested = false
|
2090
|
+
@resume_requested = false
|
2091
|
+
end
|
2092
|
+
end
|
2093
|
+
|
2094
|
+
# Wait for user control input
|
2095
|
+
def wait_for_control_input
|
2096
|
+
return unless @control_interface_enabled
|
2097
|
+
|
2098
|
+
loop do
|
2099
|
+
if pause_requested?
|
2100
|
+
handle_pause_state
|
2101
|
+
elsif stop_requested?
|
2102
|
+
handle_stop_state
|
2103
|
+
break
|
2104
|
+
elsif resume_requested?
|
2105
|
+
handle_resume_state
|
2106
|
+
break
|
2107
|
+
elsif ENV["RACK_ENV"] == "test" || defined?(RSpec)
|
2108
|
+
sleep(0.1)
|
2109
|
+
else
|
2110
|
+
Async::Task.current.sleep(0.1)
|
2111
|
+
end
|
2112
|
+
end
|
2113
|
+
end
|
2114
|
+
|
2115
|
+
# Handle pause state
|
2116
|
+
def handle_pause_state
|
2117
|
+
puts "\nāøļø HARNESS PAUSED"
|
2118
|
+
puts "=" * 50
|
2119
|
+
puts "š® Control Options:"
|
2120
|
+
puts " 'r' + Enter: Resume execution"
|
2121
|
+
puts " 's' + Enter: Stop execution"
|
2122
|
+
puts " 'h' + Enter: Show help"
|
2123
|
+
puts " 'q' + Enter: Quit control interface"
|
2124
|
+
puts "=" * 50
|
2125
|
+
|
2126
|
+
loop do
|
2127
|
+
input = Readline.readline("Paused> ", true)
|
2128
|
+
|
2129
|
+
case input&.strip&.downcase
|
2130
|
+
when "r", "resume"
|
2131
|
+
request_resume
|
2132
|
+
break
|
2133
|
+
when "s", "stop"
|
2134
|
+
request_stop
|
2135
|
+
break
|
2136
|
+
when "h", "help"
|
2137
|
+
show_control_help
|
2138
|
+
when "q", "quit"
|
2139
|
+
stop_control_interface
|
2140
|
+
break
|
2141
|
+
else
|
2142
|
+
puts "ā Invalid command. Type 'h' for help."
|
2143
|
+
end
|
2144
|
+
end
|
2145
|
+
end
|
2146
|
+
|
2147
|
+
# Handle stop state
|
2148
|
+
def handle_stop_state
|
2149
|
+
puts "\nš HARNESS STOPPED"
|
2150
|
+
puts "=" * 50
|
2151
|
+
puts "Execution has been stopped by user request."
|
2152
|
+
puts "You can restart the harness from where it left off."
|
2153
|
+
puts "=" * 50
|
2154
|
+
end
|
2155
|
+
|
2156
|
+
# Handle resume state
|
2157
|
+
def handle_resume_state
|
2158
|
+
puts "\nā¶ļø HARNESS RESUMED"
|
2159
|
+
puts "=" * 50
|
2160
|
+
puts "Execution has been resumed."
|
2161
|
+
puts "=" * 50
|
2162
|
+
end
|
2163
|
+
|
2164
|
+
# Show control help
|
2165
|
+
def show_control_help
|
2166
|
+
puts "\nš Control Interface Help"
|
2167
|
+
puts "=" * 50
|
2168
|
+
puts "š® Available Commands:"
|
2169
|
+
puts " 'p' or 'pause' - Pause the harness execution"
|
2170
|
+
puts " 'r' or 'resume' - Resume the harness execution"
|
2171
|
+
puts " 's' or 'stop' - Stop the harness execution"
|
2172
|
+
puts " 'h' or 'help' - Show this help message"
|
2173
|
+
puts " 'q' or 'quit' - Quit the control interface"
|
2174
|
+
puts ""
|
2175
|
+
puts "š Control States:"
|
2176
|
+
puts " Running - Harness is executing normally"
|
2177
|
+
puts " Paused - Harness is paused, waiting for resume"
|
2178
|
+
puts " Stopped - Harness has been stopped by user"
|
2179
|
+
puts " Resumed - Harness has been resumed from pause"
|
2180
|
+
puts ""
|
2181
|
+
puts "š” Tips:"
|
2182
|
+
puts " ⢠You can pause/resume/stop at any time during execution"
|
2183
|
+
puts " ⢠The harness will save its state when paused/stopped"
|
2184
|
+
puts " ⢠You can restart from where you left off"
|
2185
|
+
puts " ⢠Use 'h' for help at any time"
|
2186
|
+
puts "=" * 50
|
2187
|
+
end
|
2188
|
+
|
2189
|
+
# Control interface main loop
|
2190
|
+
def control_interface_loop
|
2191
|
+
loop do
|
2192
|
+
input = Readline.readline("Control> ", true)
|
2193
|
+
|
2194
|
+
case input&.strip&.downcase
|
2195
|
+
when "p", "pause"
|
2196
|
+
request_pause
|
2197
|
+
when "r", "resume"
|
2198
|
+
request_resume
|
2199
|
+
when "s", "stop"
|
2200
|
+
request_stop
|
2201
|
+
when "h", "help"
|
2202
|
+
show_control_help
|
2203
|
+
when "q", "quit"
|
2204
|
+
stop_control_interface
|
2205
|
+
break
|
2206
|
+
when ""
|
2207
|
+
# Empty input, continue
|
2208
|
+
next
|
2209
|
+
else
|
2210
|
+
puts "ā Invalid command. Type 'h' for help."
|
2211
|
+
end
|
2212
|
+
rescue Interrupt
|
2213
|
+
puts "\nš Control interface interrupted. Stopping..."
|
2214
|
+
request_stop
|
2215
|
+
break
|
2216
|
+
rescue => e
|
2217
|
+
puts "ā Control interface error: #{e.message}"
|
2218
|
+
puts " Type 'h' for help or 'q' to quit."
|
2219
|
+
end
|
2220
|
+
end
|
2221
|
+
|
2222
|
+
# Check for control input during execution
|
2223
|
+
def check_control_input
|
2224
|
+
return unless @control_interface_enabled
|
2225
|
+
|
2226
|
+
if pause_requested?
|
2227
|
+
handle_pause_state
|
2228
|
+
elsif stop_requested?
|
2229
|
+
handle_stop_state
|
2230
|
+
return :stop
|
2231
|
+
elsif resume_requested?
|
2232
|
+
handle_resume_state
|
2233
|
+
return :resume
|
2234
|
+
end
|
2235
|
+
|
2236
|
+
nil
|
2237
|
+
end
|
2238
|
+
|
2239
|
+
# Enable control interface
|
2240
|
+
def enable_control_interface
|
2241
|
+
@control_interface_enabled = true
|
2242
|
+
puts "š® Control interface enabled"
|
2243
|
+
end
|
2244
|
+
|
2245
|
+
# Disable control interface
|
2246
|
+
def disable_control_interface
|
2247
|
+
@control_interface_enabled = false
|
2248
|
+
stop_control_interface
|
2249
|
+
puts "š® Control interface disabled"
|
2250
|
+
end
|
2251
|
+
|
2252
|
+
# Get control status
|
2253
|
+
def get_control_status
|
2254
|
+
@control_mutex.synchronize do
|
2255
|
+
{
|
2256
|
+
enabled: @control_interface_enabled,
|
2257
|
+
pause_requested: @pause_requested,
|
2258
|
+
stop_requested: @stop_requested,
|
2259
|
+
resume_requested: @resume_requested,
|
2260
|
+
control_thread_alive: @control_thread&.alive? || false
|
2261
|
+
}
|
2262
|
+
end
|
2263
|
+
end
|
2264
|
+
|
2265
|
+
# Display control status
|
2266
|
+
def display_control_status
|
2267
|
+
status = get_control_status
|
2268
|
+
|
2269
|
+
puts "\nš® Control Interface Status"
|
2270
|
+
puts "=" * 40
|
2271
|
+
puts "Enabled: #{status[:enabled] ? "ā
Yes" : "ā No"}"
|
2272
|
+
puts "Pause Requested: #{status[:pause_requested] ? "āøļø Yes" : "ā¶ļø No"}"
|
2273
|
+
puts "Stop Requested: #{status[:stop_requested] ? "š Yes" : "ā¶ļø No"}"
|
2274
|
+
puts "Resume Requested: #{status[:resume_requested] ? "ā¶ļø Yes" : "āøļø No"}"
|
2275
|
+
puts "Control Thread: #{status[:control_thread_alive] ? "š¢ Active" : "š“ Inactive"}"
|
2276
|
+
puts "=" * 40
|
2277
|
+
end
|
2278
|
+
|
2279
|
+
# Interactive control menu
|
2280
|
+
def show_control_menu
|
2281
|
+
puts "\nš® Harness Control Menu"
|
2282
|
+
puts "=" * 50
|
2283
|
+
puts "1. Start Control Interface"
|
2284
|
+
puts "2. Stop Control Interface"
|
2285
|
+
puts "3. Pause Harness"
|
2286
|
+
puts "4. Resume Harness"
|
2287
|
+
puts "5. Stop Harness"
|
2288
|
+
puts "6. Show Control Status"
|
2289
|
+
puts "7. Show Help"
|
2290
|
+
puts "8. Exit Menu"
|
2291
|
+
puts "=" * 50
|
2292
|
+
|
2293
|
+
loop do
|
2294
|
+
choice = Readline.readline("Select option (1-8): ", true)
|
2295
|
+
|
2296
|
+
case choice&.strip
|
2297
|
+
when "1"
|
2298
|
+
start_control_interface
|
2299
|
+
when "2"
|
2300
|
+
stop_control_interface
|
2301
|
+
when "3"
|
2302
|
+
request_pause
|
2303
|
+
when "4"
|
2304
|
+
request_resume
|
2305
|
+
when "5"
|
2306
|
+
request_stop
|
2307
|
+
when "6"
|
2308
|
+
display_control_status
|
2309
|
+
when "7"
|
2310
|
+
show_control_help
|
2311
|
+
when "8"
|
2312
|
+
puts "š Exiting control menu..."
|
2313
|
+
break
|
2314
|
+
else
|
2315
|
+
puts "ā Invalid option. Please select 1-8."
|
2316
|
+
end
|
2317
|
+
end
|
2318
|
+
end
|
2319
|
+
|
2320
|
+
# Quick control commands
|
2321
|
+
def quick_pause
|
2322
|
+
request_pause
|
2323
|
+
puts "āøļø Quick pause requested. Use 'r' to resume."
|
2324
|
+
end
|
2325
|
+
|
2326
|
+
def quick_resume
|
2327
|
+
request_resume
|
2328
|
+
puts "ā¶ļø Quick resume requested."
|
2329
|
+
end
|
2330
|
+
|
2331
|
+
def quick_stop
|
2332
|
+
request_stop
|
2333
|
+
puts "š Quick stop requested."
|
2334
|
+
end
|
2335
|
+
|
2336
|
+
# Control interface with timeout
|
2337
|
+
def control_interface_with_timeout(timeout_seconds = 30)
|
2338
|
+
return unless @control_interface_enabled
|
2339
|
+
|
2340
|
+
start_time = Time.now
|
2341
|
+
|
2342
|
+
loop do
|
2343
|
+
if pause_requested?
|
2344
|
+
handle_pause_state
|
2345
|
+
elsif stop_requested?
|
2346
|
+
handle_stop_state
|
2347
|
+
break
|
2348
|
+
elsif resume_requested?
|
2349
|
+
handle_resume_state
|
2350
|
+
break
|
2351
|
+
elsif Time.now - start_time > timeout_seconds
|
2352
|
+
puts "\nā° Control interface timeout reached. Continuing execution..."
|
2353
|
+
break
|
2354
|
+
elsif ENV["RACK_ENV"] == "test" || defined?(RSpec)
|
2355
|
+
sleep(0.1)
|
2356
|
+
else
|
2357
|
+
Async::Task.current.sleep(0.1)
|
2358
|
+
end
|
2359
|
+
end
|
2360
|
+
end
|
2361
|
+
|
2362
|
+
# Emergency stop
|
2363
|
+
def emergency_stop
|
2364
|
+
puts "\nšØ EMERGENCY STOP INITIATED"
|
2365
|
+
puts "=" * 50
|
2366
|
+
puts "All execution will be halted immediately."
|
2367
|
+
puts "This action cannot be undone."
|
2368
|
+
puts "=" * 50
|
2369
|
+
|
2370
|
+
@control_mutex.synchronize do
|
2371
|
+
@stop_requested = true
|
2372
|
+
@pause_requested = false
|
2373
|
+
@resume_requested = false
|
2374
|
+
end
|
2375
|
+
|
2376
|
+
stop_control_interface
|
2377
|
+
puts "š Emergency stop completed."
|
2378
|
+
end
|
2379
|
+
end
|
2380
|
+
end
|
2381
|
+
end
|