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
data/lib/tng/utils.rb ADDED
@@ -0,0 +1,325 @@
1
+ require "yaml"
2
+ require "json"
3
+ require "fileutils"
4
+ require "prism"
5
+
6
+ module Tng
7
+ module Utils
8
+ def clear_screen
9
+ system("clear") || system("cls")
10
+ end
11
+
12
+ def center_text(text, width = nil)
13
+ width ||= @terminal_width || 80 # Use fallback if still nil
14
+ lines = text.split("\n")
15
+ lines.map do |line|
16
+ # Remove ANSI color codes for length calculation
17
+ clean_line = line.gsub(/\e\[[0-9;]*m/, "")
18
+ padding = [(width - clean_line.length) / 2, 0].max
19
+ " " * padding + line
20
+ end.join("\n")
21
+ end
22
+
23
+ def copy_to_clipboard(text)
24
+ # Try to copy to clipboard
25
+ if system("which pbcopy > /dev/null 2>&1")
26
+ system("echo '#{text}' | pbcopy")
27
+ success_msg = @pastel.green("✅ Command copied to clipboard!")
28
+ elsif system("which xclip > /dev/null 2>&1")
29
+ system("echo '#{text}' | xclip -selection clipboard")
30
+ success_msg = @pastel.green("✅ Command copied to clipboard!")
31
+ else
32
+ success_msg = @pastel.yellow("📋 Copy this command:")
33
+ puts center_text(success_msg)
34
+ puts center_text(@pastel.bright_white(text))
35
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
36
+ return
37
+ end
38
+
39
+ puts center_text(success_msg)
40
+ @prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
41
+ end
42
+
43
+ def load_rails_environment
44
+ # Use bundler environment to avoid gem conflicts
45
+ require "bundler/setup"
46
+ require "./config/environment"
47
+ true
48
+ rescue LoadError => e
49
+ puts "Failed to load Rails: #{e.message}"
50
+ false
51
+ end
52
+
53
+ def find_rails_root
54
+ current_dir = Dir.pwd
55
+ while current_dir != "/"
56
+ return current_dir if File.exist?(File.join(current_dir, "config", "application.rb"))
57
+
58
+ current_dir = File.dirname(current_dir)
59
+ end
60
+ nil
61
+ end
62
+
63
+ def camelize(str, first_letter = :upper)
64
+ # Use Rails' camelize method if available, otherwise use custom implementation
65
+ if defined?(ActiveSupport::Inflector) && ActiveSupport::Inflector.respond_to?(:camelize)
66
+ case first_letter
67
+ when :upper
68
+ ActiveSupport::Inflector.camelize(str, true)
69
+ when :lower
70
+ ActiveSupport::Inflector.camelize(str, false)
71
+ else
72
+ raise ArgumentError, "Invalid option, use either :upper or :lower."
73
+ end
74
+ elsif str.respond_to?(:camelize)
75
+ str.camelize(first_letter)
76
+ else
77
+ # Custom implementation
78
+ result = str.gsub(/(?:^|_)([a-z])/) { Regexp.last_match(1).upcase }
79
+ case first_letter
80
+ when :upper
81
+ result
82
+ when :lower
83
+ result[0].downcase + result[1..] if result.length.positive?
84
+ else
85
+ raise ArgumentError, "Invalid option, use either :upper or :lower."
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.has_gem?(gem_name)
91
+ return false unless defined?(Bundler)
92
+
93
+ gemfile_specs = Bundler.load.specs
94
+ gemfile_specs.any? { |spec| spec.name == gem_name }
95
+ rescue StandardError
96
+ false
97
+ end
98
+
99
+ def self.save_test_file(test_content)
100
+ puts "📋 Raw API response: #{test_content[0..200]}..." if ENV["DEBUG"]
101
+ parsed_response = JSON.parse(test_content)
102
+
103
+ if parsed_response["error"]
104
+ puts "❌ API responded with an error: #{parsed_response["error"]}"
105
+ return
106
+ end
107
+ # Validate required fields
108
+ unless parsed_response["file_content"]
109
+ puts "❌ API response missing file_content field"
110
+ puts "📋 Response keys: #{parsed_response.keys.inspect}"
111
+ return
112
+ end
113
+
114
+ # Handle both possible field names for file path
115
+ file_path = parsed_response["test_file_path"] || parsed_response["file_path"] || parsed_response["file_name"] || parsed_response["file"]
116
+ unless file_path
117
+ puts "❌ API response missing test_file_path or file_path field"
118
+ puts "📋 Response keys: #{parsed_response.keys.inspect}"
119
+ return
120
+ end
121
+
122
+ begin
123
+ File.write(file_path, parsed_response["file_content"])
124
+ rescue Errno::ENOENT
125
+ # Create directory if it doesn't exist
126
+ FileUtils.mkdir_p(File.dirname(file_path))
127
+ File.write(file_path, parsed_response["file_content"])
128
+ end
129
+ puts "✅ Test generated successfully!"
130
+ absolute_path = File.expand_path(file_path)
131
+ puts "Please review the generated tests at \e]8;;file://#{absolute_path}\e\\#{file_path}\e]8;;\e\\"
132
+
133
+ # Count tests in the generated file
134
+ test_count = count_tests_in_file(file_path)
135
+
136
+ # Determine run command based on test framework
137
+ run_command = if file_path.include?("/spec/")
138
+ "bundle exec rspec #{file_path}"
139
+ else
140
+ "bundle exec rails test #{file_path}"
141
+ end
142
+
143
+ # Return file information for CLI to use
144
+ {
145
+ file_path: file_path,
146
+ absolute_path: absolute_path,
147
+ run_command: run_command,
148
+ test_class_name: parsed_response["test_class_name"],
149
+ test_count: test_count
150
+ }
151
+ end
152
+
153
+ def self.fixture_content
154
+ factory_library = Tng.factory_library || "active_record"
155
+
156
+ case factory_library.downcase
157
+ when "fixtures"
158
+ load_all_fixtures_data
159
+ when "fabrication", "fabricator"
160
+ load_all_fabricator_data
161
+ when "factory_girl", "factory_bot"
162
+ load_all_factory_data
163
+ when "active_record"
164
+ load_all_active_record_data
165
+ else
166
+ puts "⚠️ Warning: Unknown factory library '#{factory_library}'. Using Active Record object creation as fallback."
167
+ load_all_fixtures_data
168
+ end
169
+ end
170
+
171
+ def self.load_all_fixtures_data
172
+ fixture_data = {}
173
+ # TODO: Load proper folder for Rspec. This is only valid for minitest.
174
+ fixtures_dir = Rails.root.join("test", "fixtures")
175
+
176
+ return fixture_data unless Dir.exist?(fixtures_dir)
177
+
178
+ fixture_files = Dir.glob("#{fixtures_dir}/*.yml")
179
+
180
+ fixture_files.each do |fixture_file|
181
+ model_name = File.basename(fixture_file, ".yml")
182
+
183
+ begin
184
+ fixtures = YAML.load_file(fixture_file)
185
+ fixture_data[model_name] = fixtures
186
+ rescue StandardError => e
187
+ puts "⚠️ Warning: Could not load fixture file #{fixture_file}: #{e.message}"
188
+ end
189
+ end
190
+
191
+ fixture_data
192
+ end
193
+
194
+ def self.load_all_fabricator_data
195
+ fabricator_data = {}
196
+ fabricators_dir = Rails.root.join("spec", "fabricators")
197
+
198
+ return fabricator_data unless Dir.exist?(fabricators_dir)
199
+
200
+ Dir.glob("#{fabricators_dir}/*_fabricator.rb").each do |fabricator_file|
201
+ model_name = File.basename(fabricator_file, "_fabricator.rb")
202
+
203
+ begin
204
+ content = File.read(fabricator_file)
205
+ fabricator_data[model_name] = parse_fabricator_structure(content, model_name)
206
+ rescue StandardError => e
207
+ puts "⚠️ Warning: Could not load fabricator file #{fabricator_file}: #{e.message}"
208
+ end
209
+ end
210
+
211
+ fabricator_data
212
+ end
213
+
214
+ def self.load_all_factory_data
215
+ factory_data = {}
216
+ factory_dirs = [
217
+ Rails.root.join("spec", "factories"),
218
+ Rails.root.join("test", "factories")
219
+ ]
220
+
221
+ factory_dirs.each do |factory_dir|
222
+ next unless Dir.exist?(factory_dir)
223
+
224
+ Dir.glob("#{factory_dir}/*.rb").each do |factory_file|
225
+ content = File.read(factory_file)
226
+
227
+ begin
228
+ # Extract all factory definitions from the file
229
+ content.scan(/factory\s+:(\w+)\s+do/) do |match|
230
+ model_name = match[0]
231
+ factory_data[model_name] = parse_factory_structure(content, model_name)
232
+ end
233
+ rescue StandardError => e
234
+ puts "⚠️ Warning: Could not load factory file #{factory_file}: #{e.message}"
235
+ end
236
+ end
237
+ end
238
+
239
+ factory_data
240
+ end
241
+
242
+ def self.parse_fabricator_structure(content, model_name)
243
+ # Parse fabricator file to extract attribute structure
244
+ attributes = {}
245
+
246
+ # Look for Fabricator definitions
247
+ content.scan(/Fabricator\(:#{model_name}\) do \|f\|(.*?)end/m) do |block|
248
+ block[0].scan(/f\.(\w+)\s+(.+)/) do |attr, value|
249
+ attributes[attr] = parse_attribute_value(value)
250
+ end
251
+ end
252
+
253
+ { "attributes" => attributes, "type" => "fabricator" }
254
+ end
255
+
256
+ def self.parse_factory_structure(content, model_name)
257
+ # Parse factory file to extract attribute structure
258
+ attributes = {}
259
+
260
+ # Look for factory definitions
261
+ content.scan(/factory :#{model_name} do(.*?)end/m) do |block|
262
+ block[0].scan(/(\w+)\s+(.+)/) do |attr, value|
263
+ attributes[attr] = parse_attribute_value(value)
264
+ end
265
+ end
266
+
267
+ { "attributes" => attributes, "type" => "factory" }
268
+ end
269
+
270
+ def self.parse_attribute_value(value)
271
+ # Clean up and parse attribute values
272
+ value.strip.gsub(/^["']|["']$/, "")
273
+ end
274
+
275
+ def self.load_all_active_record_data
276
+ []
277
+ end
278
+
279
+ def self.count_tests_in_file(file_path)
280
+ return 0 unless File.exist?(file_path)
281
+
282
+ begin
283
+ content = File.read(file_path)
284
+ result = Prism.parse(content)
285
+ return 0 unless result.success?
286
+
287
+ count_test_nodes(result.value)
288
+ rescue StandardError => e
289
+ puts "⚠️ Warning: Could not parse #{file_path}: #{e.message}" if ENV["DEBUG"]
290
+ 0
291
+ end
292
+ end
293
+
294
+ private_class_method def self.count_test_nodes(node)
295
+ return 0 unless node.respond_to?(:child_nodes)
296
+
297
+ count = 0
298
+
299
+ # Check if current node is a test
300
+ count += 1 if test_node?(node)
301
+
302
+ # Recursively check child nodes
303
+ node.child_nodes.each do |child|
304
+ count += count_test_nodes(child) if child
305
+ end
306
+
307
+ count
308
+ end
309
+
310
+ private_class_method def self.test_node?(node)
311
+ case node
312
+ when Prism::DefNode
313
+ # Minitest: def test_something
314
+ node.name.to_s.start_with?("test_")
315
+ when Prism::CallNode
316
+ # RSpec: it "...", specify "..."
317
+ # Minitest: test "..."
318
+ method_name = node.name.to_s
319
+ %w[it specify test].include?(method_name)
320
+ else
321
+ false
322
+ end
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tng
4
+ VERSION = "0.2.1"
5
+ end
data/lib/tng.rb ADDED
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ $VERBOSE = nil
4
+
5
+ require_relative "tng/version"
6
+ require_relative "tng/utils"
7
+ require_relative "tng/api/http_client"
8
+ require_relative "tng/ui/theme"
9
+ require_relative "tng/ui/post_install_box"
10
+ require_relative "tng/ui/authentication_warning_display"
11
+ require_relative "tng/services/test_generator"
12
+
13
+ require_relative "tng/railtie" if defined?(Rails)
14
+
15
+ begin
16
+ platform = RUBY_PLATFORM
17
+ binary_name = if platform.include?("darwin") # macOS
18
+ "tng.bundle"
19
+ elsif platform.include?("linux")
20
+ "tng.so"
21
+ else
22
+ raise "Unsupported platform: #{platform}"
23
+ end
24
+
25
+ binary_paths = [
26
+ File.expand_path("../binaries/#{binary_name}", __dir__), # From gem root
27
+ File.expand_path("../../binaries/#{binary_name}", __FILE__), # Alternative path
28
+ File.expand_path("binaries/#{binary_name}", __dir__) # Direct from lib
29
+ ]
30
+
31
+ loaded = false
32
+ binary_paths.each do |path|
33
+ next unless File.exist?(path)
34
+
35
+ require path
36
+ loaded = true
37
+ break
38
+ end
39
+
40
+ raise LoadError, "Could not find binary at any of: #{binary_paths.join(", ")}" unless loaded
41
+ rescue LoadError => e
42
+ puts "Warning: Could not load native extension: #{e.message}"
43
+ puts "Some functionality may not be available."
44
+ end
45
+
46
+ # Load Ruby analyzers after native binary to ensure Rust classes are available
47
+ # Ruby analyzers depend on Rust classes (e.g., Tng::Analyzer::Controller)
48
+ require_relative "tng/analyzers/controller"
49
+ require_relative "tng/analyzers/model"
50
+ require_relative "tng/analyzers/service"
51
+
52
+ module Tng
53
+ class Error < StandardError; end
54
+
55
+ @config = {
56
+ api_key: nil,
57
+ base_url: "https://app.tng.sh/",
58
+ read_source_code: true,
59
+ read_test_code: true,
60
+ testing_framework: "minitest",
61
+ authentication_enabled: false,
62
+ authorization_library: nil,
63
+ authentication_library: nil,
64
+ authentication_methods: [],
65
+ mock_library: "minitest/mock",
66
+ http_mock_library: "webmock",
67
+ factory_library: "active_record"
68
+ }
69
+
70
+ def self.configure
71
+ yield(self) if block_given?
72
+ end
73
+
74
+ def self.config
75
+ @config
76
+ end
77
+
78
+ def self.api_key=(value)
79
+ @config[:api_key] = value
80
+ end
81
+
82
+ def self.api_key
83
+ @config[:api_key]
84
+ end
85
+
86
+ def self.base_url=(value)
87
+ @config[:base_url] = value
88
+ end
89
+
90
+ def self.base_url
91
+ @config[:base_url]
92
+ end
93
+
94
+ def self.read_source_code=(value)
95
+ @config[:read_source_code] = value
96
+ end
97
+
98
+ def self.read_source_code
99
+ @config[:read_source_code]
100
+ end
101
+
102
+ def self.read_test_code=(value)
103
+ @config[:read_test_code] = value
104
+ end
105
+
106
+ def self.read_test_code
107
+ @config[:read_test_code]
108
+ end
109
+
110
+ def self.testing_framework=(value)
111
+ @config[:testing_framework] = value
112
+ initialize_framework_defaults(value)
113
+ end
114
+
115
+ def self.testing_framework
116
+ @config[:testing_framework]
117
+ end
118
+
119
+ def self.authentication_enabled=(value)
120
+ @config[:authentication_enabled] = value
121
+ end
122
+
123
+ def self.authentication_enabled
124
+ @config[:authentication_enabled]
125
+ end
126
+
127
+ def self.authentication_library=(value)
128
+ @config[:authentication_library] = value
129
+ end
130
+
131
+ def self.authentication_library
132
+ @config[:authentication_library]
133
+ end
134
+
135
+ def self.authorization_library=(value)
136
+ @config[:authorization_library] = value
137
+ end
138
+
139
+ def self.authorization_library
140
+ @config[:authorization_library]
141
+ end
142
+
143
+ def self.authentication_methods=(value)
144
+ @config[:authentication_methods] = value
145
+ end
146
+
147
+ def self.authentication_methods
148
+ @config[:authentication_methods]
149
+ end
150
+
151
+ def self.mock_library=(value)
152
+ @config[:mock_library] = value
153
+ end
154
+
155
+ def self.mock_library
156
+ @config[:mock_library]
157
+ end
158
+
159
+ def self.http_mock_library=(value)
160
+ @config[:http_mock_library] = value
161
+ end
162
+
163
+ def self.http_mock_library
164
+ @config[:http_mock_library]
165
+ end
166
+
167
+ def self.factory_library=(value)
168
+ @config[:factory_library] = value
169
+ end
170
+
171
+ def self.factory_library
172
+ @config[:factory_library]
173
+ end
174
+
175
+ def self.test_style=(value)
176
+ @config[:test_style] = value
177
+ end
178
+
179
+ def self.test_style
180
+ @config[:test_style]
181
+ end
182
+
183
+ def self.setup_style=(value)
184
+ @config[:setup_style] = value
185
+ end
186
+
187
+ def self.setup_style
188
+ @config[:setup_style]
189
+ end
190
+
191
+ def self.assertion_style=(value)
192
+ @config[:assertion_style] = value
193
+ end
194
+
195
+ def self.assertion_style
196
+ @config[:assertion_style]
197
+ end
198
+
199
+ def self.teardown_style=(value)
200
+ @config[:teardown_style] = value
201
+ end
202
+
203
+ def self.teardown_style
204
+ @config[:teardown_style]
205
+ end
206
+
207
+ def self.describe_style=(value)
208
+ @config[:describe_style] = value
209
+ end
210
+
211
+ def self.describe_style
212
+ @config[:describe_style]
213
+ end
214
+
215
+ def self.context_style=(value)
216
+ @config[:context_style] = value
217
+ end
218
+
219
+ def self.context_style
220
+ @config[:context_style]
221
+ end
222
+
223
+ def self.it_style=(value)
224
+ @config[:it_style] = value
225
+ end
226
+
227
+ def self.it_style
228
+ @config[:it_style]
229
+ end
230
+
231
+ def self.before_style=(value)
232
+ @config[:before_style] = value
233
+ end
234
+
235
+ def self.before_style
236
+ @config[:before_style]
237
+ end
238
+
239
+ def self.after_style=(value)
240
+ @config[:after_style] = value
241
+ end
242
+
243
+ def self.after_style
244
+ @config[:after_style]
245
+ end
246
+
247
+ def self.let_style=(value)
248
+ @config[:let_style] = value
249
+ end
250
+
251
+ def self.let_style
252
+ @config[:let_style]
253
+ end
254
+
255
+ def self.subject_style=(value)
256
+ @config[:subject_style] = value
257
+ end
258
+
259
+ def self.subject_style
260
+ @config[:subject_style]
261
+ end
262
+
263
+ def self.authentication_configured?
264
+ return false unless authentication_enabled
265
+ return false if authentication_methods.nil? || authentication_methods.empty?
266
+
267
+ authentication_methods.all? do |method|
268
+ method.is_a?(Hash) &&
269
+ method.key?(:method) && !method[:method].to_s.strip.empty? &&
270
+ method.key?(:file_location) && !method[:file_location].to_s.strip.empty? &&
271
+ method.key?(:auth_type) && !method[:auth_type].to_s.strip.empty?
272
+ end
273
+ end
274
+
275
+ def self.initialize_framework_defaults(framework)
276
+ if framework == "minitest"
277
+ @config.merge!({
278
+ test_style: "spec",
279
+ setup_style: false,
280
+ assertion_style: "assert",
281
+ teardown_style: false
282
+ })
283
+ @config.delete(:describe_style)
284
+ @config.delete(:context_style)
285
+ @config.delete(:it_style)
286
+ @config.delete(:before_style)
287
+ @config.delete(:after_style)
288
+ @config.delete(:let_style)
289
+ @config.delete(:subject_style)
290
+ elsif framework == "rspec"
291
+ @config.merge!({
292
+ describe_style: true,
293
+ context_style: "context",
294
+ it_style: "it",
295
+ before_style: "before",
296
+ after_style: "after",
297
+ let_style: true,
298
+ subject_style: true
299
+ })
300
+ @config.delete(:test_style)
301
+ @config.delete(:setup_style)
302
+ @config.delete(:assertion_style)
303
+ @config.delete(:teardown_style)
304
+ end
305
+ end
306
+
307
+ initialize_framework_defaults(@config[:testing_framework])
308
+ end
data/tng.gemspec ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/tng/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "tng"
7
+ spec.version = Tng::VERSION
8
+ spec.authors = ["ralucab"]
9
+ spec.email = ["claudiu.garba@gmail.com"]
10
+
11
+ spec.summary = "TNG generates tests using static code analysis and LLM"
12
+ spec.description = "TNG (Test Next Generation) is a Rails gem that automatically generates comprehensive test files by analyzing your Ruby code using static analysis and AI. It supports models, controllers, and services with intelligent test case generation."
13
+ spec.homepage = "https://tng.sh/"
14
+ spec.required_ruby_version = ">= 3.1.0"
15
+ spec.required_rubygems_version = ">= 3.3.11"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/tng-sh/tng-rails"
20
+ spec.license = "Nonstandard"
21
+ spec.metadata["license_uri"] = "https://github.com/tng-sh/tng-rails/blob/master/LICENSE.md"
22
+
23
+ # Package pre-compiled binaries and exclude the Rust source code.
24
+ spec.files = Dir.glob("lib/**/*.rb") +
25
+ Dir.glob("binaries/*") +
26
+ Dir.glob("bin/**/*") +
27
+ ["LICENSE.md", "README.md", "Gemfile", "Rakefile", "tng.gemspec"]
28
+
29
+ spec.bindir = "bin"
30
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+ spec.extensions = []
33
+
34
+ spec.add_dependency "httpx", "~> 1.5"
35
+ spec.add_dependency "pastel", "~> 0.8.0"
36
+ spec.add_dependency "prism", "~> 1.4.0"
37
+ spec.add_dependency "rb_sys", "~> 0.9.91"
38
+ spec.add_dependency "tty-box", "~> 0.7"
39
+ spec.add_dependency "tty-file", "~> 0.10"
40
+ spec.add_dependency "tty-option", "~> 0.3"
41
+ spec.add_dependency "tty-progressbar", "~> 0.18.3"
42
+ spec.add_dependency "tty-prompt", "~> 0.23"
43
+ spec.add_dependency "tty-reader", "~> 0.9"
44
+ spec.add_dependency "tty-screen", "~> 0.8"
45
+ spec.add_dependency "tty-spinner", "~> 0.9.3"
46
+ spec.add_dependency "tty-table", "~> 0.12"
47
+
48
+ spec.post_install_message = begin
49
+ require_relative "lib/tng/ui/post_install_box"
50
+ PostInstallBox.new(spec.version).render
51
+ rescue LoadError, StandardError
52
+ "TNG v#{spec.version} installed successfully!\n" +
53
+ "Run 'rails g tng:install' to get started.\n" +
54
+ "Use 'bundle exec tng --help' for usage information."
55
+ end
56
+ end