rubion 0.3.3 → 0.3.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: 72ddffc4eae85bd07e59db90bb20e6890ec3bc3b2da6d725dc9ffea8d7f3e7a5
4
- data.tar.gz: 4d3fc54759167ab0b568489b61dacacdb7898b0716fb8fde245de186ea1e4995
3
+ metadata.gz: ca049f036597e496afc3e38e4aa0005588d7d5ea544242566b8bb3b4b88ae5a1
4
+ data.tar.gz: 8f3c8b61f3cd5aee897220695adca2c70c334ec0c9fff8991421fe13df08caa5
5
5
  SHA512:
6
- metadata.gz: b3c7dffd83662d797e033288bc326645a14bb77847f16bb1472ab219d75081b4ee97769854a9eac641548c6f601b864cff7bc524d6a64a5605f0bda166f7def1
7
- data.tar.gz: 253bf224334af3d725c074a7c5f3867ab7cede744663bf46e36a16f74232691648c95210ffc64db673569cb96c1f036152880d7dcb7dac512774824c011874d7
6
+ metadata.gz: 4fc7ed3e75f7693842cad49a84f23f485191d85348216f5f4bb8a9194cb8d879a3c1212b83e891f9e4238468dced1303f1c6813bdf5c784679b87025cb21be1a
7
+ data.tar.gz: 7770cc5710d9ab0386e1fc17aa7d9ce9e8ec5690d1c8429959caf9bdbcdccb257e129c2d6b6d86e50b5d577eb5ba403504c648757c185345cb9fe99f9d124cb4
@@ -82,8 +82,11 @@ module Rubion
82
82
  t.headings = ['Name', 'Current', 'Date', 'Latest', 'Date', 'Behind By(Time)', 'Behind By(Versions)']
83
83
 
84
84
  versions.each do |gem|
85
+ # Add ✅ prefix for direct dependencies
86
+ gem_name = gem[:direct] ? "#{gem[:gem]} ✅ " : gem[:gem]
87
+
85
88
  t.add_row [
86
- gem[:gem],
89
+ gem_name,
87
90
  gem[:current],
88
91
  gem[:current_date] || 'N/A',
89
92
  gem[:latest],
@@ -139,8 +142,11 @@ module Rubion
139
142
  t.headings = ['Name', 'Current', 'Date', 'Latest', 'Date', 'Behind By(Time)', 'Behind By(Versions)']
140
143
 
141
144
  versions.each do |pkg|
145
+ # Add ✅ prefix for direct dependencies
146
+ package_name = pkg[:direct] ? "✅ #{pkg[:package]}" : pkg[:package]
147
+
142
148
  t.add_row [
143
- pkg[:package],
149
+ package_name,
144
150
  pkg[:current],
145
151
  pkg[:current_date] || 'N/A',
146
152
  pkg[:latest],
@@ -248,7 +254,10 @@ module Rubion
248
254
  sorted = versions.sort_by do |item|
249
255
  case normalized_column
250
256
  when 'name'
251
- item[name_key_sym].to_s.downcase
257
+ # Remove ✅ prefix for sorting
258
+ name = item[name_key_sym].to_s
259
+ name = name.sub(/^✅\s+/, '') if name.start_with?('✅')
260
+ name.downcase
252
261
  when 'current'
253
262
  parse_version_for_sort(item[:current])
254
263
  when 'date'
@@ -6,7 +6,6 @@ require_relative 'reporter'
6
6
  require 'net/http'
7
7
  require 'uri'
8
8
  require 'date'
9
- require 'thread'
10
9
 
11
10
  module Rubion
12
11
  class Scanner
@@ -26,37 +25,39 @@ module Rubion
26
25
  @result = ScanResult.new
27
26
  @package_manager = package_manager
28
27
  @package_manager_detected = false
28
+ @direct_gems = nil
29
+ @direct_packages = nil
29
30
  end
30
31
 
31
32
  def scan
32
33
  puts "🔍 Scanning project at: #{@project_path}\n\n"
33
-
34
+
34
35
  scan_ruby_gems
35
36
  scan_npm_packages
36
-
37
+
37
38
  @result
38
39
  end
39
40
 
40
- def scan_incremental(options = { gems: true, packages: true, sort_by: "Behind By(Time)", sort_desc: true })
41
+ def scan_incremental(options = { gems: true, packages: true, sort_by: 'Behind By(Time)', sort_desc: true })
41
42
  puts "🔍 Scanning project at: #{@project_path}\n\n"
42
-
43
+
43
44
  # Scan and display Ruby gems first (if enabled)
44
45
  if options[:gems]
45
46
  scan_ruby_gems
46
-
47
+
47
48
  # Print gem results immediately
48
49
  puts "\n"
49
50
  reporter = Reporter.new(@result, sort_by: options[:sort_by], sort_desc: options[:sort_desc])
50
51
  reporter.print_gem_vulnerabilities
51
52
  reporter.print_gem_versions
52
53
  end
53
-
54
+
54
55
  # Then scan NPM packages (if enabled)
55
56
  if options[:packages]
56
- puts "\n" if options[:gems] # Add spacing if gems were scanned
57
+ puts "\n" if options[:gems] # Add spacing if gems were scanned
57
58
  scan_npm_packages
58
59
  end
59
-
60
+
60
61
  @result
61
62
  end
62
63
 
@@ -64,10 +65,10 @@ module Rubion
64
65
 
65
66
  def scan_ruby_gems
66
67
  return unless File.exist?(File.join(@project_path, 'Gemfile.lock'))
67
-
68
+
68
69
  # Check for vulnerabilities using bundler-audit
69
70
  check_gem_vulnerabilities
70
-
71
+
71
72
  # Check for outdated versions using bundle outdated (will show progress)
72
73
  check_gem_versions
73
74
  end
@@ -75,86 +76,86 @@ module Rubion
75
76
  def scan_npm_packages
76
77
  package_json = File.join(@project_path, 'package.json')
77
78
  return unless File.exist?(package_json)
78
-
79
+
79
80
  # Detect package manager if not already set
80
81
  unless @package_manager_detected
81
- @package_manager = @package_manager || detect_package_manager
82
+ @package_manager ||= detect_package_manager
82
83
  @package_manager_detected = true
83
84
  end
84
-
85
+
85
86
  unless @package_manager
86
- puts " ⚠️ Neither npm nor yarn is available. Skipping package scanning."
87
+ puts ' ⚠️ Neither npm nor yarn is available. Skipping package scanning.'
87
88
  return
88
89
  end
89
-
90
+
90
91
  # Check for vulnerabilities using package manager audit
91
92
  check_npm_vulnerabilities
92
-
93
+
93
94
  # Check for outdated versions using package manager outdated (will show progress)
94
95
  check_npm_versions
95
96
  end
96
97
 
97
98
  def check_gem_vulnerabilities
98
99
  # Try to use bundler-audit if available
99
- stdout, stderr, status = Open3.capture3("bundle-audit check 2>&1", chdir: @project_path)
100
-
100
+ stdout, stderr, status = Open3.capture3('bundle-audit check 2>&1', chdir: @project_path)
101
+
101
102
  # bundle-audit returns exit code 1 when vulnerabilities are found, 0 when none found
102
103
  # Always parse if there's output (vulnerabilities found) or if it succeeded (no vulnerabilities)
103
- if stdout.include?("vulnerabilities found") || stdout.include?("Name:") || status.success?
104
+ if stdout.include?('vulnerabilities found') || stdout.include?('Name:') || status.success?
104
105
  parse_bundler_audit_output(stdout)
105
106
  else
106
107
  # No vulnerabilities found or bundler-audit not available
107
108
  @result.gem_vulnerabilities = []
108
109
  end
109
- rescue => e
110
+ rescue StandardError => e
110
111
  puts " ⚠️ Could not run bundle-audit (#{e.message}). Skipping gem vulnerability check."
111
112
  @result.gem_vulnerabilities = []
112
113
  end
113
114
 
114
115
  def check_gem_versions
115
- stdout, stderr, status = Open3.capture3("bundle outdated --parseable", chdir: @project_path)
116
-
116
+ stdout, stderr, status = Open3.capture3('bundle outdated --parseable', chdir: @project_path)
117
+
117
118
  if status.success? || !stdout.empty?
118
119
  parse_bundle_outdated_output(stdout)
119
120
  else
120
121
  # No outdated gems found
121
122
  @result.gem_versions = []
122
123
  end
123
- rescue => e
124
+ rescue StandardError => e
124
125
  puts " ⚠️ Could not run bundle outdated (#{e.message}). Skipping gem version check."
125
126
  @result.gem_versions = []
126
127
  end
127
128
 
128
129
  def check_npm_vulnerabilities
129
130
  return unless @package_manager
130
-
131
+
131
132
  command = "#{@package_manager} audit --json 2>&1"
132
133
  stdout, stderr, status = Open3.capture3(command, chdir: @project_path)
133
-
134
+
134
135
  begin
135
136
  data = JSON.parse(stdout)
136
137
  parse_npm_audit_output(data)
137
138
  rescue JSON::ParserError
138
139
  @result.package_vulnerabilities = []
139
140
  end
140
- rescue => e
141
+ rescue StandardError => e
141
142
  puts " ⚠️ Could not run #{@package_manager} audit (#{e.message}). Skipping package vulnerability check."
142
143
  @result.package_vulnerabilities = []
143
144
  end
144
145
 
145
146
  def check_npm_versions
146
147
  return unless @package_manager
147
-
148
+
148
149
  command = "#{@package_manager} outdated --json 2>&1"
149
150
  stdout, stderr, status = Open3.capture3(command, chdir: @project_path)
150
-
151
+
151
152
  begin
152
153
  data = JSON.parse(stdout) unless stdout.empty?
153
154
  parse_npm_outdated_output(data || {})
154
155
  rescue JSON::ParserError
155
156
  @result.package_versions = []
156
157
  end
157
- rescue => e
158
+ rescue StandardError => e
158
159
  puts " ⚠️ Could not run #{@package_manager} outdated (#{e.message}). Skipping package version check."
159
160
  @result.package_versions = []
160
161
  end
@@ -164,86 +165,86 @@ module Rubion
164
165
  def parse_bundler_audit_output(output)
165
166
  vulnerabilities = []
166
167
  current_gem = nil
167
-
168
+
168
169
  output.each_line do |line|
169
170
  line = line.strip
170
171
  next if line.empty?
171
-
172
+
172
173
  if line =~ /^Name: (.+)/
173
- current_gem = { gem: $1.strip }
174
+ current_gem = { gem: ::Regexp.last_match(1).strip }
174
175
  elsif line =~ /^Version: (.+)/ && current_gem
175
- current_gem[:version] = $1.strip
176
+ current_gem[:version] = ::Regexp.last_match(1).strip
176
177
  elsif line =~ /^CVE: (.+)/ && current_gem
177
- current_gem[:advisory] = $1.strip
178
+ current_gem[:advisory] = ::Regexp.last_match(1).strip
178
179
  elsif line =~ /^Advisory: (.+)/ && current_gem
179
180
  # Fallback for older bundle-audit versions
180
- current_gem[:advisory] = $1.strip
181
+ current_gem[:advisory] = ::Regexp.last_match(1).strip
181
182
  elsif line =~ /^Criticality: (.+)/ && current_gem
182
- current_gem[:severity] = $1.strip
183
+ current_gem[:severity] = ::Regexp.last_match(1).strip
183
184
  elsif line =~ /^Title: (.+)/ && current_gem
184
- current_gem[:title] = $1.strip
185
+ current_gem[:title] = ::Regexp.last_match(1).strip
185
186
  # Only add if we have at least name, version, and title
186
- if current_gem[:gem] && current_gem[:version] && current_gem[:title]
187
- vulnerabilities << current_gem
188
- end
187
+ vulnerabilities << current_gem if current_gem[:gem] && current_gem[:version] && current_gem[:title]
189
188
  current_gem = nil
190
189
  end
191
190
  end
192
-
191
+
193
192
  # Handle case where vulnerability block ends without Title (use CVE as title)
194
193
  if current_gem && current_gem[:gem] && current_gem[:version]
195
- current_gem[:title] ||= current_gem[:advisory] || "Vulnerability detected"
194
+ current_gem[:title] ||= current_gem[:advisory] || 'Vulnerability detected'
196
195
  vulnerabilities << current_gem
197
196
  end
198
-
197
+
199
198
  @result.gem_vulnerabilities = vulnerabilities
200
199
  end
201
200
 
202
201
  def parse_bundle_outdated_output(output)
203
202
  versions = []
204
203
  lines_to_process = []
205
-
204
+
206
205
  # First pass: collect all lines to process
207
206
  output.each_line do |line|
208
207
  next if line.strip.empty?
209
-
208
+
210
209
  # Parse format: gem_name (newest version, installed version, requested version)
211
- if line =~ /^(.+?)\s+\(newest\s+(.+?),\s+installed\s+(.+?)(?:,|\))/
212
- lines_to_process << {
213
- gem_name: $1.strip,
214
- current_version: $3.strip,
215
- latest_version: $2.strip
216
- }
217
- end
210
+ next unless line =~ /^(.+?)\s+\(newest\s+(.+?),\s+installed\s+(.+?)(?:,|\))/
211
+
212
+ lines_to_process << {
213
+ gem_name: ::Regexp.last_match(1).strip,
214
+ current_version: ::Regexp.last_match(3).strip,
215
+ latest_version: ::Regexp.last_match(2).strip
216
+ }
218
217
  end
219
-
218
+
220
219
  total = lines_to_process.size
221
-
220
+
222
221
  # Process in parallel with threads (limit to 10 concurrent requests)
223
222
  mutex = Mutex.new
224
223
  thread_pool = []
225
224
  max_threads = 10
226
-
225
+
227
226
  lines_to_process.each_with_index do |line_data, index|
228
227
  # Wait if we have too many threads
229
- if thread_pool.size >= max_threads
230
- thread_pool.shift.join
231
- end
232
-
228
+ thread_pool.shift.join if thread_pool.size >= max_threads
229
+
233
230
  thread = Thread.new do
234
231
  # Fetch all version info once per gem (includes dates and version list)
235
232
  gem_data = fetch_gem_all_versions(line_data[:gem_name])
236
-
233
+
237
234
  # Extract dates for current and latest versions
238
235
  current_date = gem_data[:versions][line_data[:current_version]] || 'N/A'
239
236
  latest_date = gem_data[:versions][line_data[:latest_version]] || 'N/A'
240
-
237
+
241
238
  # Calculate time difference
242
239
  time_diff = calculate_time_difference(current_date, latest_date)
243
-
240
+
244
241
  # Count versions between current and latest
245
- version_count = count_versions_from_list(gem_data[:version_list], line_data[:current_version], line_data[:latest_version])
246
-
242
+ version_count = count_versions_from_list(gem_data[:version_list], line_data[:current_version],
243
+ line_data[:latest_version])
244
+
245
+ # Check if this is a direct dependency
246
+ direct_dependency = is_direct_gem?(line_data[:gem_name])
247
+
247
248
  result = {
248
249
  gem: line_data[:gem_name],
249
250
  current: line_data[:current_version],
@@ -252,38 +253,39 @@ module Rubion
252
253
  latest_date: latest_date,
253
254
  time_diff: time_diff,
254
255
  version_count: version_count,
256
+ direct: direct_dependency,
255
257
  index: index
256
258
  }
257
-
259
+
258
260
  mutex.synchronize do
259
261
  versions << result
260
262
  print "\r📦 Checking Ruby gems... #{versions.size}/#{total}"
261
263
  $stdout.flush
262
264
  end
263
265
  end
264
-
266
+
265
267
  thread_pool << thread
266
268
  end
267
-
269
+
268
270
  # Wait for all threads to complete
269
271
  thread_pool.each(&:join)
270
-
272
+
271
273
  # Sort by original index to maintain order
272
274
  versions.sort_by! { |v| v[:index] }
273
275
  versions.each { |v| v.delete(:index) }
274
-
276
+
275
277
  puts "\r📦 Checking Ruby gems... #{total}/#{total} ✓" if total > 0
276
-
278
+
277
279
  @result.gem_versions = versions
278
280
  end
279
281
 
280
282
  def parse_npm_audit_output(data)
281
283
  vulnerabilities = []
282
-
284
+
283
285
  if data['vulnerabilities'] && data['vulnerabilities'].is_a?(Hash)
284
286
  data['vulnerabilities'].each do |name, info|
285
287
  next unless info.is_a?(Hash)
286
-
288
+
287
289
  # Extract title from via array
288
290
  title = 'Vulnerability detected'
289
291
  if info['via'].is_a?(Array) && info['via'].first.is_a?(Hash)
@@ -291,7 +293,7 @@ module Rubion
291
293
  elsif info['via'].is_a?(String)
292
294
  title = info['via']
293
295
  end
294
-
296
+
295
297
  vulnerabilities << {
296
298
  package: name,
297
299
  version: info['range'] || info['version'] || 'unknown',
@@ -300,57 +302,59 @@ module Rubion
300
302
  }
301
303
  end
302
304
  end
303
-
305
+
304
306
  @result.package_vulnerabilities = vulnerabilities
305
- rescue => e
307
+ rescue StandardError => e
306
308
  puts " ⚠️ Error parsing npm audit data: #{e.message}"
307
309
  @result.package_vulnerabilities = []
308
310
  end
309
311
 
310
312
  def parse_npm_outdated_output(data)
311
313
  versions = []
312
-
314
+
313
315
  if data.is_a?(Hash)
314
316
  packages_to_process = []
315
-
317
+
316
318
  # First pass: collect all packages to process
317
319
  data.each do |name, info|
318
320
  next unless info.is_a?(Hash)
319
-
321
+
320
322
  packages_to_process << {
321
323
  name: name,
322
324
  current_version: info['current'] || 'unknown',
323
325
  latest_version: info['latest'] || 'unknown'
324
326
  }
325
327
  end
326
-
328
+
327
329
  total = packages_to_process.size
328
-
330
+
329
331
  # Process in parallel with threads (limit to 10 concurrent requests)
330
332
  mutex = Mutex.new
331
333
  thread_pool = []
332
334
  max_threads = 10
333
-
335
+
334
336
  packages_to_process.each_with_index do |pkg_data, index|
335
337
  # Wait if we have too many threads
336
- if thread_pool.size >= max_threads
337
- thread_pool.shift.join
338
- end
339
-
338
+ thread_pool.shift.join if thread_pool.size >= max_threads
339
+
340
340
  thread = Thread.new do
341
341
  # Fetch all version info once per package (includes dates and version list)
342
342
  pkg_data_full = fetch_npm_all_versions(pkg_data[:name])
343
-
343
+
344
344
  # Extract dates for current and latest versions
345
345
  current_date = pkg_data_full[:versions][pkg_data[:current_version]] || 'N/A'
346
346
  latest_date = pkg_data_full[:versions][pkg_data[:latest_version]] || 'N/A'
347
-
347
+
348
348
  # Calculate time difference
349
349
  time_diff = calculate_time_difference(current_date, latest_date)
350
-
350
+
351
351
  # Count versions between current and latest
352
- version_count = count_versions_from_list(pkg_data_full[:version_list], pkg_data[:current_version], pkg_data[:latest_version])
353
-
352
+ version_count = count_versions_from_list(pkg_data_full[:version_list], pkg_data[:current_version],
353
+ pkg_data[:latest_version])
354
+
355
+ # Check if this is a direct dependency
356
+ direct_dependency = is_direct_package?(pkg_data[:name])
357
+
354
358
  result = {
355
359
  package: pkg_data[:name],
356
360
  current: pkg_data[:current_version],
@@ -359,31 +363,32 @@ module Rubion
359
363
  latest_date: latest_date,
360
364
  time_diff: time_diff,
361
365
  version_count: version_count,
366
+ direct: direct_dependency,
362
367
  index: index
363
368
  }
364
-
369
+
365
370
  mutex.synchronize do
366
371
  versions << result
367
372
  print "\r📦 Checking NPM packages... #{versions.size}/#{total}"
368
373
  $stdout.flush
369
374
  end
370
375
  end
371
-
376
+
372
377
  thread_pool << thread
373
378
  end
374
-
379
+
375
380
  # Wait for all threads to complete
376
381
  thread_pool.each(&:join)
377
-
382
+
378
383
  # Sort by original index to maintain order
379
384
  versions.sort_by! { |v| v[:index] }
380
385
  versions.each { |v| v.delete(:index) }
381
-
386
+
382
387
  puts "\r📦 Checking NPM packages... #{total}/#{total} ✓" if total > 0
383
388
  end
384
-
389
+
385
390
  @result.package_versions = versions
386
- rescue => e
391
+ rescue StandardError => e
387
392
  puts " ⚠️ Error parsing npm outdated data: #{e.message}"
388
393
  @result.package_versions = []
389
394
  end
@@ -463,27 +468,27 @@ module Rubion
463
468
  # Fetch all gem version info (dates and version list) from RubyGems API in one call
464
469
  def fetch_gem_all_versions(gem_name)
465
470
  return { versions: {}, version_list: [] } if gem_name.nil?
466
-
471
+
467
472
  uri = URI("https://rubygems.org/api/v1/versions/#{gem_name}.json")
468
-
473
+
469
474
  # Set timeout to avoid hanging
470
475
  http = Net::HTTP.new(uri.host, uri.port)
471
476
  http.use_ssl = true
472
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
477
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
473
478
  http.open_timeout = 2
474
479
  http.read_timeout = 3
475
-
480
+
476
481
  request = Net::HTTP::Get.new(uri.request_uri)
477
482
  response = http.request(request)
478
-
483
+
479
484
  return { versions: {}, version_list: [] } unless response.is_a?(Net::HTTPSuccess)
480
-
485
+
481
486
  all_versions = JSON.parse(response.body)
482
-
487
+
483
488
  # Build hash of version => date
484
489
  version_dates = {}
485
490
  version_list = []
486
-
491
+
487
492
  all_versions.each do |v|
488
493
  version_num = v['number']
489
494
  if v['created_at']
@@ -494,9 +499,9 @@ module Rubion
494
499
  end
495
500
  version_list << version_num
496
501
  end
497
-
502
+
498
503
  { versions: version_dates, version_list: version_list }
499
- rescue => e
504
+ rescue StandardError => e
500
505
  puts " Debug: Error fetching versions for #{gem_name}: #{e.message}" if ENV['DEBUG']
501
506
  { versions: {}, version_list: [] }
502
507
  end
@@ -505,13 +510,13 @@ module Rubion
505
510
  def count_versions_from_list(version_list, current_version, latest_version)
506
511
  return 'N/A' if current_version == 'unknown' || latest_version == 'unknown' || version_list.empty?
507
512
  return 0 if current_version == latest_version
508
-
513
+
509
514
  # Find indices of current and latest versions
510
515
  current_idx = version_list.index(current_version)
511
516
  latest_idx = version_list.index(latest_version)
512
-
517
+
513
518
  return 'N/A' if current_idx.nil? || latest_idx.nil?
514
-
519
+
515
520
  # Count versions between (exclusive of current, inclusive of latest)
516
521
  (latest_idx - current_idx).abs
517
522
  end
@@ -519,33 +524,33 @@ module Rubion
519
524
  # Calculate time difference between two dates
520
525
  def calculate_time_difference(date1_str, date2_str)
521
526
  return 'N/A' if date1_str == 'N/A' || date2_str == 'N/A'
522
-
527
+
523
528
  begin
524
529
  # Parse dates - convert "8/13/2025" format to Date object
525
530
  # Split by / and create Date object
526
531
  parts1 = date1_str.split('/').map(&:to_i)
527
532
  parts2 = date2_str.split('/').map(&:to_i)
528
-
533
+
529
534
  date1 = Date.new(parts1[2], parts1[0], parts1[1])
530
535
  date2 = Date.new(parts2[2], parts2[0], parts2[1])
531
-
536
+
532
537
  # Always use the later date minus the earlier date
533
538
  diff = (date2 - date1).to_i.abs
534
-
539
+
535
540
  if diff < 30
536
- "#{diff} day#{diff != 1 ? 's' : ''}"
541
+ "#{diff} day#{'s' if diff != 1}"
537
542
  elsif diff < 365
538
543
  months = (diff / 30.0).round
539
- "#{months} month#{months != 1 ? 's' : ''}"
544
+ "#{months} month#{'s' if months != 1}"
540
545
  else
541
546
  years = (diff / 365.0).round(1)
542
547
  if years == years.to_i
543
- "#{years.to_i} year#{years.to_i != 1 ? 's' : ''}"
548
+ "#{years.to_i} year#{'s' if years.to_i != 1}"
544
549
  else
545
550
  "#{years} years"
546
551
  end
547
552
  end
548
- rescue => e
553
+ rescue StandardError => e
549
554
  puts " Debug: Error calculating time diff: #{e.message}" if ENV['DEBUG']
550
555
  'N/A'
551
556
  end
@@ -555,7 +560,7 @@ module Rubion
555
560
  def detect_package_manager
556
561
  npm_available = check_command_available('npm')
557
562
  yarn_available = check_command_available('yarn')
558
-
563
+
559
564
  if npm_available && yarn_available
560
565
  # Both available - prompt user
561
566
  prompt_package_manager_choice
@@ -567,28 +572,81 @@ module Rubion
567
572
  nil
568
573
  end
569
574
  end
570
-
575
+
571
576
  # Check if a command is available in the system
572
577
  def check_command_available(command)
573
578
  _, _, status = Open3.capture3("which #{command} 2>&1")
574
579
  status.success?
575
- rescue
580
+ rescue StandardError
576
581
  false
577
582
  end
578
-
583
+
584
+ # Parse Gemfile to get direct dependencies
585
+ def parse_gemfile
586
+ return @direct_gems if @direct_gems
587
+
588
+ gemfile_path = File.join(@project_path, 'Gemfile')
589
+ return [] unless File.exist?(gemfile_path)
590
+
591
+ direct_gems = []
592
+ content = File.read(gemfile_path)
593
+
594
+ # Match gem declarations: gem 'name', gem "name", gem('name'), gem("name")
595
+ # Also handle version constraints and options
596
+ content.scan(/^\s*gem\s+['"]([^'"]+)['"]/) do |match|
597
+ gem_name = match[0]
598
+ direct_gems << gem_name unless gem_name.nil?
599
+ end
600
+
601
+ @direct_gems = direct_gems.uniq
602
+ end
603
+
604
+ # Check if a gem is a direct dependency
605
+ def is_direct_gem?(gem_name)
606
+ parse_gemfile.include?(gem_name)
607
+ end
608
+
609
+ # Parse package.json to get direct dependencies
610
+ def parse_package_json
611
+ return @direct_packages if @direct_packages
612
+
613
+ package_json_path = File.join(@project_path, 'package.json')
614
+ return [] unless File.exist?(package_json_path)
615
+
616
+ begin
617
+ content = File.read(package_json_path)
618
+ data = JSON.parse(content)
619
+
620
+ # Get dependencies from dependencies, devDependencies, peerDependencies, optionalDependencies
621
+ direct_packages = []
622
+ %w[dependencies devDependencies peerDependencies optionalDependencies].each do |key|
623
+ direct_packages.concat(data[key].keys) if data[key].is_a?(Hash)
624
+ end
625
+
626
+ @direct_packages = direct_packages.uniq
627
+ rescue JSON::ParserError, Errno::ENOENT
628
+ @direct_packages = []
629
+ end
630
+ end
631
+
632
+ # Check if a package is a direct dependency
633
+ def is_direct_package?(package_name)
634
+ parse_package_json.include?(package_name)
635
+ end
636
+
579
637
  # Prompt user to choose between npm and yarn when both are available
580
638
  def prompt_package_manager_choice
581
639
  puts "\n Both npm and yarn are available. Which would you like to use?"
582
- print " Enter 'npm' or 'yarn' (default: npm): "
583
-
640
+ print " Enter 'n' for npm or 'y' for yarn (default: npm): "
641
+
584
642
  choice = $stdin.gets.chomp.strip.downcase
585
-
586
- if choice.empty? || choice == 'npm'
643
+
644
+ if choice.empty? || choice == 'n' || choice == 'npm'
587
645
  'npm'
588
- elsif choice == 'yarn'
646
+ elsif %w[y yarn].include?(choice)
589
647
  'yarn'
590
648
  else
591
- puts " ⚠️ Invalid choice. Using npm as default."
649
+ puts ' ⚠️ Invalid choice. Using npm as default.'
592
650
  'npm'
593
651
  end
594
652
  end
@@ -604,7 +662,7 @@ module Rubion
604
662
  # Set timeout to avoid hanging
605
663
  http = Net::HTTP.new(uri.host, uri.port)
606
664
  http.use_ssl = true
607
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
665
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
608
666
  http.open_timeout = 2
609
667
  http.read_timeout = 3
610
668
 
@@ -614,31 +672,28 @@ module Rubion
614
672
  return { versions: {}, version_list: [] } unless response.is_a?(Net::HTTPSuccess)
615
673
 
616
674
  data = JSON.parse(response.body)
617
-
675
+
618
676
  return { versions: {}, version_list: [] } unless data['time'] && data['versions']
619
-
677
+
620
678
  # Build hash of version => date and sorted version list
621
679
  version_dates = {}
622
680
  version_times = data['time'].select { |k, v| k != 'created' && k != 'modified' && data['versions'][k] }
623
-
681
+
624
682
  # Sort versions by release date
625
683
  sorted_versions = version_times.sort_by { |k, v| DateTime.parse(v) }.map(&:first)
626
-
684
+
627
685
  # Build date hash
628
686
  version_times.each do |version, date_str|
629
- begin
630
- date = DateTime.parse(date_str)
631
- version_dates[version] = date.strftime('%-m/%-d/%Y')
632
- rescue
633
- version_dates[version] = 'N/A'
634
- end
687
+ date = DateTime.parse(date_str)
688
+ version_dates[version] = date.strftime('%-m/%-d/%Y')
689
+ rescue StandardError
690
+ version_dates[version] = 'N/A'
635
691
  end
636
-
692
+
637
693
  { versions: version_dates, version_list: sorted_versions }
638
- rescue => e
694
+ rescue StandardError => e
639
695
  puts " Debug: Error fetching versions for #{package_name}: #{e.message}" if ENV['DEBUG']
640
696
  { versions: {}, version_list: [] }
641
697
  end
642
698
  end
643
699
  end
644
-
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubion
4
- VERSION = "0.3.3"
4
+ VERSION = "0.3.4"
5
5
  end
6
6
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubion
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - bipashant