tng 0.5.3 → 0.5.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.
- checksums.yaml +4 -4
- data/bin/tng +21 -6
- data/binaries/tng-darwin-arm64.bundle +0 -0
- data/binaries/tng-darwin-x86_64.bundle +0 -0
- data/binaries/tng-linux-arm64.so +0 -0
- data/binaries/tng-linux-x86_64.so +0 -0
- data/binaries/tng.bundle +0 -0
- data/lib/generators/tng/install_generator.rb +4 -0
- data/lib/tng/analyzers/controller.rb +17 -2
- data/lib/tng/analyzers/model.rb +5 -3
- data/lib/tng/analyzers/other.rb +1 -1
- data/lib/tng/analyzers/service.rb +12 -10
- data/lib/tng/api/http_client.rb +4 -4
- data/lib/tng/services/direct_generation.rb +16 -1
- data/lib/tng/services/extract_methods.rb +3 -3
- data/lib/tng/services/file_type_detector.rb +50 -14
- data/lib/tng/services/test_generator.rb +67 -78
- data/lib/tng/ui/post_install_box.rb +12 -12
- data/lib/tng/ui/theme.rb +22 -0
- data/lib/tng/utils.rb +63 -0
- data/lib/tng/version.rb +1 -1
- data/lib/tng.rb +19 -1
- metadata +5 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8adc64b4a410ddec1b69ada11a09e9cee648f88692873ae6551990753ba0f564
|
|
4
|
+
data.tar.gz: aa5c284afe9a769733b3ab87d37c1c51f23bf07ee4b3ffdfac65f60fe39f3548
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01701577e5856bbe861677eb03d8b8e629046e48e2301a4b4202cd78049252d7478ce7032c6f80cb547b9e9e324893bb5ad24859a16ae0491857287e9c040838
|
|
7
|
+
data.tar.gz: 6c8e17a8f0d9e6d6a2fa0c564b01d54eaa28c4e1d495f0fe359818477eed439632b42ea96d2bc7dfd32543fe9ada7759989c71950b538fdb76e2debc6f17860d
|
data/bin/tng
CHANGED
|
@@ -51,6 +51,10 @@ class CLI
|
|
|
51
51
|
example " • Select specific methods from filtered lists", ""
|
|
52
52
|
example " • Generate tests for individual methods", ""
|
|
53
53
|
example ""
|
|
54
|
+
example "Ignore paths via config (config/initializers/tng.rb):", ""
|
|
55
|
+
example " config.ignore_files = [\"app/models/user.rb\"]", ""
|
|
56
|
+
example " config.ignore_folders = [\"app/admin\", \"vendor\"]", ""
|
|
57
|
+
example ""
|
|
54
58
|
example "Direct mode (automatic file type detection):", ""
|
|
55
59
|
example " bundle exec tng app/controllers/users_controller.rb index", ""
|
|
56
60
|
example " bundle exec tng f=users_controller.rb m=show", ""
|
|
@@ -560,7 +564,6 @@ class CLI
|
|
|
560
564
|
|
|
561
565
|
def run_audit_for_controller_method(controller, method_info)
|
|
562
566
|
result = nil
|
|
563
|
-
|
|
564
567
|
@go_ui.show_progress("Auditing #{controller[:name]}##{method_info[:name]}") do |progress|
|
|
565
568
|
progress.update("Analyzing method context...", 25)
|
|
566
569
|
progress.update("Running logical analysis...", 50)
|
|
@@ -602,6 +605,7 @@ class CLI
|
|
|
602
605
|
puts @pastel.red("Error: File '#{file_path}' not found.")
|
|
603
606
|
return
|
|
604
607
|
end
|
|
608
|
+
return unless ensure_not_ignored(file_path)
|
|
605
609
|
|
|
606
610
|
absolute_path = File.expand_path(file_path)
|
|
607
611
|
|
|
@@ -721,7 +725,7 @@ class CLI
|
|
|
721
725
|
begin
|
|
722
726
|
files_json = Tng.list_deadcode_files(root)
|
|
723
727
|
files = JSON.parse(files_json)
|
|
724
|
-
return files.select { |f| File.file?(f) }
|
|
728
|
+
return files.select { |f| File.file?(f) && !Tng::Utils.ignored_path?(f) }
|
|
725
729
|
rescue => e
|
|
726
730
|
puts @pastel.red("Error listing repo files: #{e.message}")
|
|
727
731
|
[]
|
|
@@ -781,7 +785,6 @@ class CLI
|
|
|
781
785
|
|
|
782
786
|
def run_audit_for_model_method(model, method_info)
|
|
783
787
|
result = nil
|
|
784
|
-
|
|
785
788
|
@go_ui.show_progress("Auditing #{model[:name]}##{method_info[:name]}") do |progress|
|
|
786
789
|
progress.update("Parsing method details...", 25)
|
|
787
790
|
progress.update("Analyzing method context...", 25)
|
|
@@ -858,7 +861,6 @@ class CLI
|
|
|
858
861
|
|
|
859
862
|
def run_audit_for_service_method(service, method_info)
|
|
860
863
|
result = nil
|
|
861
|
-
|
|
862
864
|
@go_ui.show_progress("Auditing #{service[:name]}##{method_info[:name]}") do |progress|
|
|
863
865
|
progress.update("Parsing method details...", 25)
|
|
864
866
|
progress.update("Analyzing method context...", 25)
|
|
@@ -933,7 +935,6 @@ class CLI
|
|
|
933
935
|
|
|
934
936
|
def run_audit_for_other_method(other_file, method_info)
|
|
935
937
|
result = nil
|
|
936
|
-
|
|
937
938
|
@go_ui.show_progress("Auditing #{other_file[:name]}##{method_info[:name]}") do |progress|
|
|
938
939
|
progress.update("Parsing method details...", 25)
|
|
939
940
|
progress.update("Analyzing method context...", 25)
|
|
@@ -1405,7 +1406,7 @@ class CLI
|
|
|
1405
1406
|
end
|
|
1406
1407
|
|
|
1407
1408
|
def show_post_generation_menu(result)
|
|
1408
|
-
if @go_ui.is_a?(Tng::UI::JsonSession)
|
|
1409
|
+
if defined?(Tng::UI::JsonSession) && @go_ui.is_a?(Tng::UI::JsonSession)
|
|
1409
1410
|
@go_ui.show_generation_result(result)
|
|
1410
1411
|
return
|
|
1411
1412
|
end
|
|
@@ -1453,6 +1454,16 @@ class CLI
|
|
|
1453
1454
|
end
|
|
1454
1455
|
end
|
|
1455
1456
|
|
|
1457
|
+
def ensure_not_ignored(file_path)
|
|
1458
|
+
return true unless Tng::Utils.ignored_path?(file_path)
|
|
1459
|
+
|
|
1460
|
+
puts @pastel.decorate(
|
|
1461
|
+
"Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed.",
|
|
1462
|
+
Tng::UI::Theme.color(:error)
|
|
1463
|
+
)
|
|
1464
|
+
false
|
|
1465
|
+
end
|
|
1466
|
+
|
|
1456
1467
|
def initialize_config_and_clients
|
|
1457
1468
|
@config_initialized = true
|
|
1458
1469
|
|
|
@@ -1678,6 +1689,7 @@ class CLI
|
|
|
1678
1689
|
puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
|
|
1679
1690
|
return
|
|
1680
1691
|
end
|
|
1692
|
+
return unless ensure_not_ignored(full_path)
|
|
1681
1693
|
|
|
1682
1694
|
relative_path = full_path.gsub("#{Dir.pwd}/", "")
|
|
1683
1695
|
namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
|
|
@@ -1702,6 +1714,7 @@ class CLI
|
|
|
1702
1714
|
puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
|
|
1703
1715
|
return
|
|
1704
1716
|
end
|
|
1717
|
+
return unless ensure_not_ignored(full_path)
|
|
1705
1718
|
|
|
1706
1719
|
relative_path = full_path.gsub("#{Dir.pwd}/", "")
|
|
1707
1720
|
namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
|
|
@@ -1749,6 +1762,7 @@ class CLI
|
|
|
1749
1762
|
puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
|
|
1750
1763
|
return
|
|
1751
1764
|
end
|
|
1765
|
+
return unless ensure_not_ignored(full_path)
|
|
1752
1766
|
|
|
1753
1767
|
relative_path = full_path.gsub("#{Dir.pwd}/", "")
|
|
1754
1768
|
namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
|
|
@@ -1776,6 +1790,7 @@ class CLI
|
|
|
1776
1790
|
puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
|
|
1777
1791
|
return
|
|
1778
1792
|
end
|
|
1793
|
+
return unless ensure_not_ignored(full_path)
|
|
1779
1794
|
|
|
1780
1795
|
relative_path = full_path.gsub("#{Dir.pwd}/", "")
|
|
1781
1796
|
namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
|
|
Binary file
|
|
Binary file
|
data/binaries/tng-linux-arm64.so
CHANGED
|
Binary file
|
|
Binary file
|
data/binaries/tng.bundle
CHANGED
|
Binary file
|
|
@@ -43,6 +43,10 @@ module Tng
|
|
|
43
43
|
# Default auto-detection checks: spec/rails_helper.rb, spec/spec_helper.rb, test/test_helper.rb
|
|
44
44
|
# config.test_helper_path = "spec/rails_helper.rb"
|
|
45
45
|
|
|
46
|
+
# Ignore paths (relative to project root)
|
|
47
|
+
# config.ignore_files = []
|
|
48
|
+
# config.ignore_folders = []
|
|
49
|
+
|
|
46
50
|
# Authentication Configuration
|
|
47
51
|
# Set to false if your application does not require authentication
|
|
48
52
|
# config.authentication_enabled = true
|
|
@@ -5,7 +5,8 @@ module Tng
|
|
|
5
5
|
class Controller
|
|
6
6
|
def self.files_in_dir(dir)
|
|
7
7
|
dir = File.join(Dir.pwd, "app/controllers") if dir.nil?
|
|
8
|
-
Tng::Analyzer::Controller.files_in_dir(dir.to_s)
|
|
8
|
+
files = Tng::Analyzer::Controller.files_in_dir(dir.to_s)
|
|
9
|
+
Tng::Utils.filter_ignored_files(files)
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def self.routes_for_controller(file_path)
|
|
@@ -42,7 +43,7 @@ module Tng
|
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
def self.methods_for_controller(controller_name)
|
|
46
|
+
def self.methods_for_controller(controller_name, file_path = nil)
|
|
46
47
|
raise "controller_name is required" if controller_name.nil?
|
|
47
48
|
|
|
48
49
|
begin
|
|
@@ -58,6 +59,20 @@ module Tng
|
|
|
58
59
|
method.owner == controller_class
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
# Fallback to file parsing if reflection fails
|
|
63
|
+
if action_methods.empty? && file_path && File.exist?(file_path)
|
|
64
|
+
source_code = File.read(file_path)
|
|
65
|
+
result = Prism.parse(source_code)
|
|
66
|
+
defined_methods = []
|
|
67
|
+
|
|
68
|
+
result.value&.child_nodes&.each do |node|
|
|
69
|
+
next unless node.is_a?(Prism::DefNode)
|
|
70
|
+
defined_methods << node.name.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
action_methods = defined_methods
|
|
74
|
+
end
|
|
75
|
+
|
|
61
76
|
# Return method info
|
|
62
77
|
action_methods.map do |method_name|
|
|
63
78
|
{
|
data/lib/tng/analyzers/model.rb
CHANGED
|
@@ -5,7 +5,8 @@ module Tng
|
|
|
5
5
|
class Model
|
|
6
6
|
def self.files_in_dir(dir = nil)
|
|
7
7
|
dir = File.join(Dir.pwd, "app/models") if dir.nil?
|
|
8
|
-
Tng::Analyzer::Model.files_in_dir(dir.to_s)
|
|
8
|
+
files = Tng::Analyzer::Model.files_in_dir(dir.to_s)
|
|
9
|
+
Tng::Utils.filter_ignored_files(files)
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def self.value_for_model(file_path)
|
|
@@ -23,7 +24,7 @@ module Tng
|
|
|
23
24
|
Tng::Analyzer::Model.model_connections(file_path.to_s)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def self.methods_for_model(model_name)
|
|
27
|
+
def self.methods_for_model(model_name, file_path = nil)
|
|
27
28
|
raise "model_name is required" if model_name.nil?
|
|
28
29
|
|
|
29
30
|
begin
|
|
@@ -35,7 +36,8 @@ module Tng
|
|
|
35
36
|
model_class.private_instance_methods(false)
|
|
36
37
|
class_methods = model_class.public_methods(false) - Class.public_methods
|
|
37
38
|
|
|
38
|
-
model_file =
|
|
39
|
+
model_file = file_path
|
|
40
|
+
model_file ||= Object.const_source_location(model_class.name)&.first
|
|
39
41
|
|
|
40
42
|
if model_file && File.exist?(model_file)
|
|
41
43
|
source_code = File.read(model_file)
|
data/lib/tng/analyzers/other.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Tng
|
|
|
24
24
|
Tng::Analyzer::Service.parse_service_file(file_path)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def self.methods_for_service(service_name)
|
|
27
|
+
def self.methods_for_service(service_name, file_path = nil)
|
|
28
28
|
raise "service_name is required" if service_name.nil?
|
|
29
29
|
|
|
30
30
|
begin
|
|
@@ -35,11 +35,12 @@ module Tng
|
|
|
35
35
|
service_class.private_instance_methods(false)
|
|
36
36
|
class_methods = service_class.public_methods(false) - Class.public_methods
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
service_file =
|
|
38
|
+
# Prefer explicit file path or class definition location
|
|
39
|
+
service_file = file_path
|
|
40
|
+
service_file ||= Object.const_source_location(service_class.name)&.first
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
-
if instance_methods.any?
|
|
42
|
+
# Fallback to any instance method source location
|
|
43
|
+
if service_file.nil? && instance_methods.any?
|
|
43
44
|
begin
|
|
44
45
|
service_file = service_class.instance_method(instance_methods.first).source_location&.first
|
|
45
46
|
rescue StandardError
|
|
@@ -47,9 +48,6 @@ module Tng
|
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
# Fallback to const_source_location if no method source found
|
|
51
|
-
service_file ||= Object.const_source_location(service_class.name)&.first
|
|
52
|
-
|
|
53
51
|
service_methods = if service_file && File.exist?(service_file)
|
|
54
52
|
source_code = File.read(service_file)
|
|
55
53
|
result = Prism.parse(source_code)
|
|
@@ -68,7 +66,11 @@ module Tng
|
|
|
68
66
|
defined_methods.include?(method_name.to_s)
|
|
69
67
|
end
|
|
70
68
|
|
|
71
|
-
filtered_instance_methods
|
|
69
|
+
if filtered_instance_methods.empty? && filtered_class_methods.empty? && defined_methods.any?
|
|
70
|
+
defined_methods
|
|
71
|
+
else
|
|
72
|
+
filtered_instance_methods + filtered_class_methods
|
|
73
|
+
end
|
|
72
74
|
else
|
|
73
75
|
[]
|
|
74
76
|
end
|
|
@@ -107,7 +109,7 @@ module Tng
|
|
|
107
109
|
path: file_path,
|
|
108
110
|
relative_path: relative_path
|
|
109
111
|
}
|
|
110
|
-
end
|
|
112
|
+
end.reject { |file| Tng::Utils.ignored_path?(file[:path]) }
|
|
111
113
|
end
|
|
112
114
|
end
|
|
113
115
|
end
|
data/lib/tng/api/http_client.rb
CHANGED
|
@@ -6,10 +6,10 @@ module Tng
|
|
|
6
6
|
@api_endpoint = api_endpoint
|
|
7
7
|
@api_key = api_key
|
|
8
8
|
@timeout = {
|
|
9
|
-
connect_timeout:
|
|
10
|
-
read_timeout:
|
|
11
|
-
write_timeout:
|
|
12
|
-
request_timeout:
|
|
9
|
+
connect_timeout: 420,
|
|
10
|
+
read_timeout: 420,
|
|
11
|
+
write_timeout: 420,
|
|
12
|
+
request_timeout: 420
|
|
13
13
|
}
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -35,13 +35,27 @@ module Tng
|
|
|
35
35
|
return
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
if Tng::Utils.ignored_path?(file_path)
|
|
39
|
+
@go_ui.display_error(@pastel.red("❌ Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed."))
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
resolved_result = FileTypeDetector.resolve_file_path(file_path)
|
|
44
|
+
if resolved_result.is_a?(Hash) && resolved_result[:ignored]
|
|
45
|
+
@go_ui.display_error(@pastel.red("❌ Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed."))
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
resolved_path = resolved_result.is_a?(Hash) ? resolved_result[:path] : resolved_result
|
|
39
49
|
|
|
40
50
|
unless resolved_path && File.exist?(resolved_path)
|
|
41
51
|
@go_ui.display_error(@pastel.red("❌ File not found: #{file_path}"))
|
|
42
52
|
suggest_similar_files(file_path)
|
|
43
53
|
return
|
|
44
54
|
end
|
|
55
|
+
if Tng::Utils.ignored_path?(resolved_path)
|
|
56
|
+
@go_ui.display_error(@pastel.red("❌ Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed."))
|
|
57
|
+
return
|
|
58
|
+
end
|
|
45
59
|
|
|
46
60
|
type = FileTypeDetector.detect_type(resolved_path)
|
|
47
61
|
|
|
@@ -72,6 +86,7 @@ module Tng
|
|
|
72
86
|
next unless Dir.exist?(File.join(rails_root, dir))
|
|
73
87
|
|
|
74
88
|
Dir.glob(File.join(rails_root, dir, "**", "*#{base_name}*.rb")).each do |file|
|
|
89
|
+
next if Tng::Utils.ignored_path?(file)
|
|
75
90
|
similar_files << file.gsub(%r{^#{Regexp.escape(rails_root)}/}, "")
|
|
76
91
|
end
|
|
77
92
|
end
|
|
@@ -4,21 +4,21 @@ module Tng
|
|
|
4
4
|
module Services
|
|
5
5
|
module ExtractMethods
|
|
6
6
|
def extract_controller_methods(controller)
|
|
7
|
-
Tng::Analyzers::Controller.methods_for_controller(controller[:name]) || []
|
|
7
|
+
Tng::Analyzers::Controller.methods_for_controller(controller[:name], controller[:path]) || []
|
|
8
8
|
rescue StandardError => e
|
|
9
9
|
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing controller: #{e.message}", Tng::UI::Theme.color(:error)))
|
|
10
10
|
[]
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def extract_model_methods(model)
|
|
14
|
-
Tng::Analyzers::Model.methods_for_model(model[:name]) || []
|
|
14
|
+
Tng::Analyzers::Model.methods_for_model(model[:name], model[:path]) || []
|
|
15
15
|
rescue StandardError => e
|
|
16
16
|
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing model: #{e.message}", Tng::UI::Theme.color(:error)))
|
|
17
17
|
[]
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def extract_service_methods(service)
|
|
21
|
-
Tng::Analyzers::Service.methods_for_service(service[:name]) || []
|
|
21
|
+
Tng::Analyzers::Service.methods_for_service(service[:name], service[:path]) || []
|
|
22
22
|
rescue StandardError => e
|
|
23
23
|
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing service: #{e.message}", Tng::UI::Theme.color(:error)))
|
|
24
24
|
[]
|
|
@@ -70,33 +70,69 @@ module Tng
|
|
|
70
70
|
lib app/lib
|
|
71
71
|
].freeze
|
|
72
72
|
|
|
73
|
-
def
|
|
73
|
+
def candidate_paths_for(file_name)
|
|
74
74
|
file_with_ext = file_name.end_with?('.rb') ? file_name : "#{file_name}.rb"
|
|
75
|
+
candidates = []
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
if File.exist?(file_with_ext)
|
|
78
|
+
candidates << File.expand_path(file_with_ext)
|
|
79
|
+
end
|
|
77
80
|
|
|
78
81
|
rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
|
|
79
82
|
|
|
80
83
|
SEARCH_PATHS.each do |dir|
|
|
81
84
|
full_path = File.join(rails_root, dir, file_with_ext)
|
|
82
|
-
|
|
85
|
+
candidates << full_path if File.exist?(full_path)
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
return found_files.first unless found_files.empty?
|
|
87
|
+
candidates.concat(Dir.glob(File.join(rails_root, dir, '**', file_with_ext)))
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
candidates.uniq
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def find_file_in_project(file_name)
|
|
94
|
+
candidate_paths_for(file_name).find { |candidate| !Tng::Utils.ignored_path?(candidate) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def find_ignored_in_project(file_name)
|
|
98
|
+
candidate_paths_for(file_name).find { |candidate| Tng::Utils.ignored_path?(candidate) }
|
|
89
99
|
end
|
|
90
100
|
|
|
91
101
|
def resolve_file_path(file_path)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
if file_path.start_with?('/')
|
|
103
|
+
candidates = []
|
|
104
|
+
candidates << file_path if File.exist?(file_path)
|
|
105
|
+
candidates << "#{file_path}.rb" if File.exist?("#{file_path}.rb")
|
|
106
|
+
|
|
107
|
+
ignored_candidate = nil
|
|
108
|
+
candidates.each do |candidate|
|
|
109
|
+
expanded = File.expand_path(candidate)
|
|
110
|
+
if Tng::Utils.ignored_path?(expanded)
|
|
111
|
+
ignored_candidate ||= expanded
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
return { path: expanded, ignored: false }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
return { path: ignored_candidate, ignored: true } if ignored_candidate
|
|
119
|
+
return nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
ignored_candidate = nil
|
|
123
|
+
candidate_paths_for(file_path).each do |candidate|
|
|
124
|
+
expanded = File.expand_path(candidate)
|
|
125
|
+
if Tng::Utils.ignored_path?(expanded)
|
|
126
|
+
ignored_candidate ||= expanded
|
|
127
|
+
next
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
return { path: expanded, ignored: false }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return { path: ignored_candidate, ignored: true } if ignored_candidate
|
|
134
|
+
|
|
135
|
+
nil
|
|
100
136
|
end
|
|
101
137
|
|
|
102
138
|
def extract_relative_path(file_path, type)
|
|
@@ -89,7 +89,7 @@ module Tng
|
|
|
89
89
|
job_id = job_data["job_id"]
|
|
90
90
|
return { error: :server_error, message: "No job_id returned" } unless job_id
|
|
91
91
|
|
|
92
|
-
# Poll for completion (
|
|
92
|
+
# Poll for completion (audit only)
|
|
93
93
|
result = poll_for_completion(job_id, progress: progress)
|
|
94
94
|
return { error: :timeout, message: "Audit timed out" } unless result
|
|
95
95
|
|
|
@@ -234,33 +234,7 @@ module Tng
|
|
|
234
234
|
max_attempts = MAX_POLL_DURATION_SECONDS / POLL_INTERVAL_SECONDS
|
|
235
235
|
|
|
236
236
|
# Initialize agent steps if progress tracking is enabled
|
|
237
|
-
agent_step_indices =
|
|
238
|
-
if progress
|
|
239
|
-
# Current step index in progress updater (assuming it follows previous steps)
|
|
240
|
-
# We create 4 generic steps for the agents
|
|
241
|
-
# Note: indices are relative to the current session, so we just append them.
|
|
242
|
-
# But we need their indices to update them later.
|
|
243
|
-
# Since we can't ask progress for current index easily without hacking,
|
|
244
|
-
# we rely on the fact that we call update 4 times.
|
|
245
|
-
|
|
246
|
-
# Use a base offset if we could know it, but we can't reliably.
|
|
247
|
-
# Actually, if we use explicit_step, we need absolute indices.
|
|
248
|
-
# Let's assume the previous steps were 0, 1, 2, 3 based on bin/tng.
|
|
249
|
-
# So we start at 4.
|
|
250
|
-
base_idx = 4
|
|
251
|
-
|
|
252
|
-
progress.update("Context Builder: Pending...", nil, step_increment: true)
|
|
253
|
-
agent_step_indices["context_agent_status"] = base_idx
|
|
254
|
-
|
|
255
|
-
progress.update("Style Analyzer: Pending...", nil, step_increment: true)
|
|
256
|
-
agent_step_indices["style_agent_status"] = base_idx + 1
|
|
257
|
-
|
|
258
|
-
progress.update("Logic Analyzer: Pending...", nil, step_increment: true)
|
|
259
|
-
agent_step_indices["logical_issue_status"] = base_idx + 2
|
|
260
|
-
|
|
261
|
-
progress.update("Logic Generator: Pending...", nil, step_increment: true)
|
|
262
|
-
agent_step_indices["behavior_expert_status"] = base_idx + 3
|
|
263
|
-
end
|
|
237
|
+
agent_step_indices = init_agent_steps(progress)
|
|
264
238
|
|
|
265
239
|
loop do
|
|
266
240
|
attempts += 1
|
|
@@ -294,56 +268,7 @@ module Tng
|
|
|
294
268
|
status = status_data[:status]
|
|
295
269
|
|
|
296
270
|
# Update UI with granular info
|
|
297
|
-
|
|
298
|
-
info = status_data[:info]
|
|
299
|
-
|
|
300
|
-
agent_step_indices.each do |key, step_idx|
|
|
301
|
-
item_data = info[key.to_sym]
|
|
302
|
-
next unless item_data
|
|
303
|
-
|
|
304
|
-
agent_status = "pending"
|
|
305
|
-
values = []
|
|
306
|
-
|
|
307
|
-
if item_data.is_a?(Hash)
|
|
308
|
-
agent_status = item_data[:status] || "pending"
|
|
309
|
-
values = item_data[:values] || []
|
|
310
|
-
else
|
|
311
|
-
agent_status = item_data.to_s
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
label = case key
|
|
315
|
-
when "context_agent_status" then "Context Builder"
|
|
316
|
-
when "style_agent_status" then "Style Analyzer"
|
|
317
|
-
when "logical_issue_status" then "Logic Analyzer"
|
|
318
|
-
when "behavior_expert_status" then "Logic Generator"
|
|
319
|
-
else key
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
msg = if agent_status == "processing"
|
|
323
|
-
"#{label}: Processing..."
|
|
324
|
-
elsif agent_status == "completed"
|
|
325
|
-
"#{label}: Completed"
|
|
326
|
-
elsif agent_status == "failed"
|
|
327
|
-
"#{label}: Failed"
|
|
328
|
-
else
|
|
329
|
-
"#{label}: #{agent_status.capitalize}..."
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
if values.any?
|
|
333
|
-
# Clean values
|
|
334
|
-
clean_vals = values.map { |v| v.to_s.gsub("_", " ").gsub("'", "").gsub(":", "").strip }
|
|
335
|
-
display_str = clean_vals.first(3).join(", ")
|
|
336
|
-
display_str += ", ..." if clean_vals.size > 3
|
|
337
|
-
msg += " (#{display_str})"
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# Pass percentage only on the last step (Logic Generator) to keep main bar moving?
|
|
341
|
-
# Or pass it on all updates.
|
|
342
|
-
p = step_idx == agent_step_indices["behavior_expert_status"] ? pct : nil
|
|
343
|
-
|
|
344
|
-
progress.update(msg, p, step_increment: false, explicit_step: step_idx)
|
|
345
|
-
end
|
|
346
|
-
end
|
|
271
|
+
update_progress_from_info(progress, agent_step_indices, status_data[:info], pct) if progress
|
|
347
272
|
|
|
348
273
|
case status
|
|
349
274
|
when "completed"
|
|
@@ -381,6 +306,70 @@ module Tng
|
|
|
381
306
|
exit(0)
|
|
382
307
|
end
|
|
383
308
|
|
|
309
|
+
def init_agent_steps(progress)
|
|
310
|
+
return {} unless progress
|
|
311
|
+
|
|
312
|
+
base_idx = 4
|
|
313
|
+
progress.update("Context Builder: Pending...", nil, step_increment: true)
|
|
314
|
+
progress.update("Style Analyzer: Pending...", nil, step_increment: true)
|
|
315
|
+
progress.update("Logic Analyzer: Pending...", nil, step_increment: true)
|
|
316
|
+
progress.update("Logic Generator: Pending...", nil, step_increment: true)
|
|
317
|
+
|
|
318
|
+
{
|
|
319
|
+
"context_agent_status" => base_idx,
|
|
320
|
+
"style_agent_status" => base_idx + 1,
|
|
321
|
+
"logical_issue_status" => base_idx + 2,
|
|
322
|
+
"behavior_expert_status" => base_idx + 3
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def update_progress_from_info(progress, agent_step_indices, info, percent)
|
|
327
|
+
return unless progress && info.is_a?(Hash)
|
|
328
|
+
|
|
329
|
+
agent_step_indices.each do |key, step_idx|
|
|
330
|
+
item_data = info[key.to_s] || info[key.to_sym]
|
|
331
|
+
next unless item_data
|
|
332
|
+
|
|
333
|
+
agent_status = "pending"
|
|
334
|
+
values = []
|
|
335
|
+
|
|
336
|
+
if item_data.is_a?(Hash)
|
|
337
|
+
agent_status = item_data[:status] || item_data["status"] || "pending"
|
|
338
|
+
values = item_data[:values] || item_data["values"] || []
|
|
339
|
+
else
|
|
340
|
+
agent_status = item_data.to_s
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
label = case key
|
|
344
|
+
when "context_agent_status" then "Context Builder"
|
|
345
|
+
when "style_agent_status" then "Style Analyzer"
|
|
346
|
+
when "logical_issue_status" then "Logic Analyzer"
|
|
347
|
+
when "behavior_expert_status" then "Logic Generator"
|
|
348
|
+
else key
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
msg = if agent_status == "processing"
|
|
352
|
+
"#{label}: Processing..."
|
|
353
|
+
elsif agent_status == "completed"
|
|
354
|
+
"#{label}: Completed"
|
|
355
|
+
elsif agent_status == "failed"
|
|
356
|
+
"#{label}: Failed"
|
|
357
|
+
else
|
|
358
|
+
"#{label}: #{agent_status.capitalize}..."
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
if values.any?
|
|
362
|
+
clean_vals = values.map { |v| v.to_s.gsub("_", " ").gsub("'", "").gsub(":", "").strip }
|
|
363
|
+
display_str = clean_vals.first(3).join(", ")
|
|
364
|
+
display_str += ", ..." if clean_vals.size > 3
|
|
365
|
+
msg += " (#{display_str})"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
p = (percent && step_idx == agent_step_indices["behavior_expert_status"]) ? percent : nil
|
|
369
|
+
progress.update(msg, p, step_increment: false, explicit_step: step_idx)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
384
373
|
def trigger_cleanup(job_id)
|
|
385
374
|
@http_client.patch("#{CONTENT_RESPONSES_PATH}/#{job_id}/cleanup")
|
|
386
375
|
rescue StandardError => e
|
|
@@ -17,9 +17,9 @@ class PostInstallBox
|
|
|
17
17
|
|
|
18
18
|
def content
|
|
19
19
|
[
|
|
20
|
-
pastel.public_send(Tng::UI::Theme.color(:success)).bold("#{Tng::UI::Theme.icon(:success)} Tng installed successfully!"),
|
|
20
|
+
pastel.public_send(Tng::UI::Theme.color(:success)).bold("#{Tng::UI::Theme.icon(:success, ascii: true)} Tng installed successfully!"),
|
|
21
21
|
"",
|
|
22
|
-
pastel.public_send(Tng::UI::Theme.color(:warning)).bold("#{Tng::UI::Theme.icon(:config)} SETUP REQUIRED"),
|
|
22
|
+
pastel.public_send(Tng::UI::Theme.color(:warning)).bold("#{Tng::UI::Theme.icon(:config, ascii: true)} SETUP REQUIRED"),
|
|
23
23
|
"",
|
|
24
24
|
pastel.public_send(Tng::UI::Theme.color(:primary), "1. Generate configuration:"),
|
|
25
25
|
pastel.public_send(Tng::UI::Theme.color(:secondary), " rails g tng:install"),
|
|
@@ -31,30 +31,30 @@ class PostInstallBox
|
|
|
31
31
|
pastel.public_send(Tng::UI::Theme.color(:secondary), " config.api_key = 'your-license-key-here'"),
|
|
32
32
|
"",
|
|
33
33
|
pastel.public_send(Tng::UI::Theme.color(:primary),
|
|
34
|
-
"#{Tng::UI::Theme.icon(:config)} Check documentation for the correct authentication setup"),
|
|
34
|
+
"#{Tng::UI::Theme.icon(:config, ascii: true)} Check documentation for the correct authentication setup"),
|
|
35
35
|
"",
|
|
36
|
-
pastel.public_send(Tng::UI::Theme.color(:accent)).bold("#{Tng::UI::Theme.icon(:rocket)} Usage:"),
|
|
36
|
+
pastel.public_send(Tng::UI::Theme.color(:accent)).bold("#{Tng::UI::Theme.icon(:rocket, ascii: true)} Usage:"),
|
|
37
37
|
"",
|
|
38
38
|
pastel.public_send(Tng::UI::Theme.color(:primary), "Interactive mode:"),
|
|
39
39
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
40
|
-
"#{Tng::UI::Theme.icon(:bullet)} bundle exec tng") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
41
|
-
|
|
40
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} bundle exec tng") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
41
|
+
" - Interactive CLI with method selection"),
|
|
42
42
|
"",
|
|
43
43
|
pastel.public_send(Tng::UI::Theme.color(:primary), "Features:"),
|
|
44
44
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
45
|
-
"#{Tng::UI::Theme.icon(:bullet)} Test 20+ file types: Controllers, Models, Services + Jobs, Helpers, Lib, Policies, Presenters, Mailers, GraphQL, and more"),
|
|
45
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} Test 20+ file types: Controllers, Models, Services + Jobs, Helpers, Lib, Policies, Presenters, Mailers, GraphQL, and more"),
|
|
46
46
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
47
|
-
"#{Tng::UI::Theme.icon(:bullet)} Select specific methods to test"),
|
|
47
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} Select specific methods to test"),
|
|
48
48
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
49
|
-
"#{Tng::UI::Theme.icon(:bullet)} Search and filter through methods"),
|
|
49
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} Search and filter through methods"),
|
|
50
50
|
"",
|
|
51
51
|
pastel.public_send(Tng::UI::Theme.color(:primary), "Help:"),
|
|
52
52
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
53
|
-
"#{Tng::UI::Theme.icon(:bullet)} bundle exec tng --help") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
54
|
-
|
|
53
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} bundle exec tng --help") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
54
|
+
" - Show help information"),
|
|
55
55
|
"",
|
|
56
56
|
pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
57
|
-
"#{Tng::UI::Theme.icon(:lightbulb)} Generate tests for individual methods with precision")
|
|
57
|
+
"#{Tng::UI::Theme.icon(:lightbulb, ascii: true)} Generate tests for individual methods with precision")
|
|
58
58
|
].join("\n")
|
|
59
59
|
end
|
|
60
60
|
|
data/lib/tng/ui/theme.rb
CHANGED
|
@@ -26,6 +26,23 @@ module Tng
|
|
|
26
26
|
bullet: "•"
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
|
+
ICONS_ASCII = {
|
|
30
|
+
success: "[OK]",
|
|
31
|
+
error: "[ERR]",
|
|
32
|
+
warning: "[!]",
|
|
33
|
+
info: "[i]",
|
|
34
|
+
rocket: ">>",
|
|
35
|
+
run: ">",
|
|
36
|
+
wave: "hi",
|
|
37
|
+
stats: "[stats]",
|
|
38
|
+
config: "[cfg]",
|
|
39
|
+
heart: "<3",
|
|
40
|
+
lightbulb: "[tip]",
|
|
41
|
+
back: "<- ",
|
|
42
|
+
marker: ">",
|
|
43
|
+
bullet: "-"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
29
46
|
# Colors - terminal-agnostic color scheme
|
|
30
47
|
COLORS = {
|
|
31
48
|
# Primary status colors
|
|
@@ -119,6 +136,11 @@ module Tng
|
|
|
119
136
|
@background_cache
|
|
120
137
|
end
|
|
121
138
|
|
|
139
|
+
def icon(key, ascii: false)
|
|
140
|
+
icons = ascii ? ICONS_ASCII : ICONS
|
|
141
|
+
icons[key] || ""
|
|
142
|
+
end
|
|
143
|
+
|
|
122
144
|
def get_background_color
|
|
123
145
|
return :dark unless $stdout.tty? && $stdin.tty? && interactive_session?
|
|
124
146
|
|
data/lib/tng/utils.rb
CHANGED
|
@@ -76,6 +76,69 @@ module Tng
|
|
|
76
76
|
false
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
def self.project_root
|
|
80
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
81
|
+
Rails.root.to_s
|
|
82
|
+
else
|
|
83
|
+
Dir.pwd
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.normalize_path(entry, root)
|
|
88
|
+
return nil unless entry.is_a?(String)
|
|
89
|
+
|
|
90
|
+
trimmed = entry.strip
|
|
91
|
+
return nil if trimmed.empty?
|
|
92
|
+
|
|
93
|
+
normalized = trimmed.tr("\\", "/")
|
|
94
|
+
if normalized.start_with?("/")
|
|
95
|
+
File.expand_path(normalized)
|
|
96
|
+
else
|
|
97
|
+
File.expand_path(normalized, root)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.ignored_path?(path, root: nil)
|
|
102
|
+
return false unless path
|
|
103
|
+
|
|
104
|
+
root ||= project_root
|
|
105
|
+
target = normalize_path(path.to_s, root)
|
|
106
|
+
return false unless target
|
|
107
|
+
|
|
108
|
+
ignore_files = Array(Tng.config[:ignore_files])
|
|
109
|
+
ignore_folders = Array(Tng.config[:ignore_folders])
|
|
110
|
+
|
|
111
|
+
ignore_files.each do |entry|
|
|
112
|
+
resolved = normalize_path(entry.to_s, root)
|
|
113
|
+
next unless resolved
|
|
114
|
+
return true if resolved == target
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
ignore_folders.each do |entry|
|
|
118
|
+
resolved = normalize_path(entry.to_s, root)
|
|
119
|
+
next unless resolved
|
|
120
|
+
resolved = resolved.end_with?("/") ? resolved.chomp("/") : resolved
|
|
121
|
+
return true if target == resolved || target.start_with?(resolved + "/")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.filter_ignored_files(files, root: nil, path_key: :path)
|
|
128
|
+
return [] if files.nil?
|
|
129
|
+
root ||= project_root
|
|
130
|
+
|
|
131
|
+
files.reject do |file|
|
|
132
|
+
candidate =
|
|
133
|
+
if file.is_a?(Hash)
|
|
134
|
+
file[path_key] || file[path_key.to_s]
|
|
135
|
+
else
|
|
136
|
+
file
|
|
137
|
+
end
|
|
138
|
+
ignored_path?(candidate, root: root)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
79
142
|
def self.save_test_file(test_content)
|
|
80
143
|
puts "📋 Raw API response: #{test_content[0..200]}..." if ENV["DEBUG"]
|
|
81
144
|
parsed_response = JSON.parse(test_content)
|
data/lib/tng/version.rb
CHANGED
data/lib/tng.rb
CHANGED
|
@@ -98,7 +98,9 @@ module Tng
|
|
|
98
98
|
base_url: "https://app.tng.sh/",
|
|
99
99
|
test_helper_path: nil,
|
|
100
100
|
authentication_enabled: false,
|
|
101
|
-
authentication_methods: []
|
|
101
|
+
authentication_methods: [],
|
|
102
|
+
ignore_files: [],
|
|
103
|
+
ignore_folders: []
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
def self.configure
|
|
@@ -148,4 +150,20 @@ module Tng
|
|
|
148
150
|
def self.authentication_enabled
|
|
149
151
|
@config[:authentication_enabled]
|
|
150
152
|
end
|
|
153
|
+
|
|
154
|
+
def self.ignore_files=(value)
|
|
155
|
+
@config[:ignore_files] = value
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.ignore_files
|
|
159
|
+
@config[:ignore_files]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.ignore_folders=(value)
|
|
163
|
+
@config[:ignore_folders] = value
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.ignore_folders
|
|
167
|
+
@config[:ignore_folders]
|
|
168
|
+
end
|
|
151
169
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tng
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ralucab
|
|
@@ -198,31 +198,10 @@ metadata:
|
|
|
198
198
|
homepage_uri: https://tng.sh/
|
|
199
199
|
source_code_uri: https://github.com/tng-sh/tng-rails-public
|
|
200
200
|
license_uri: https://github.com/tng-sh/tng-rails-public/blob/main/LICENSE.md
|
|
201
|
-
post_install_message:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
\ │\n│ 1. Generate
|
|
206
|
-
configuration: │\n│ rails g tng:install
|
|
207
|
-
\ │\n│ │\n│
|
|
208
|
-
\ 2. Edit configuration file: │\n│ config/initializers/tng.rb
|
|
209
|
-
\ │\n│ │\n│
|
|
210
|
-
\ 3. Add your license key: │\n│ config.api_key
|
|
211
|
-
= 'your-license-key-here' │\n│ │\n│
|
|
212
|
-
\ \U0001F4CB Check documentation for the correct authentication setup │\n│
|
|
213
|
-
\ │\n│ \U0001F680
|
|
214
|
-
Usage: │\n│ │\n│
|
|
215
|
-
\ Interactive mode: │\n│ • bundle
|
|
216
|
-
exec tng - Interactive CLI with method selection │\n│ │\n│
|
|
217
|
-
\ Features: │\n│ • Test
|
|
218
|
-
20+ file types: Controllers, Models, Services + Jobs, │\n│ Helpers, Lib, Policies,
|
|
219
|
-
Presenters, Mailers, GraphQL, and more │\n│ • Select specific methods to test
|
|
220
|
-
\ │\n│ • Search and filter through methods │\n│
|
|
221
|
-
\ │\n│ Help:
|
|
222
|
-
\ │\n│ • bundle exec
|
|
223
|
-
tng --help - Show help information │\n│ │\n│
|
|
224
|
-
\ \U0001F4A1 Generate tests for individual methods with precision │\n└────────────────────────────────────────────────────────────
|
|
225
|
-
v0.5.3 ┘\n"
|
|
201
|
+
post_install_message: |-
|
|
202
|
+
TNG v0.5.4 installed successfully!
|
|
203
|
+
Run 'rails g tng:install' to get started.
|
|
204
|
+
Use 'bundle exec tng --help' for usage information.
|
|
226
205
|
rdoc_options: []
|
|
227
206
|
require_paths:
|
|
228
207
|
- lib
|