tng 0.1.0

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