tng 0.5.2 → 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.
@@ -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
@@ -89,6 +104,9 @@ module Tng
89
104
  end
90
105
 
91
106
  method_info = methods.find { |m| m[:name].downcase == method_name.downcase }
107
+ if method_info && @params[:class_name]
108
+ method_info = method_info.merge(class_name: @params[:class_name])
109
+ end
92
110
 
93
111
  unless method_info
94
112
  @go_ui.display_error(@pastel.red("❌ Method '#{method_name}' not found in #{file_object[:name]}"))
@@ -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
@@ -256,7 +256,7 @@ module Tng
256
256
  puts "Trace results error: #{e.message}"
257
257
  end
258
258
 
259
- # Show impact audit results
259
+ # Show regression check results
260
260
  # @param impact_file_path [String] Path to JSON impact file
261
261
  def show_impact_results(impact_file_path)
262
262
  system(@binary_path, "ruby-impact-results", "--file", impact_file_path)
@@ -415,7 +415,7 @@ module Tng
415
415
  { cmd: "bundle exec tng --file=FILE --method=METHOD", desc: "Direct test generation" },
416
416
  { cmd: "bundle exec tng --file=FILE --method=METHOD --audit", desc: "Direct audit mode" },
417
417
  { cmd: "bundle exec tng --file=FILE --method=METHOD --trace", desc: "Symbolic trace mode" },
418
- { cmd: "bundle exec tng --file=FILE --method=METHOD --impact", desc: "Impact audit mode" },
418
+ { cmd: "bundle exec tng --file=FILE --method=METHOD --impact", desc: "Regression check mode" },
419
419
  { cmd: "bundle exec tng --file=FILE --clones", desc: "Check for code duplicates" },
420
420
  { cmd: "bundle exec tng --file=FILE --deadcode", desc: "Run dead code detection" },
421
421
  { cmd: "bundle exec tng --file=FILE --method=METHOD --xray", desc: "X-Ray visualization" }
@@ -426,7 +426,7 @@ module Tng
426
426
  "Per-method test generation",
427
427
  "Code audit for issues & behaviors",
428
428
  "Symbolic execution traces",
429
- "Impact audit against Git HEAD",
429
+ "Regression check against Git HEAD",
430
430
  "Duplicate code detection (Clones)",
431
431
  "Dead code detection (Rust-powered)",
432
432
  "X-Ray Logic Flow Visualization",
@@ -438,7 +438,7 @@ module Tng
438
438
  { flag: "--method=NAME", desc: "Method name to generate test for" },
439
439
  { flag: "--audit", desc: "Run audit mode instead of test generation" },
440
440
  { flag: "--trace", desc: "Run symbolic trace mode" },
441
- { flag: "--impact", desc: "Run impact audit mode" },
441
+ { flag: "--impact", desc: "Run regression check mode" },
442
442
  { flag: "--clones", desc: "Run clone detection mode" },
443
443
  { flag: "--level=1|2|3|all", desc: "Set clone detection level (default: all)" },
444
444
  { flag: "--deadcode", desc: "Run dead code detection" },
@@ -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.2"
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