tng 0.2.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +15 -0
  3. data/LICENSE.md +32 -0
  4. data/README.md +413 -0
  5. data/Rakefile +124 -0
  6. data/bin/load_dev +22 -0
  7. data/bin/tng +888 -0
  8. data/binaries/tng.bundle +0 -0
  9. data/binaries/tng.so +0 -0
  10. data/lib/generators/tng/install_generator.rb +236 -0
  11. data/lib/tng/analyzers/controller.rb +114 -0
  12. data/lib/tng/analyzers/model.rb +131 -0
  13. data/lib/tng/analyzers/other.rb +277 -0
  14. data/lib/tng/analyzers/service.rb +150 -0
  15. data/lib/tng/api/http_client.rb +100 -0
  16. data/lib/tng/railtie.rb +11 -0
  17. data/lib/tng/services/direct_generation.rb +320 -0
  18. data/lib/tng/services/extract_methods.rb +39 -0
  19. data/lib/tng/services/test_generator.rb +287 -0
  20. data/lib/tng/services/testng.rb +100 -0
  21. data/lib/tng/services/user_app_config.rb +76 -0
  22. data/lib/tng/ui/about_display.rb +66 -0
  23. data/lib/tng/ui/authentication_warning_display.rb +172 -0
  24. data/lib/tng/ui/configuration_display.rb +52 -0
  25. data/lib/tng/ui/controller_test_flow_display.rb +79 -0
  26. data/lib/tng/ui/display_banner.rb +44 -0
  27. data/lib/tng/ui/goodbye_display.rb +41 -0
  28. data/lib/tng/ui/model_test_flow_display.rb +80 -0
  29. data/lib/tng/ui/other_test_flow_display.rb +78 -0
  30. data/lib/tng/ui/post_install_box.rb +80 -0
  31. data/lib/tng/ui/service_test_flow_display.rb +78 -0
  32. data/lib/tng/ui/show_help.rb +78 -0
  33. data/lib/tng/ui/system_status_display.rb +128 -0
  34. data/lib/tng/ui/theme.rb +258 -0
  35. data/lib/tng/ui/user_stats_display.rb +160 -0
  36. data/lib/tng/utils.rb +325 -0
  37. data/lib/tng/version.rb +5 -0
  38. data/lib/tng.rb +308 -0
  39. data/tng.gemspec +56 -0
  40. metadata +293 -0
@@ -0,0 +1,100 @@
1
+ module Services
2
+ class Testng
3
+ STATS_PATH = "cli/tng_rails/stats"
4
+
5
+ def initialize(http_client)
6
+ @http_client = http_client
7
+ end
8
+
9
+ def check_system_status
10
+ response = @http_client.ping
11
+
12
+ if response.is_a?(HTTPX::ErrorResponse)
13
+ return {
14
+ status: :error,
15
+ message: "Unable to connect to TNG service: #{response.error&.message}",
16
+ error_type: :connection_error
17
+ }
18
+ end
19
+
20
+ case response.status
21
+ when 200
22
+ begin
23
+ data = JSON.parse(response.body)
24
+ current_version = data["current_version"]["gem_version"]
25
+ server_base_url = data["base_url"]
26
+ user_base_url = Tng::Services::UserAppConfig.base_url
27
+
28
+ if current_version > Tng::VERSION
29
+ return {
30
+ status: :version_mismatch,
31
+ message: "Version mismatch detected",
32
+ current_version: current_version,
33
+ gem_version: Tng::VERSION,
34
+ error_type: :version_mismatch
35
+ }
36
+ end
37
+
38
+ # Check for base URL mismatch
39
+ if server_base_url && user_base_url && server_base_url != user_base_url
40
+ return {
41
+ status: :base_url_mismatch,
42
+ message: "Base URL mismatch detected",
43
+ server_base_url: server_base_url,
44
+ user_base_url: user_base_url,
45
+ current_version: current_version,
46
+ gem_version: Tng::VERSION,
47
+ error_type: :base_url_mismatch
48
+ }
49
+ end
50
+
51
+ {
52
+ status: :ok,
53
+ message: data["message"] || "System is operational",
54
+ current_version: current_version,
55
+ gem_version: Tng::VERSION,
56
+ server_base_url: server_base_url
57
+ }
58
+ rescue JSON::ParserError => e
59
+ {
60
+ status: :error,
61
+ message: "Invalid response from server: #{e.message}",
62
+ error_type: :parse_error
63
+ }
64
+ end
65
+ else
66
+ {
67
+ status: :service_down,
68
+ message: "TestNG service is currently unavailable (HTTP #{response.status})",
69
+ error_type: :service_unavailable
70
+ }
71
+ end
72
+ end
73
+
74
+ def get_user_stats
75
+ headers = {
76
+ "Authorization" => "Bearer #{Tng::Services::UserAppConfig.api_key}",
77
+ "Content-Type" => "application/json",
78
+ "User-Agent" => "Tng/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
79
+ }
80
+
81
+ response = @http_client.get(STATS_PATH, headers: headers)
82
+
83
+ return if response.is_a?(HTTPX::ErrorResponse)
84
+
85
+ if response.status == 200
86
+ begin
87
+ stats_data = JSON.parse(response.body)
88
+ puts "✅ Statistics fetched successfully!"
89
+ stats_data
90
+ rescue JSON::ParserError => e
91
+ puts "❌ Failed to parse statistics response: #{e.message}"
92
+ nil
93
+ end
94
+ else
95
+ puts "❌ Failed to fetch statistics (#{response.status})"
96
+ nil
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tng
4
+ module Services
5
+ class UserAppConfig
6
+ def self.api_key
7
+ Tng.api_key
8
+ end
9
+
10
+ def self.base_url
11
+ Tng.base_url
12
+ end
13
+
14
+ def self.configured?
15
+ Tng.api_key && Tng.base_url
16
+ end
17
+
18
+ def self.missing_config
19
+ missing = []
20
+ missing << "API key" unless api_key
21
+ missing << "Base URL" unless base_url
22
+ missing
23
+ end
24
+
25
+ def self.config_with_source
26
+ auth_entry_points_with_source = []
27
+ methods = Tng.authentication_methods
28
+ methods.each do |method|
29
+ auth_entry_points_with_source << {
30
+ method: method[:method],
31
+ file: method[:file_location],
32
+ auth_type: method[:auth_type],
33
+ source: extract_method_source(method[:file_location], method[:method])
34
+ }
35
+ end
36
+ Tng.config[:authentication_entry_points_with_source] = auth_entry_points_with_source
37
+ Tng.config
38
+ end
39
+
40
+ def self.find_method_in_ast(node, method_name)
41
+ return unless node.is_a?(Prism::Node)
42
+
43
+ return node if node.is_a?(Prism::DefNode) && node.name == method_name.to_sym
44
+
45
+ node.child_nodes.each do |child|
46
+ result = find_method_in_ast(child, method_name)
47
+ return result if result
48
+ end
49
+
50
+ nil
51
+ end
52
+
53
+ def self.extract_method_source(file_path, method_name)
54
+ return unless file_path && method_name
55
+
56
+ full_path = Rails.root.join(file_path)
57
+ return unless File.exist?(full_path)
58
+
59
+ file_content = File.read(full_path)
60
+ result = Prism.parse(file_content)
61
+
62
+ method_node = find_method_in_ast(result.value, method_name)
63
+ return unless method_node
64
+
65
+ start_line = method_node.location.start_line - 1
66
+ end_line = method_node.location.end_line - 1
67
+
68
+ lines = file_content.lines
69
+ method_source = lines[start_line..end_line].join
70
+ method_source.strip
71
+ rescue StandardError
72
+ nil
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+ require "pastel"
5
+ require "tty-screen"
6
+ require_relative "theme"
7
+
8
+ class AboutDisplay
9
+ def initialize(pastel, prompt, version)
10
+ @pastel = pastel
11
+ @prompt = prompt
12
+ @version = version
13
+ @terminal_width = begin
14
+ TTY::Screen.width
15
+ rescue StandardError
16
+ 80
17
+ end
18
+ end
19
+
20
+ def display
21
+ about_content = [
22
+ "",
23
+ @pastel.public_send(Tng::UI::Theme.color(:secondary),
24
+ "Tng is an LLM-powered test generation tool for Rails applications."),
25
+ "",
26
+ @pastel.public_send(Tng::UI::Theme.color(:accent), "Features:"),
27
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
28
+ "#{Tng::UI::Theme.icon(:bullet)} 20+ Rails file types supported"),
29
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
30
+ "#{Tng::UI::Theme.icon(:bullet)} Core: Controllers, Models, Services"),
31
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
32
+ "#{Tng::UI::Theme.icon(:bullet)} Extended: Jobs, Helpers, Lib, Policies, Presenters, Mailers, GraphQL + more"),
33
+ @pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} LLM-powered test suggestions"),
34
+ @pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Test coverage analysis"),
35
+ "",
36
+ @pastel.public_send(Tng::UI::Theme.color(:accent), "Technology:"),
37
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
38
+ "#{Tng::UI::Theme.icon(:bullet)} Static code analysis with Prism"),
39
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
40
+ "#{Tng::UI::Theme.icon(:bullet)} Intelligent pattern recognition"),
41
+ @pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Rails-specific optimizations"),
42
+ "",
43
+ @pastel.public_send(Tng::UI::Theme.color(:muted), "Version: #{@version}"),
44
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
45
+ "Built with #{Tng::UI::Theme.icon(:heart)} for Rails developers")
46
+ ].join("\n")
47
+
48
+ box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
49
+ style = Tng::UI::Theme.box_style(:default)
50
+
51
+ about_box = TTY::Box.frame(
52
+ title: { top_left: " About Tng " },
53
+ style: style[:style],
54
+ padding: style[:padding],
55
+ width: box_width
56
+ ) do
57
+ about_content
58
+ end
59
+
60
+ puts Tng::UI::Theme.center_box(about_box, box_width, @terminal_width)
61
+ @prompt.keypress(Tng::UI::Theme.center_text(
62
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
63
+ "Press any key to continue..."), @terminal_width
64
+ ))
65
+ end
66
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+ require "pastel"
5
+ require "tty-screen"
6
+ require_relative "theme"
7
+
8
+ class AuthenticationWarningDisplay
9
+ def initialize(pastel)
10
+ @pastel = pastel
11
+ @terminal_width = begin
12
+ TTY::Screen.width
13
+ rescue StandardError
14
+ 80
15
+ end
16
+ end
17
+
18
+ def display_if_missing
19
+ return false unless authentication_config_missing?
20
+
21
+ display_warning
22
+ handle_user_choice
23
+ end
24
+
25
+ def display_warning
26
+ warning_content = [
27
+ @pastel.public_send(Tng::UI::Theme.color(:error)).bold("Authentication Configuration Missing"),
28
+ "",
29
+ @pastel.public_send(Tng::UI::Theme.color(:primary), "TNG needs authentication setup to generate proper tests."),
30
+ "",
31
+ @pastel.public_send(Tng::UI::Theme.color(:accent), "Missing: ") + build_missing_items_summary,
32
+ "",
33
+ @pastel.public_send(Tng::UI::Theme.color(:secondary), "Quick fix:"),
34
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
35
+ "#{Tng::UI::Theme.icon(:bullet)} Edit config/initializers/tng.rb"),
36
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
37
+ "#{Tng::UI::Theme.icon(:bullet)} Uncomment authentication_methods array"),
38
+ @pastel.public_send(Tng::UI::Theme.color(:muted), "#{Tng::UI::Theme.icon(:bullet)} Add your auth method details"),
39
+ "",
40
+ @pastel.public_send(Tng::UI::Theme.color(:success), "Press Enter to continue anyway"),
41
+ @pastel.public_send(Tng::UI::Theme.color(:error), "Press Esc to cancel and fix configuration")
42
+ ].join("\n")
43
+
44
+ box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
45
+ style = Tng::UI::Theme.box_style(:warning)
46
+
47
+ warning_box = TTY::Box.frame(
48
+ title: { top_left: " Auth Warning " },
49
+ style: style[:style],
50
+ padding: style[:padding],
51
+ width: box_width
52
+ ) do
53
+ warning_content
54
+ end
55
+
56
+ puts Tng::UI::Theme.center_box(warning_box, box_width, @terminal_width)
57
+ puts
58
+ end
59
+
60
+ private
61
+
62
+ def handle_user_choice
63
+ require "io/console"
64
+
65
+ loop do
66
+ key = $stdin.getch
67
+ case key
68
+ when "\r", "\n" # Enter key
69
+ return false # Continue anyway
70
+ when "\e" # Escape key
71
+ return true # Cancel (authentication missing)
72
+ when "\u0003" # Ctrl+C
73
+ puts "\n#{@pastel.public_send(Tng::UI::Theme.color(:warning), "Operation cancelled.")}"
74
+ exit(0)
75
+ end
76
+ end
77
+ rescue StandardError
78
+ # Fallback if getch is not available
79
+ puts @pastel.public_send(Tng::UI::Theme.color(:muted), "Press Enter to continue or Ctrl+C to cancel...")
80
+ gets
81
+ false
82
+ end
83
+
84
+ def build_missing_items_summary
85
+ issues = []
86
+
87
+ issues << "empty auth methods" if Tng.authentication_enabled && authentication_methods_empty?
88
+
89
+ issues << "invalid auth config" if !authentication_methods_empty? && !authentication_methods_valid?
90
+
91
+ issues << "auth disabled" unless Tng.authentication_enabled
92
+
93
+ issues.empty? ? "unknown issue" : issues.join(", ")
94
+ end
95
+
96
+ def authentication_config_missing?
97
+ # Check if authentication is enabled but methods are not configured
98
+ return true if Tng.authentication_enabled && authentication_methods_empty?
99
+
100
+ # Check if authentication methods exist but are invalid
101
+ return true if !authentication_methods_empty? && !authentication_methods_valid?
102
+
103
+ false
104
+ end
105
+
106
+ def authentication_methods_empty?
107
+ methods = Tng.authentication_methods
108
+ methods.nil? || methods.empty?
109
+ end
110
+
111
+ def authentication_methods_valid?
112
+ methods = Tng.authentication_methods
113
+ return false if methods.nil? || methods.empty?
114
+
115
+ methods.all? do |method|
116
+ method.is_a?(Hash) &&
117
+ method.key?(:method) && !method[:method].to_s.strip.empty? &&
118
+ method.key?(:file_location) && !method[:file_location].to_s.strip.empty? &&
119
+ method.key?(:auth_type) && !method[:auth_type].to_s.strip.empty?
120
+ end
121
+ end
122
+
123
+ def build_missing_items_list
124
+ issues = []
125
+
126
+ if Tng.authentication_enabled && authentication_methods_empty?
127
+ issues << @pastel.public_send(Tng::UI::Theme.color(:error), " • authentication_methods array is empty")
128
+ end
129
+
130
+ if !authentication_methods_empty? && !authentication_methods_valid?
131
+ issues << @pastel.public_send(Tng::UI::Theme.color(:error), " • authentication_methods contains invalid entries")
132
+
133
+ Tng.authentication_methods.each_with_index do |method, index|
134
+ next if method.is_a?(Hash)
135
+
136
+ issues << @pastel.public_send(Tng::UI::Theme.color(:muted), " - Entry #{index + 1}: not a valid hash")
137
+ next
138
+ end
139
+
140
+ Tng.authentication_methods.each_with_index do |method, index|
141
+ next unless method.is_a?(Hash)
142
+
143
+ if !method.key?(:method) || method[:method].to_s.strip.empty?
144
+ issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
145
+ " - Entry #{index + 1}: missing or empty 'method'")
146
+ end
147
+
148
+ if !method.key?(:file_location) || method[:file_location].to_s.strip.empty?
149
+ issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
150
+ " - Entry #{index + 1}: missing or empty 'file_location'")
151
+ end
152
+
153
+ if !method.key?(:auth_type) || method[:auth_type].to_s.strip.empty?
154
+ issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
155
+ " - Entry #{index + 1}: missing or empty 'auth_type'")
156
+ end
157
+ end
158
+ end
159
+
160
+ unless Tng.authentication_enabled
161
+ issues << @pastel.public_send(Tng::UI::Theme.color(:warning),
162
+ " • authentication_enabled is set to false")
163
+ end
164
+
165
+ if issues.empty?
166
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
167
+ " • Configuration appears valid")
168
+ else
169
+ issues.join("\n")
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+ require "pastel"
5
+ require "tty-screen"
6
+ require_relative "theme"
7
+
8
+ class ConfigurationDisplay
9
+ def initialize(pastel)
10
+ @pastel = pastel
11
+ @terminal_width = begin
12
+ TTY::Screen.width
13
+ rescue StandardError
14
+ 80
15
+ end
16
+ end
17
+
18
+ def display_missing_config(missing_config)
19
+ config_content = [
20
+ @pastel.public_send(Tng::UI::Theme.color(:error)).bold("Configuration Required"),
21
+ "",
22
+ @pastel.public_send(Tng::UI::Theme.color(:accent), "Missing required configuration:"),
23
+ missing_config.map do |key|
24
+ @pastel.public_send(Tng::UI::Theme.color(:secondary), "#{Tng::UI::Theme.icon(:bullet)} #{key}")
25
+ end.join("\n"),
26
+ "",
27
+ @pastel.public_send(Tng::UI::Theme.color(:secondary), "Quick fix:"),
28
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
29
+ "#{Tng::UI::Theme.icon(:bullet)} Edit config/initializers/tng.rb"),
30
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
31
+ "#{Tng::UI::Theme.icon(:bullet)} Set your API key and other required values"),
32
+ "",
33
+ @pastel.public_send(Tng::UI::Theme.color(:success), "Press Enter to continue after fixing"),
34
+ @pastel.public_send(Tng::UI::Theme.color(:error), "Press Ctrl+C to exit")
35
+ ].join("\n")
36
+
37
+ box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
38
+ style = Tng::UI::Theme.box_style(:error)
39
+
40
+ config_box = TTY::Box.frame(
41
+ title: { top_left: " Config Required " },
42
+ style: style[:style],
43
+ padding: style[:padding],
44
+ width: box_width
45
+ ) do
46
+ config_content
47
+ end
48
+
49
+ puts Tng::UI::Theme.center_box(config_box, box_width, @terminal_width)
50
+ puts
51
+ end
52
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "tty-spinner"
5
+ require "pastel"
6
+ require "tty-screen"
7
+ require_relative "theme"
8
+
9
+ class ControllerTestFlowDisplay
10
+ def initialize(prompt, pastel)
11
+ @prompt = prompt
12
+ @pastel = pastel
13
+ @terminal_width = begin
14
+ TTY::Screen.width
15
+ rescue StandardError
16
+ 80
17
+ end
18
+ end
19
+
20
+ def select_controller(controllers)
21
+ header = @pastel.public_send(Tng::UI::Theme.color(:primary)).bold("#{Tng::UI::Theme.icon(:config)} Select controller to test:")
22
+ puts Tng::UI::Theme.center_text(header, @terminal_width)
23
+
24
+ @prompt.select(
25
+ "",
26
+ cycle: true,
27
+ per_page: 12,
28
+ filter: true,
29
+ symbols: { marker: Tng::UI::Theme.icon(:marker) }
30
+ ) do |menu|
31
+ controllers.each do |controller|
32
+ display_name = "#{controller[:name]} #{@pastel.public_send(Tng::UI::Theme.color(:muted),
33
+ "(#{controller[:path]})")}"
34
+ menu.choice display_name, controller
35
+ end
36
+ menu.choice @pastel.public_send(Tng::UI::Theme.color(:secondary), "#{Tng::UI::Theme.icon(:back)} Back"), :back
37
+ end
38
+ end
39
+
40
+ def show_no_controllers_message
41
+ error_msg = "#{@pastel.public_send(Tng::UI::Theme.color(:error)).bold("#{Tng::UI::Theme.icon(:error)} No controllers found in your application")}\n#{@pastel.public_send(
42
+ Tng::UI::Theme.color(:muted), "Make sure you have controllers in app/controllers/"
43
+ )}"
44
+ puts Tng::UI::Theme.center_text(error_msg, @terminal_width)
45
+ @prompt.keypress(Tng::UI::Theme.center_text(
46
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
47
+ "Press any key to continue..."), @terminal_width
48
+ ))
49
+ end
50
+
51
+ def select_controller_method(controller, methods)
52
+ header = @pastel.public_send(Tng::UI::Theme.color(:primary)).bold("#{Tng::UI::Theme.icon(:rocket)} Select method to test in #{controller[:name]}")
53
+ puts Tng::UI::Theme.center_text(header, @terminal_width)
54
+
55
+ @prompt.select(
56
+ "",
57
+ cycle: true,
58
+ per_page: 10,
59
+ filter: true,
60
+ symbols: { marker: Tng::UI::Theme.icon(:marker) }
61
+ ) do |menu|
62
+ methods.each do |method|
63
+ menu.choice method[:name], method
64
+ end
65
+ menu.choice @pastel.public_send(Tng::UI::Theme.color(:secondary), "#{Tng::UI::Theme.icon(:back)} Back"), :back
66
+ end
67
+ end
68
+
69
+ def show_no_methods_message(controller)
70
+ error_msg = "#{@pastel.public_send(Tng::UI::Theme.color(:error)).bold("#{Tng::UI::Theme.icon(:error)} No methods found in #{controller[:name]}")}\n#{@pastel.public_send(
71
+ Tng::UI::Theme.color(:muted), "Controller may be empty or have syntax errors"
72
+ )}"
73
+ puts Tng::UI::Theme.center_text(error_msg, @terminal_width)
74
+ @prompt.keypress(Tng::UI::Theme.center_text(
75
+ @pastel.public_send(Tng::UI::Theme.color(:muted),
76
+ "Press any key to continue..."), @terminal_width
77
+ ))
78
+ end
79
+ end
@@ -0,0 +1,44 @@
1
+ require "tty-box"
2
+ require "pastel"
3
+ require "tty-screen"
4
+ require_relative "theme"
5
+
6
+ class DisplayBanner
7
+ attr_reader :pastel, :version, :terminal_width
8
+
9
+ def initialize(pastel, version)
10
+ @pastel = pastel
11
+ @version = version
12
+ @terminal_width = begin
13
+ TTY::Screen.width
14
+ rescue StandardError
15
+ 80
16
+ end
17
+ end
18
+
19
+ def render
20
+ banner_content = [
21
+ pastel.public_send(Tng::UI::Theme.color(:secondary)).bold("Tng - LLM-Powered Rails Test Generator"),
22
+ pastel.public_send(Tng::UI::Theme.color(:muted), "Version: #{version}"),
23
+ "",
24
+ pastel.public_send(Tng::UI::Theme.color(:accent), "Generate comprehensive tests for your Rails application"),
25
+ pastel.public_send(Tng::UI::Theme.color(:muted), "Powered by LLM and static analysis")
26
+ ].join("\n")
27
+
28
+ box_width = 64
29
+ box_width = terminal_width if box_width > terminal_width
30
+ style = Tng::UI::Theme.box_style(:default)
31
+
32
+ box = TTY::Box.frame(
33
+ title: { top_left: " Tng ", bottom_right: " Ready " },
34
+ style: style[:style],
35
+ padding: style[:padding],
36
+ align: :center,
37
+ width: box_width
38
+ ) do
39
+ banner_content
40
+ end
41
+
42
+ "#{Tng::UI::Theme.center_box(box, box_width, terminal_width)}\n"
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-box"
4
+ require "pastel"
5
+ require "tty-screen"
6
+ require_relative "theme"
7
+
8
+ class GoodbyeDisplay
9
+ def initialize(pastel)
10
+ @pastel = pastel
11
+ @terminal_width = begin
12
+ TTY::Screen.width
13
+ rescue StandardError
14
+ 80
15
+ end
16
+ end
17
+
18
+ def display
19
+ box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
20
+ style = Tng::UI::Theme.box_style(:success)
21
+
22
+ goodbye_box = TTY::Box.frame(
23
+ title: { top_left: " Thank You " },
24
+ style: style[:style],
25
+ padding: style[:padding],
26
+ align: :center,
27
+ width: box_width
28
+ ) do
29
+ [
30
+ @pastel.public_send(Tng::UI::Theme.color(:success)).bold("Thanks for using Tng!"),
31
+ "",
32
+ @pastel.public_send(Tng::UI::Theme.color(:secondary), "Happy testing!"),
33
+ "",
34
+ @pastel.public_send(Tng::UI::Theme.color(:muted), "Your Rails tests are now supercharged with LLM")
35
+ ].join("\n")
36
+ end
37
+
38
+ puts Tng::UI::Theme.center_box(goodbye_box, box_width, @terminal_width)
39
+ puts
40
+ end
41
+ end