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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -214
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +38 -23
  5. data/lib/aidp/analysis/seams.rb +2 -31
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +1 -13
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
  8. data/lib/aidp/analyze/error_handler.rb +2 -75
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/cli/jobs_command.rb +100 -432
  15. data/lib/aidp/cli.rb +309 -239
  16. data/lib/aidp/config.rb +298 -10
  17. data/lib/aidp/debug_logger.rb +195 -0
  18. data/lib/aidp/debug_mixin.rb +187 -0
  19. data/lib/aidp/execute/progress.rb +9 -0
  20. data/lib/aidp/execute/runner.rb +221 -40
  21. data/lib/aidp/execute/steps.rb +17 -7
  22. data/lib/aidp/execute/workflow_selector.rb +211 -0
  23. data/lib/aidp/harness/completion_checker.rb +268 -0
  24. data/lib/aidp/harness/condition_detector.rb +1526 -0
  25. data/lib/aidp/harness/config_loader.rb +373 -0
  26. data/lib/aidp/harness/config_manager.rb +382 -0
  27. data/lib/aidp/harness/config_schema.rb +1006 -0
  28. data/lib/aidp/harness/config_validator.rb +355 -0
  29. data/lib/aidp/harness/configuration.rb +477 -0
  30. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  31. data/lib/aidp/harness/error_handler.rb +616 -0
  32. data/lib/aidp/harness/provider_config.rb +423 -0
  33. data/lib/aidp/harness/provider_factory.rb +306 -0
  34. data/lib/aidp/harness/provider_manager.rb +1269 -0
  35. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  36. data/lib/aidp/harness/runner.rb +411 -0
  37. data/lib/aidp/harness/state/errors.rb +28 -0
  38. data/lib/aidp/harness/state/metrics.rb +219 -0
  39. data/lib/aidp/harness/state/persistence.rb +128 -0
  40. data/lib/aidp/harness/state/provider_state.rb +132 -0
  41. data/lib/aidp/harness/state/ui_state.rb +68 -0
  42. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  43. data/lib/aidp/harness/state_manager.rb +586 -0
  44. data/lib/aidp/harness/status_display.rb +888 -0
  45. data/lib/aidp/harness/ui/base.rb +16 -0
  46. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  47. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  48. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  49. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  50. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  51. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  52. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  53. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  54. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  55. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  56. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  57. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  58. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  59. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  60. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  61. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  62. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  63. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  64. data/lib/aidp/harness/user_interface.rb +2381 -0
  65. data/lib/aidp/provider_manager.rb +131 -7
  66. data/lib/aidp/providers/anthropic.rb +28 -103
  67. data/lib/aidp/providers/base.rb +170 -0
  68. data/lib/aidp/providers/cursor.rb +52 -181
  69. data/lib/aidp/providers/gemini.rb +24 -107
  70. data/lib/aidp/providers/macos_ui.rb +99 -5
  71. data/lib/aidp/providers/opencode.rb +194 -0
  72. data/lib/aidp/storage/csv_storage.rb +172 -0
  73. data/lib/aidp/storage/file_manager.rb +214 -0
  74. data/lib/aidp/storage/json_storage.rb +140 -0
  75. data/lib/aidp/version.rb +1 -1
  76. data/lib/aidp.rb +54 -39
  77. data/templates/COMMON/AGENT_BASE.md +11 -0
  78. data/templates/EXECUTE/00_PRD.md +4 -4
  79. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  80. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  81. data/templates/EXECUTE/08_TASKS.md +4 -4
  82. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  83. data/templates/README.md +279 -0
  84. data/templates/aidp-development.yml.example +373 -0
  85. data/templates/aidp-minimal.yml.example +48 -0
  86. data/templates/aidp-production.yml.example +475 -0
  87. data/templates/aidp.yml.example +598 -0
  88. metadata +93 -69
  89. data/lib/aidp/analyze/agent_personas.rb +0 -71
  90. data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
  91. data/lib/aidp/analyze/data_retention_manager.rb +0 -421
  92. data/lib/aidp/analyze/database.rb +0 -260
  93. data/lib/aidp/analyze/dependencies.rb +0 -335
  94. data/lib/aidp/analyze/export_manager.rb +0 -418
  95. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  96. data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
  97. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  98. data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
  99. data/lib/aidp/analyze/memory_manager.rb +0 -339
  100. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  101. data/lib/aidp/analyze/parallel_processor.rb +0 -454
  102. data/lib/aidp/analyze/performance_optimizer.rb +0 -691
  103. data/lib/aidp/analyze/repository_chunker.rb +0 -697
  104. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  105. data/lib/aidp/analyze/storage.rb +0 -655
  106. data/lib/aidp/analyze/tool_configuration.rb +0 -441
  107. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  108. data/lib/aidp/database/pg_adapter.rb +0 -148
  109. data/lib/aidp/database_config.rb +0 -69
  110. data/lib/aidp/database_connection.rb +0 -72
  111. data/lib/aidp/job_manager.rb +0 -41
  112. data/lib/aidp/jobs/base_job.rb +0 -45
  113. data/lib/aidp/jobs/provider_execution_job.rb +0 -83
  114. data/lib/aidp/project_detector.rb +0 -117
  115. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  116. data/lib/aidp/providers/supervised_base.rb +0 -317
  117. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  118. data/lib/aidp/sync.rb +0 -13
  119. 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