tng 0.1.4

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.

Potentially problematic release.


This version of tng might be problematic. Click here for more details.

data/bin/tng ADDED
@@ -0,0 +1,1122 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Set a clean process name for the terminal
5
+ $0 = "tng"
6
+
7
+ # Add the lib directory to the load path for development
8
+ lib_path = File.expand_path("../lib", __dir__)
9
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
10
+
11
+ # Load the main gem first to ensure the Rust extension is loaded
12
+ require "tng"
13
+
14
+ require "tty-prompt"
15
+ require "tty-spinner"
16
+ require "tty-box"
17
+ require "tty-table"
18
+ require "tty-progressbar"
19
+ require "tty-screen"
20
+ require "tty-option"
21
+ require "pastel"
22
+ require "tng/ui/show_help"
23
+ require "tng/ui/post_install_box"
24
+ require "tng/ui/system_status_display"
25
+ require "tng/ui/configuration_display"
26
+ require "tng/ui/authentication_warning_display"
27
+ require "tng/ui/display_banner"
28
+ require "tng/ui/user_stats_display"
29
+ require "tng/ui/about_display"
30
+ require "tng/services/testng"
31
+ require "tng/ui/goodbye_display"
32
+ require "tng/api/http_client"
33
+ require "tng/ui/controller_test_flow_display"
34
+ require "tng/ui/model_test_flow_display"
35
+ require "tng/ui/service_test_flow_display"
36
+ require "tng/analyzers/controller"
37
+ require "tng/analyzers/model"
38
+ require "tng/analyzers/service"
39
+ require "tng/services/test_generator"
40
+ require "tng/services/user_app_config"
41
+ require "tng/utils"
42
+
43
+ class CLI
44
+ include TTY::Option
45
+ include Tng::Utils
46
+
47
+ usage do
48
+ program "tng"
49
+ desc "LLM-Powered Rails Test Generator"
50
+ example "Interactive mode (full UI with search & selection):", "bundle exec tng"
51
+ example " • Browse and search controllers/models", ""
52
+ example " • Select specific files from filtered lists", ""
53
+ example " • Choose test generation options", ""
54
+ example "", ""
55
+ example "Direct generation (bypass UI):", ""
56
+ example "Standard format:", "bundle exec tng --type=controller --file=users_controller"
57
+ example "Short aliases:", "bundle exec tng -t c -f ping"
58
+ example "Mixed format:", "bundle exec tng -t=c f=ping"
59
+ example "All equivalent:", "bundle exec tng --type=model --file=user"
60
+ example " ", "bundle exec tng -t m -f user"
61
+ example " ", "bundle exec tng -t=m f=user"
62
+ end
63
+
64
+ option :type do
65
+ short "-t"
66
+ long "--type=TYPE"
67
+ desc "Test type (controller, model, service)"
68
+ permit %w[controller model service c m mo s se]
69
+ end
70
+
71
+ option :file do
72
+ short "-f"
73
+ long "--file=FILE"
74
+ desc "File name (without .rb extension)"
75
+ end
76
+
77
+ option :method do
78
+ short "-m"
79
+ long "--method=METHOD"
80
+ desc "Method name (for per-method tests)"
81
+ end
82
+
83
+ flag :help do
84
+ short "-h"
85
+ long "--help"
86
+ desc "Show help message"
87
+ end
88
+
89
+ def initialize
90
+ @prompt = TTY::Prompt.new
91
+ @pastel = Pastel.new
92
+ @terminal_width = begin
93
+ TTY::Screen.width
94
+ rescue StandardError
95
+ 80
96
+ end
97
+ @config_initialized = false
98
+ @http_client = nil
99
+ @testng = nil
100
+ end
101
+
102
+ def start
103
+ # Preprocess ARGV to handle mixed formats like: -t=value or f=value
104
+ normalized_argv = preprocess_arguments(ARGV.dup)
105
+
106
+ parse(normalized_argv)
107
+
108
+ if params[:help]
109
+ help_display = ShowHelp.new(@pastel, Tng::VERSION)
110
+ puts help_display.render
111
+ return
112
+ end
113
+
114
+ # Load Rails environment if we're in a Rails application (only for non-help commands)
115
+ rails_loaded = load_rails_environment
116
+
117
+ # Initialize config and clients after Rails is loaded
118
+ initialize_config_and_clients if rails_loaded
119
+
120
+ if params[:type] && params[:file]
121
+ # Initialize config for direct generation if not already done
122
+ initialize_config_and_clients unless @config_initialized
123
+ run_direct_generation
124
+ else
125
+ # Check if we're in a Rails app and if Rails loaded properly
126
+ if find_rails_root && !rails_loaded && !defined?(Rails)
127
+ puts @pastel.red("❌ Failed to load Rails environment.")
128
+ puts @pastel.yellow("Please ensure you're running this command from a Rails application directory.")
129
+ puts @pastel.dim("Try running: bundle exec tng")
130
+ return
131
+ end
132
+
133
+ # Check if configuration is set up before proceeding
134
+ return unless check_configuration
135
+
136
+ check_system_status
137
+ clear_screen
138
+ puts DisplayBanner.new(@pastel, Tng::VERSION).render
139
+ main_menu
140
+ end
141
+ end
142
+
143
+ def preprocess_arguments(argv)
144
+ normalized = []
145
+
146
+ argv.each do |arg|
147
+ case arg
148
+ when /^(type|t)=(.+)$/ # Handle type=value or t=value (without dash)
149
+ normalized << "--type=#{::Regexp.last_match(2)}"
150
+ when /^(file|f)=(.+)$/ # Handle file=value or f=value (without dash)
151
+ normalized << "--file=#{::Regexp.last_match(2)}"
152
+ when /^(method|m)=(.+)$/ # Handle method=value or m=value (without dash)
153
+ normalized << "--method=#{::Regexp.last_match(2)}"
154
+ when /^(help|h)=(.+)$/ # Handle help=value or h=value (without dash)
155
+ normalized << "--help=#{::Regexp.last_match(2)}"
156
+ when /^-([tf])=(.+)$/ # Handle -t=value or -f=value (convert to long form)
157
+ key = ::Regexp.last_match(1) == "t" ? "type" : "file"
158
+ normalized << "--#{key}=#{::Regexp.last_match(2)}"
159
+ when /^-([mh])=(.+)$/ # Handle -m=value or -h=value
160
+ key = ::Regexp.last_match(1) == "m" ? "method" : "help"
161
+ normalized << "--#{key}=#{::Regexp.last_match(2)}"
162
+ when /^--\w+=.+$/ # Handle --key=value (already correct)
163
+ normalized << arg
164
+ when /^-[tfmh]$/ # Handle single flags like -t, -f, -m, -h (keep as is)
165
+ normalized << arg
166
+ when /^--\w+$/ # Handle long flags like --help (keep as is)
167
+ normalized << arg
168
+ else
169
+ # Regular arguments or values
170
+ normalized << arg
171
+ end
172
+ end
173
+
174
+ normalized
175
+ end
176
+
177
+ def main_menu
178
+ loop do
179
+ clear_screen
180
+ puts DisplayBanner.new(@pastel, Tng::VERSION).render
181
+
182
+ choice = @prompt.select(
183
+ center_text("Select an option:"),
184
+ cycle: true,
185
+ per_page: 5,
186
+ symbols: { marker: "→" }
187
+ ) do |menu|
188
+ menu.choice "Generate tests", :tests
189
+ menu.choice "User stats", :stats
190
+ menu.choice "About", :about
191
+ menu.choice "Exit", :exit, key: "q"
192
+ end
193
+
194
+ handle_choice(choice)
195
+ break if choice == :exit
196
+ end
197
+ end
198
+
199
+ def handle_choice(choice)
200
+ case choice
201
+ when :tests
202
+ handle_test_generation
203
+ when :stats
204
+ user_stats
205
+ when :about
206
+ show_about
207
+ when :exit
208
+ show_goodbye
209
+ end
210
+ end
211
+
212
+ def handle_test_generation
213
+ choice = @prompt.select(
214
+ center_text("What would you like to test?"),
215
+ cycle: true,
216
+ per_page: 5,
217
+ symbols: { marker: "→" }
218
+ ) do |menu|
219
+ menu.choice "Controller", :controller
220
+ menu.choice "Model", :model
221
+ menu.choice "Service", :service
222
+ menu.choice @pastel.cyan("⬅️ Back"), :back
223
+ end
224
+
225
+ case choice
226
+ when :controller
227
+ generate_controller_tests
228
+ when :model
229
+ generate_model_tests
230
+ when :service
231
+ generate_service_tests
232
+ when :route
233
+ generate_route_tests
234
+ end
235
+ end
236
+
237
+ def generate_controller_tests
238
+ header = @pastel.bright_white.bold("Scanning for controllers...")
239
+ puts center_text(header)
240
+
241
+ spinner = TTY::Spinner.new(
242
+ center_text("[:spinner] Analyzing controllers..."),
243
+ format: :dots,
244
+ success_mark: @pastel.green("✅"),
245
+ error_mark: @pastel.red("❌")
246
+ )
247
+ spinner.auto_spin
248
+ controllers = Tng::Analyzers::Controller.files_in_dir("app/controllers").map do |file|
249
+ # Extract the full path relative to app/controllers to build proper namespace
250
+ relative_path = file[:path].gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
251
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
252
+
253
+ {
254
+ name: namespaced_name,
255
+ path: file[:path]
256
+ }
257
+ end
258
+
259
+ flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
260
+
261
+ if controllers.empty?
262
+ spinner.error
263
+ flow_display.show_no_controllers_message
264
+ return
265
+ end
266
+
267
+ success_msg = @pastel.green("Found #{controllers.length} controllers")
268
+ spinner.success(center_text(success_msg))
269
+
270
+ controller_choice = flow_display.select_controller(controllers)
271
+ return if controller_choice == :back
272
+
273
+ show_controller_test_options(controller_choice)
274
+ end
275
+
276
+ def generate_model_tests
277
+ header = @pastel.bright_white.bold("Scanning for models...")
278
+ puts center_text(header)
279
+
280
+ spinner = TTY::Spinner.new(
281
+ center_text("[:spinner] Analyzing models..."),
282
+ format: :dots,
283
+ success_mark: @pastel.green("✅"),
284
+ error_mark: @pastel.red("❌")
285
+ )
286
+ spinner.auto_spin
287
+
288
+ models = Tng::Analyzers::Model.files_in_dir("app/models").map do |file|
289
+ # Extract the full path relative to app/models to build proper namespace
290
+ relative_path = file[:path].gsub(%r{^.*app/models/}, "").gsub(".rb", "")
291
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
292
+
293
+ {
294
+ name: namespaced_name,
295
+ path: file[:path]
296
+ }
297
+ end
298
+
299
+ flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
300
+
301
+ if models.empty?
302
+ spinner.error
303
+ flow_display.show_no_models_message
304
+ return
305
+ end
306
+
307
+ success_msg = @pastel.green("Found #{models.length} models")
308
+ spinner.success(center_text(success_msg))
309
+
310
+ model_choice = flow_display.select_model(models)
311
+ return if model_choice == :back
312
+
313
+ show_model_test_options(model_choice)
314
+ end
315
+
316
+ def generate_service_tests
317
+ header = @pastel.bright_white.bold("Scanning for services...")
318
+ puts center_text(header)
319
+
320
+ spinner = TTY::Spinner.new(
321
+ center_text("[:spinner] Analyzing services..."),
322
+ format: :dots,
323
+ success_mark: @pastel.green("✅"),
324
+ error_mark: @pastel.red("❌")
325
+ )
326
+ spinner.auto_spin
327
+
328
+ services = Tng::Analyzers::Service.files_in_dir.map do |file|
329
+ # Extract the full path relative to app/services or app/service to build proper namespace
330
+ relative_path = file[:path].gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
331
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
332
+
333
+ {
334
+ name: namespaced_name,
335
+ path: file[:path]
336
+ }
337
+ end
338
+
339
+ flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
340
+
341
+ if services.empty?
342
+ spinner.error
343
+ flow_display.show_no_services_message
344
+ return
345
+ end
346
+
347
+ success_msg = @pastel.green("Found #{services.length} services")
348
+ spinner.success(center_text(success_msg))
349
+
350
+ service_choice = flow_display.select_service(services)
351
+ return if service_choice == :back
352
+
353
+ show_service_test_options(service_choice)
354
+ end
355
+
356
+ def generate_test_for_controller(controller)
357
+ header = @pastel.bright_white.bold("🎯 Generating tests for #{controller[:name]}")
358
+ puts center_text(header)
359
+
360
+ # Show controller analysis
361
+ analysis_msg = @pastel.dim("Analyzing controller structure...")
362
+ puts center_text(analysis_msg)
363
+
364
+ # Center the progress bar
365
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
366
+ progress_bar = TTY::ProgressBar.new(
367
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
368
+ total: 5,
369
+ bar_format: :box,
370
+ complete: @pastel.green("█"),
371
+ incomplete: @pastel.dim("░"),
372
+ width: 40
373
+ )
374
+
375
+ step_msg = @pastel.dim("Parsing controller file...")
376
+ puts center_text(step_msg)
377
+ progress_bar.advance(1)
378
+ sleep(0.3)
379
+
380
+ step_msg = @pastel.dim("Extracting actions...")
381
+ puts center_text(step_msg)
382
+ progress_bar.advance(1)
383
+ sleep(0.3)
384
+
385
+ step_msg = @pastel.dim("Analyzing dependencies...")
386
+ puts center_text(step_msg)
387
+ progress_bar.advance(1)
388
+ sleep(0.3)
389
+
390
+ step_msg = @pastel.dim("Generating test code...")
391
+ puts center_text(step_msg)
392
+ progress_bar.advance(1)
393
+ # test for the controller
394
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_controller(controller)
395
+ sleep(0.5)
396
+
397
+ step_msg = @pastel.dim("Writing test file...")
398
+ puts center_text(step_msg)
399
+ progress_bar.advance(1)
400
+ sleep(0.2)
401
+
402
+ completion_msg = @pastel.green.bold("✅ Test generation completed!")
403
+ puts center_text(completion_msg)
404
+
405
+ # Show post-generation menu if test was generated successfully
406
+ if result && result[:file_path]
407
+ show_post_generation_menu(result)
408
+ else
409
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
410
+ end
411
+ end
412
+
413
+ def show_controller_method_selection(controller)
414
+ header = @pastel.bright_white.bold("🔍 Analyzing methods for #{controller[:name]}")
415
+ puts center_text(header)
416
+
417
+ # Extract methods from controller
418
+ methods = extract_controller_methods(controller)
419
+
420
+ if methods.empty?
421
+ flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
422
+ flow_display.show_no_methods_message(controller)
423
+ return
424
+ end
425
+
426
+ # Show method selection UI
427
+ flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
428
+ method_choice = flow_display.select_controller_method(controller, methods)
429
+
430
+ return if method_choice == :back
431
+
432
+ # Generate test for selected method
433
+ generate_test_for_controller_method(controller, method_choice)
434
+ end
435
+
436
+ def extract_controller_methods(controller)
437
+ methods = Tng::Analyzers::Controller.methods_for_controller(controller[:name])
438
+ Array(methods)
439
+ rescue StandardError => e
440
+ puts center_text(@pastel.red("❌ Error analyzing controller: #{e.message}"))
441
+ []
442
+ end
443
+
444
+ def generate_test_for_controller_method(controller, method_info)
445
+ header = @pastel.bright_white.bold("🎯 Generating test for #{controller[:name]}##{method_info[:name]}")
446
+ puts center_text(header)
447
+
448
+ # Show method analysis
449
+ analysis_msg = @pastel.dim("Analyzing method structure...")
450
+ puts center_text(analysis_msg)
451
+
452
+ # Center the progress bar
453
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
454
+ progress_bar = TTY::ProgressBar.new(
455
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
456
+ total: 5,
457
+ bar_format: :box,
458
+ complete: @pastel.green("█"),
459
+ incomplete: @pastel.dim("░"),
460
+ width: 40
461
+ )
462
+
463
+ step_msg = @pastel.dim("Parsing method details...")
464
+ puts center_text(step_msg)
465
+ progress_bar.advance(1)
466
+ sleep(0.3)
467
+
468
+ step_msg = @pastel.dim("Analyzing method dependencies...")
469
+ puts center_text(step_msg)
470
+ progress_bar.advance(1)
471
+ sleep(0.3)
472
+
473
+ step_msg = @pastel.dim("Extracting method context...")
474
+ puts center_text(step_msg)
475
+ progress_bar.advance(1)
476
+ sleep(0.3)
477
+
478
+ step_msg = @pastel.dim("Generating method test code...")
479
+ puts center_text(step_msg)
480
+ progress_bar.advance(1)
481
+ # Generate test for the specific method
482
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_controller_method(controller, method_info)
483
+ sleep(0.5)
484
+
485
+ step_msg = @pastel.dim("Writing test file...")
486
+ puts center_text(step_msg)
487
+ progress_bar.advance(1)
488
+ sleep(0.2)
489
+
490
+ completion_msg = @pastel.green.bold("✅ Method test generation completed!")
491
+ puts center_text(completion_msg)
492
+
493
+ # Show post-generation menu if test was generated successfully
494
+ if result && result[:file_path]
495
+ show_post_generation_menu(result)
496
+ else
497
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
498
+ end
499
+ end
500
+
501
+ def show_controller_test_options(controller_choice)
502
+ flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
503
+ test_option = flow_display.select_test_option(controller_choice)
504
+
505
+ return if test_option == :back
506
+
507
+ # Check authentication configuration before proceeding with test generation
508
+ return unless check_authentication_configuration
509
+
510
+ case test_option
511
+ when :all_possible_tests
512
+ generate_test_for_controller(controller_choice)
513
+ when :per_method
514
+ show_controller_method_selection(controller_choice)
515
+ end
516
+ end
517
+
518
+ def show_model_test_options(model_choice)
519
+ flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
520
+ test_option = flow_display.select_test_option(model_choice)
521
+
522
+ return if test_option == :back
523
+
524
+ # Check authentication configuration before proceeding with test generation
525
+ return unless check_authentication_configuration
526
+
527
+ case test_option
528
+ when :all_possible_tests
529
+ generate_test_for_model(model_choice)
530
+ when :per_method
531
+ show_model_method_selection(model_choice)
532
+ end
533
+ end
534
+
535
+ def show_service_test_options(service_choice)
536
+ flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
537
+ test_option = flow_display.select_test_option(service_choice)
538
+
539
+ return if test_option == :back
540
+
541
+ # Check authentication configuration before proceeding with test generation
542
+ return unless check_authentication_configuration
543
+
544
+ case test_option
545
+ when :all_possible_tests
546
+ generate_test_for_service(service_choice)
547
+ when :per_method
548
+ show_service_method_selection(service_choice)
549
+ end
550
+ end
551
+
552
+ def generate_test_for_model(model)
553
+ header = @pastel.bright_white.bold("🎯 Generating tests for #{model[:name]}")
554
+ puts center_text(header)
555
+
556
+ # Show model analysis
557
+ analysis_msg = @pastel.dim("Analyzing model structure...")
558
+ puts center_text(analysis_msg)
559
+
560
+ # Center the progress bar
561
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
562
+ progress_bar = TTY::ProgressBar.new(
563
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
564
+ total: 4,
565
+ bar_format: :box,
566
+ complete: @pastel.green("█"),
567
+ incomplete: @pastel.dim("░"),
568
+ width: 40
569
+ )
570
+
571
+ step_msg = @pastel.dim("Parsing model file...")
572
+ puts center_text(step_msg)
573
+ progress_bar.advance(1)
574
+ sleep(0.3)
575
+
576
+ step_msg = @pastel.dim("Analyzing validations...")
577
+ puts center_text(step_msg)
578
+ progress_bar.advance(1)
579
+ sleep(0.3)
580
+
581
+ step_msg = @pastel.dim("Analyzing associations...")
582
+ puts center_text(step_msg)
583
+ progress_bar.advance(1)
584
+ sleep(0.3)
585
+
586
+ step_msg = @pastel.dim("Generating test file...")
587
+ puts center_text(step_msg)
588
+ progress_bar.advance(1)
589
+ sleep(0.3)
590
+
591
+ # test for the model
592
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_model(model)
593
+ sleep(0.5)
594
+
595
+ step_msg = @pastel.dim("Writing test file...")
596
+ puts center_text(step_msg)
597
+ progress_bar.advance(1)
598
+ sleep(0.2)
599
+ p result
600
+ completion_msg = @pastel.green.bold("✅ Test generation completed!")
601
+ puts center_text(completion_msg)
602
+
603
+ # Show post-generation menu if test was generated successfully
604
+ if result && result[:file_path]
605
+ show_post_generation_menu(result)
606
+ else
607
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
608
+ end
609
+ end
610
+
611
+ def show_model_method_selection(model)
612
+ header = @pastel.bright_white.bold("🔍 Analyzing methods for #{model[:name]}")
613
+ puts center_text(header)
614
+
615
+ # Extract methods from model
616
+ methods = extract_model_methods(model)
617
+
618
+ if methods.empty?
619
+ flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
620
+ flow_display.show_no_methods_message(model)
621
+ return
622
+ end
623
+
624
+ # Show method selection UI
625
+ flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
626
+ method_choice = flow_display.select_model_method(model, methods)
627
+
628
+ return if method_choice == :back
629
+
630
+ # Generate test for selected method
631
+ generate_test_for_model_method(model, method_choice)
632
+ end
633
+
634
+ def extract_model_methods(model)
635
+ methods = Tng::Analyzers::Model.methods_for_model(model[:name])
636
+ Array(methods)
637
+ rescue StandardError => e
638
+ puts center_text(@pastel.red("❌ Error analyzing model: #{e.message}"))
639
+ []
640
+ end
641
+
642
+ def extract_service_methods(service)
643
+ methods = Tng::Analyzers::Service.methods_for_service(service[:name])
644
+ Array(methods)
645
+ rescue StandardError => e
646
+ puts center_text(@pastel.red("❌ Error analyzing service: #{e.message}"))
647
+ []
648
+ end
649
+
650
+ def generate_test_for_model_method(model, method_info)
651
+ header = @pastel.bright_white.bold("🎯 Generating test for #{model[:name]}##{method_info[:name]}")
652
+ puts center_text(header)
653
+
654
+ # Show method analysis
655
+ analysis_msg = @pastel.dim("Analyzing method structure...")
656
+ puts center_text(analysis_msg)
657
+
658
+ # Center the progress bar
659
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
660
+ progress_bar = TTY::ProgressBar.new(
661
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
662
+ total: 4,
663
+ bar_format: :box,
664
+ complete: @pastel.green("█"),
665
+ incomplete: @pastel.dim("░"),
666
+ width: 40
667
+ )
668
+
669
+ step_msg = @pastel.dim("Parsing method details...")
670
+ puts center_text(step_msg)
671
+ progress_bar.advance(1)
672
+ sleep(0.3)
673
+
674
+ step_msg = @pastel.dim("Analyzing method dependencies...")
675
+ puts center_text(step_msg)
676
+ progress_bar.advance(1)
677
+ sleep(0.3)
678
+
679
+ step_msg = @pastel.dim("Generating method test code...")
680
+ puts center_text(step_msg)
681
+ progress_bar.advance(1)
682
+ # Generate test for the specific method
683
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_model_method(model, method_info)
684
+ sleep(0.5)
685
+
686
+ step_msg = @pastel.dim("Writing test file...")
687
+ puts center_text(step_msg)
688
+ progress_bar.advance(1)
689
+ sleep(0.2)
690
+
691
+ completion_msg = @pastel.green.bold("✅ Method test generation completed!")
692
+ puts center_text(completion_msg)
693
+
694
+ # Show post-generation menu if test was generated successfully
695
+ if result && result[:file_path]
696
+ show_post_generation_menu(result)
697
+ else
698
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
699
+ end
700
+ end
701
+
702
+ def generate_test_for_service_method(service, method_info)
703
+ header = @pastel.bright_white.bold("🎯 Generating test for #{service[:name]}##{method_info[:name]}")
704
+ puts center_text(header)
705
+
706
+ # Show method analysis
707
+ analysis_msg = @pastel.dim("Analyzing method structure...")
708
+ puts center_text(analysis_msg)
709
+
710
+ # Center the progress bar
711
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
712
+ progress_bar = TTY::ProgressBar.new(
713
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
714
+ total: 4,
715
+ bar_format: :box,
716
+ complete: @pastel.green("█"),
717
+ incomplete: @pastel.dim("░"),
718
+ width: 40
719
+ )
720
+
721
+ step_msg = @pastel.dim("Parsing method details...")
722
+ puts center_text(step_msg)
723
+ progress_bar.advance(1)
724
+ sleep(0.3)
725
+
726
+ step_msg = @pastel.dim("Analyzing method dependencies...")
727
+ puts center_text(step_msg)
728
+ progress_bar.advance(1)
729
+ sleep(0.3)
730
+
731
+ step_msg = @pastel.dim("Generating method test code...")
732
+ puts center_text(step_msg)
733
+ progress_bar.advance(1)
734
+ # Generate test for the specific method
735
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_service_method(service, method_info)
736
+ sleep(0.5)
737
+
738
+ step_msg = @pastel.dim("Writing test file...")
739
+ puts center_text(step_msg)
740
+ progress_bar.advance(1)
741
+ sleep(0.2)
742
+
743
+ completion_msg = @pastel.green.bold("✅ Method test generation completed!")
744
+ puts center_text(completion_msg)
745
+
746
+ # Show post-generation menu if test was generated successfully
747
+ if result && result[:file_path]
748
+ show_post_generation_menu(result)
749
+ else
750
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
751
+ end
752
+ end
753
+
754
+ def show_service_method_selection(service)
755
+ header = @pastel.bright_white.bold("🔍 Analyzing methods for #{service[:name]}")
756
+ puts center_text(header)
757
+
758
+ # Extract methods from service
759
+ methods = extract_service_methods(service)
760
+
761
+ if methods.empty?
762
+ flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
763
+ flow_display.show_no_methods_message(service)
764
+ return
765
+ end
766
+
767
+ # Show method selection UI
768
+ flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
769
+ method_choice = flow_display.select_service_method(service, methods)
770
+
771
+ return if method_choice == :back
772
+
773
+ # Generate test for selected method
774
+ generate_test_for_service_method(service, method_choice)
775
+ end
776
+
777
+ def generate_test_for_service(service)
778
+ header = @pastel.bright_white.bold("🎯 Generating tests for #{service[:name]}")
779
+ puts center_text(header)
780
+
781
+ # Show service analysis
782
+ analysis_msg = @pastel.dim("Analyzing service structure...")
783
+ puts center_text(analysis_msg)
784
+
785
+ # Center the progress bar
786
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
787
+ progress_bar = TTY::ProgressBar.new(
788
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
789
+ total: 4,
790
+ bar_format: :box,
791
+ complete: @pastel.green("█"),
792
+ incomplete: @pastel.dim("░"),
793
+ width: 40
794
+ )
795
+
796
+ step_msg = @pastel.dim("Parsing service file...")
797
+ puts center_text(step_msg)
798
+ progress_bar.advance(1)
799
+ sleep(0.3)
800
+
801
+ step_msg = @pastel.dim("Analyzing methods...")
802
+ puts center_text(step_msg)
803
+ progress_bar.advance(1)
804
+ sleep(0.3)
805
+
806
+ step_msg = @pastel.dim("Analyzing dependencies...")
807
+ puts center_text(step_msg)
808
+ progress_bar.advance(1)
809
+ sleep(0.3)
810
+
811
+ step_msg = @pastel.dim("Generating test file...")
812
+ puts center_text(step_msg)
813
+ progress_bar.advance(1)
814
+ sleep(0.3)
815
+
816
+ # test for the service
817
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_service(service)
818
+ sleep(0.5)
819
+
820
+ step_msg = @pastel.dim("Writing test file...")
821
+ puts center_text(step_msg)
822
+ progress_bar.advance(1)
823
+ sleep(0.2)
824
+
825
+ completion_msg = @pastel.green.bold("✅ Test generation completed!")
826
+ puts center_text(completion_msg)
827
+
828
+ # Show post-generation menu if test was generated successfully
829
+ if result && result[:file_path]
830
+ show_post_generation_menu(result)
831
+ else
832
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
833
+ end
834
+ end
835
+
836
+ # STATS
837
+ def user_stats
838
+ stats_data = @testng.get_user_stats
839
+ UserStatsDisplay.new(@pastel, @prompt).display(stats_data)
840
+ end
841
+
842
+ def check_system_status
843
+ # Only show status check in interactive mode
844
+ puts @pastel.dim("🔍 Checking system status...") unless params[:type] && params[:file]
845
+
846
+ status = @testng.check_system_status
847
+ status_ok = SystemStatusDisplay.new(@pastel, params).display(status)
848
+
849
+ return if status_ok
850
+
851
+ puts
852
+ puts @pastel.dim("Press any key to exit...")
853
+ $stdin.getch
854
+ exit(1)
855
+ end
856
+
857
+ def check_configuration
858
+ return true if Tng::Services::UserAppConfig.configured?
859
+
860
+ missing = Tng::Services::UserAppConfig.missing_config
861
+
862
+ ConfigurationDisplay.new(@pastel).display_missing_config(missing)
863
+ false
864
+ end
865
+
866
+ def check_authentication_configuration
867
+ auth_warning = AuthenticationWarningDisplay.new(@pastel)
868
+ auth_missing = auth_warning.display_if_missing
869
+
870
+ if auth_missing
871
+ # User chose to cancel (pressed Esc)
872
+ return false
873
+ end
874
+
875
+ # User chose to continue (pressed Enter) or no auth issues
876
+ true
877
+ end
878
+
879
+ # ABOUT
880
+ def show_about
881
+ AboutDisplay.new(@pastel, @prompt, Tng::VERSION).display
882
+ end
883
+
884
+ # GOODBYE
885
+ def show_goodbye
886
+ clear_screen
887
+ GoodbyeDisplay.new(@pastel).display
888
+ end
889
+
890
+ def run_direct_generation
891
+ # Normalize type aliases
892
+ type = normalize_type(params[:type])
893
+
894
+ unless %w[controller model service].include?(type)
895
+ puts @pastel.red("❌ Invalid type: #{params[:type]}")
896
+ puts @pastel.yellow("Supported types: controller, model, service")
897
+ return
898
+ end
899
+
900
+ # Check configuration first
901
+ return unless check_configuration
902
+
903
+ # Check authentication configuration
904
+ return unless check_authentication_configuration
905
+
906
+ # Check system status (but don't show the spinner in direct mode)
907
+ status = @testng.check_system_status
908
+ unless SystemStatusDisplay.new(@pastel, params).display(status)
909
+ puts @pastel.red("❌ System status check failed")
910
+ return
911
+ end
912
+
913
+ case type
914
+ when "controller"
915
+ run_direct_controller_generation
916
+ when "model"
917
+ run_direct_model_generation
918
+ when "service"
919
+ run_direct_service_generation
920
+ end
921
+ end
922
+
923
+ def normalize_type(type_param)
924
+ case type_param&.downcase
925
+ when "c", "controller"
926
+ "controller"
927
+ when "m", "mo", "model"
928
+ "model"
929
+ when "s", "se", "service"
930
+ "service"
931
+ else
932
+ type_param&.downcase
933
+ end
934
+ end
935
+
936
+ def run_direct_controller_generation
937
+ file_name = params[:file]
938
+ file_name += "_controller" unless file_name.end_with?("_controller")
939
+
940
+ # Build the expected path
941
+ controller_path = File.join("app/controllers", "#{file_name}.rb")
942
+
943
+ # Check if file exists
944
+ unless File.exist?(controller_path)
945
+ puts @pastel.red("❌ Controller file not found: #{controller_path}")
946
+ return
947
+ end
948
+
949
+ # Extract the full path relative to app/controllers to build proper namespace
950
+ relative_path = controller_path.gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
951
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
952
+
953
+ controller = {
954
+ name: namespaced_name,
955
+ path: controller_path
956
+ }
957
+
958
+ puts @pastel.bright_white("🎯 Generating tests for #{controller[:name]}...")
959
+
960
+ # Generate the test directly without progress bars for cleaner output
961
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_controller(controller)
962
+
963
+ if result && result[:file_path]
964
+ show_post_generation_menu(result)
965
+ else
966
+ puts @pastel.red("❌ Failed to generate test")
967
+ end
968
+ end
969
+
970
+ def run_direct_model_generation
971
+ file_name = params[:file]
972
+
973
+ # Build the expected path
974
+ model_path = File.join("app/models", "#{file_name}.rb")
975
+
976
+ # Check if file exists
977
+ unless File.exist?(model_path)
978
+ puts @pastel.red("❌ Model file not found: #{model_path}")
979
+ return
980
+ end
981
+
982
+ # Extract the full path relative to app/models to build proper namespace
983
+ relative_path = model_path.gsub(%r{^.*app/models/}, "").gsub(".rb", "")
984
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
985
+
986
+ model = {
987
+ name: namespaced_name,
988
+ path: model_path
989
+ }
990
+
991
+ puts @pastel.bright_white("🎯 Generating tests for #{model[:name]}...")
992
+
993
+ # Generate the test directly without progress bars for cleaner output
994
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_model(model)
995
+
996
+ if result && result[:file_path]
997
+ show_post_generation_menu(result)
998
+ else
999
+ puts @pastel.red("❌ Failed to generate test")
1000
+ end
1001
+ end
1002
+
1003
+ def run_direct_service_generation
1004
+ file_name = params[:file]
1005
+
1006
+ # Try different service directories
1007
+ service_dirs = ["app/services", "app/service"]
1008
+ service_path = nil
1009
+
1010
+ service_dirs.each do |dir|
1011
+ potential_path = File.join(dir, "#{file_name}.rb")
1012
+ if File.exist?(potential_path)
1013
+ service_path = potential_path
1014
+ break
1015
+ end
1016
+ end
1017
+
1018
+ # Check if file exists
1019
+ unless service_path
1020
+ puts @pastel.red("❌ Service file not found: #{file_name}.rb")
1021
+ puts @pastel.yellow("Searched in: #{service_dirs.join(", ")}")
1022
+ return
1023
+ end
1024
+
1025
+ # Extract the full path relative to app/services or app/service to build proper namespace
1026
+ relative_path = service_path.gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
1027
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
1028
+
1029
+ service = {
1030
+ name: namespaced_name,
1031
+ path: service_path
1032
+ }
1033
+
1034
+ puts @pastel.bright_white("🎯 Generating tests for #{service[:name]}...")
1035
+
1036
+ # Generate the test directly without progress bars for cleaner output
1037
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_service(service)
1038
+
1039
+ if result && result[:file_path]
1040
+ show_post_generation_menu(result)
1041
+ else
1042
+ puts @pastel.red("❌ Failed to generate test")
1043
+ end
1044
+ end
1045
+
1046
+ def show_post_generation_menu(result)
1047
+ # Check if we're in direct mode
1048
+ is_direct_mode = params[:type] && params[:file]
1049
+
1050
+ if is_direct_mode
1051
+ # In direct mode, show info and exit
1052
+ puts
1053
+ header = @pastel.bright_white.bold("🎉 Test Generated Successfully!")
1054
+ puts center_text(header)
1055
+ puts
1056
+
1057
+ # Show file info
1058
+ info_msg = [
1059
+ @pastel.cyan("📁 File: #{result[:file_path]}"),
1060
+ @pastel.yellow("🏃 Run: #{result[:run_command]}")
1061
+ ].join("\n")
1062
+ puts center_text(info_msg)
1063
+ puts
1064
+
1065
+ # In direct mode, just show the success message and exit
1066
+ puts center_text(@pastel.green("✅ Test generation completed successfully!"))
1067
+ return
1068
+ end
1069
+
1070
+ # Interactive mode - show menu loop
1071
+ loop do
1072
+ puts
1073
+ header = @pastel.bright_white.bold("🎉 Test Generated Successfully!")
1074
+ puts center_text(header)
1075
+ puts
1076
+
1077
+ # Show file info
1078
+ info_msg = [
1079
+ @pastel.cyan("📁 File: #{result[:file_path]}"),
1080
+ @pastel.yellow("🏃 Run: #{result[:run_command]}")
1081
+ ].join("\n")
1082
+ puts center_text(info_msg)
1083
+ puts
1084
+
1085
+ choice = @prompt.select(
1086
+ @pastel.bright_white("What would you like to do?"),
1087
+ cycle: true,
1088
+ symbols: { marker: "▶" }
1089
+ ) do |menu|
1090
+ menu.choice @pastel.yellow("📋 Copy run command"), :copy_command
1091
+ menu.choice @pastel.cyan("⬅️ Back to main menu"), :back
1092
+ end
1093
+
1094
+ case choice
1095
+ when :copy_command
1096
+ copy_to_clipboard(result[:run_command])
1097
+ when :back
1098
+ break
1099
+ end
1100
+ end
1101
+ end
1102
+
1103
+ def initialize_config_and_clients
1104
+ @config_initialized = true
1105
+
1106
+ # Check if configuration is properly set up
1107
+ unless Tng::Services::UserAppConfig.configured?
1108
+ missing = Tng::Services::UserAppConfig.missing_config
1109
+ ConfigurationDisplay.new(@pastel).display_missing_config(missing)
1110
+ exit 1
1111
+ end
1112
+
1113
+ @http_client = Tng::HttpClient.new(
1114
+ Tng::Services::UserAppConfig.base_url,
1115
+ Tng::Services::UserAppConfig.api_key
1116
+ )
1117
+ @testng = Services::Testng.new(@http_client)
1118
+ end
1119
+ end
1120
+
1121
+ cli = CLI.new
1122
+ cli.start