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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e58f1737f7ea5758205fa1f6b41985b468d090cb9c770a7af7ed3e78f3cff989
4
- data.tar.gz: 5b8f8c374574ca0dd254a6c3549418cdaf508842740911203f12f86c581690d7
3
+ metadata.gz: 8adc64b4a410ddec1b69ada11a09e9cee648f88692873ae6551990753ba0f564
4
+ data.tar.gz: aa5c284afe9a769733b3ab87d37c1c51f23bf07ee4b3ffdfac65f60fe39f3548
5
5
  SHA512:
6
- metadata.gz: cc2064efd17db8711d02eb8a3e75197fb3fb2ea016a23e75bafc7094875779d092c1cfeb2773f4f918aafa66bb4d93aa7d589233fbe8d1e466e2f439e3e2da42
7
- data.tar.gz: f635125b141d218d832f08da04143bbc41f24faf18ee5d056d0b1aee0c57829bb22530b486b8c615c201a2339de05c2ce70b6087bb7eec1c6d4187a1d16c3dbd
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
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
  {
@@ -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 = Object.const_source_location(model_class.name)&.first
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)
@@ -122,7 +122,7 @@ module Tng
122
122
  relative_path: relative_path,
123
123
  type: file_type
124
124
  }
125
- end
125
+ end.reject { |file| Tng::Utils.ignored_path?(file[:path]) }
126
126
  end
127
127
 
128
128
  def self.determine_file_type(file_path)
@@ -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
- # Try to get source file from any method, fallback to const_source_location
39
- service_file = nil
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
- # First try to get file from an instance method
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 + filtered_class_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
@@ -6,10 +6,10 @@ module Tng
6
6
  @api_endpoint = api_endpoint
7
7
  @api_key = api_key
8
8
  @timeout = {
9
- connect_timeout: 300,
10
- read_timeout: 300,
11
- write_timeout: 300,
12
- request_timeout: 300
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
- resolved_path = FileTypeDetector.resolve_file_path(file_path)
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 find_file_in_project(file_name)
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
- return File.expand_path(file_with_ext) if File.exist?(file_with_ext)
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
- return full_path if File.exist?(full_path)
85
+ candidates << full_path if File.exist?(full_path)
83
86
 
84
- found_files = Dir.glob(File.join(rails_root, dir, '**', file_with_ext))
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
- nil
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
- resolved = if file_path.start_with?('/')
93
- File.exist?(file_path) ? file_path : File.exist?("#{file_path}.rb") ? "#{file_path}.rb" : nil
94
- else
95
- found = find_file_in_project(file_path)
96
- found ? File.expand_path(found) : nil
97
- end
98
-
99
- resolved
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 (similar to test generation)
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
- if progress && status_data[:info].is_a?(Hash)
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
- " - Interactive CLI with method selection"),
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
- " - Show help information"),
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tng
4
- VERSION = "0.5.3"
4
+ VERSION = "0.5.4"
5
5
  end
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.3
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: "┌ TNG ───────────────────────────────────────────────────────────────┐\n│
202
- \ │\n│ ✅ Tng
203
- installed successfully! │\n│ │\n│
204
- \ \U0001F4CB SETUP REQUIRED │\n│
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