stackharbinger 0.4.0 → 1.0.0

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.
data/lib/harbinger/cli.rb CHANGED
@@ -11,6 +11,10 @@ require "harbinger/analyzers/postgres_detector"
11
11
  require "harbinger/analyzers/mysql_detector"
12
12
  require "harbinger/analyzers/redis_detector"
13
13
  require "harbinger/analyzers/mongo_detector"
14
+ require "harbinger/analyzers/python_detector"
15
+ require "harbinger/analyzers/node_detector"
16
+ require "harbinger/analyzers/rust_detector"
17
+ require "harbinger/analyzers/go_detector"
14
18
  require "harbinger/eol_fetcher"
15
19
  require "harbinger/config_manager"
16
20
  require "harbinger/exporters/json_exporter"
@@ -22,6 +26,65 @@ module Harbinger
22
26
  true
23
27
  end
24
28
 
29
+ # Ecosystem priority for determining primary language
30
+ ECOSYSTEM_PRIORITY = %w[ruby python rust go nodejs].freeze
31
+
32
+ # Ecosystem definitions with languages and databases
33
+ ECOSYSTEMS = {
34
+ "ruby" => {
35
+ name: "Ruby Ecosystem",
36
+ languages: ["ruby", "rails"],
37
+ databases: ["postgres", "mysql", "redis", "mongo"]
38
+ },
39
+ "python" => {
40
+ name: "Python Ecosystem",
41
+ languages: ["python"],
42
+ databases: ["postgres", "mysql", "redis", "mongo"]
43
+ },
44
+ "rust" => {
45
+ name: "Rust Ecosystem",
46
+ languages: ["rust"],
47
+ databases: ["postgres", "mysql", "redis", "mongo"]
48
+ },
49
+ "go" => {
50
+ name: "Go Ecosystem",
51
+ languages: ["go"],
52
+ databases: ["postgres", "mysql", "redis", "mongo"]
53
+ },
54
+ "nodejs" => {
55
+ name: "Node.js Ecosystem",
56
+ languages: ["nodejs"],
57
+ databases: ["postgres", "mysql", "redis", "mongo"]
58
+ }
59
+ }.freeze
60
+
61
+ # Component display names for table headers
62
+ COMPONENT_DISPLAY_NAMES = {
63
+ "ruby" => "Ruby",
64
+ "rails" => "Rails",
65
+ "python" => "Python",
66
+ "nodejs" => "Node.js",
67
+ "rust" => "Rust",
68
+ "go" => "Go",
69
+ "postgres" => "PostgreSQL",
70
+ "mysql" => "MySQL",
71
+ "redis" => "Redis",
72
+ "mongo" => "MongoDB"
73
+ }.freeze
74
+
75
+ # Product name mapping for EOL API lookups
76
+ PRODUCT_NAME_MAP = {
77
+ "ruby" => "ruby",
78
+ "rails" => "rails",
79
+ "postgres" => "postgresql",
80
+ "mysql" => "mysql",
81
+ "redis" => "redis",
82
+ "mongo" => "mongodb",
83
+ "python" => "python",
84
+ "nodejs" => "nodejs",
85
+ "rust" => "rust"
86
+ }.freeze
87
+
25
88
  desc "scan", "Scan a project directory and detect versions"
26
89
  option :path, type: :string, aliases: "-p", desc: "Path to project directory (defaults to current directory)"
27
90
  option :save, type: :boolean, aliases: "-s", desc: "Save project to config for dashboard"
@@ -74,203 +137,34 @@ module Harbinger
74
137
  return
75
138
  end
76
139
 
140
+ # Group projects by ecosystem
77
141
  fetcher = EolFetcher.new
78
- rows = []
79
-
80
- # Track which columns have data
81
- has_ruby = false
82
- has_rails = false
83
- has_postgres = false
84
- has_mysql = false
85
- has_redis = false
86
- has_mongo = false
87
-
88
- projects.each do |name, data|
89
- ruby_version = data["ruby"]
90
- rails_version = data["rails"]
91
- postgres_version = data["postgres"]
92
- mysql_version = data["mysql"]
93
- redis_version = data["redis"]
94
- mongo_version = data["mongo"]
95
-
96
- # Filter out gem-only database versions
97
- postgres_version = nil if postgres_version&.include?("gem")
98
- mysql_version = nil if mysql_version&.include?("gem")
99
- redis_version = nil if redis_version&.include?("gem")
100
- mongo_version = nil if mongo_version&.include?("gem")
101
-
102
- # Skip projects with no matching products
103
- ruby_present = ruby_version && !ruby_version.empty?
104
- rails_present = rails_version && !rails_version.empty?
105
- postgres_present = postgres_version && !postgres_version.empty?
106
- mysql_present = mysql_version && !mysql_version.empty?
107
- redis_present = redis_version && !redis_version.empty?
108
- mongo_present = mongo_version && !mongo_version.empty?
109
-
110
- next unless ruby_present || rails_present || postgres_present || mysql_present || redis_present || mongo_present
111
-
112
- # Track which columns have data
113
- has_ruby ||= ruby_present
114
- has_rails ||= rails_present
115
- has_postgres ||= postgres_present
116
- has_mysql ||= mysql_present
117
- has_redis ||= redis_present
118
- has_mongo ||= mongo_present
119
-
120
- # Determine worst EOL status
121
- worst_status = :green
122
- status_text = "✓ Current"
123
-
124
- if ruby_present
125
- ruby_eol = fetcher.eol_date_for("ruby", ruby_version)
126
- if ruby_eol
127
- days = days_until(ruby_eol)
128
- status = eol_color(days)
129
- worst_status = status if status_priority(status) > status_priority(worst_status)
130
- if days.negative?
131
- status_text = "✗ Ruby EOL"
132
- elsif days < 180
133
- status_text = "⚠ Ruby ending soon"
134
- end
135
- end
136
- end
137
-
138
- if rails_present
139
- rails_eol = fetcher.eol_date_for("rails", rails_version)
140
- if rails_eol
141
- days = days_until(rails_eol)
142
- status = eol_color(days)
143
- worst_status = status if status_priority(status) > status_priority(worst_status)
144
- if days.negative?
145
- status_text = "✗ Rails EOL"
146
- elsif days < 180 && !status_text.include?("EOL")
147
- status_text = "⚠ Rails ending soon"
148
- end
149
- end
150
- end
151
-
152
- if postgres_present
153
- postgres_eol = fetcher.eol_date_for("postgresql", postgres_version)
154
- if postgres_eol
155
- days = days_until(postgres_eol)
156
- status = eol_color(days)
157
- worst_status = status if status_priority(status) > status_priority(worst_status)
158
- if days.negative?
159
- status_text = "✗ PostgreSQL EOL"
160
- elsif days < 180 && !status_text.include?("EOL")
161
- status_text = "⚠ PostgreSQL ending soon"
162
- end
163
- end
164
- end
165
-
166
- if mysql_present
167
- mysql_eol = fetcher.eol_date_for("mysql", mysql_version)
168
- if mysql_eol
169
- days = days_until(mysql_eol)
170
- status = eol_color(days)
171
- worst_status = status if status_priority(status) > status_priority(worst_status)
172
- if days.negative?
173
- status_text = "✗ MySQL EOL"
174
- elsif days < 180 && !status_text.include?("EOL")
175
- status_text = "⚠ MySQL ending soon"
176
- end
177
- end
178
- end
179
-
180
- if redis_present
181
- redis_eol = fetcher.eol_date_for("redis", redis_version)
182
- if redis_eol
183
- days = days_until(redis_eol)
184
- status = eol_color(days)
185
- worst_status = status if status_priority(status) > status_priority(worst_status)
186
- if days.negative?
187
- status_text = "✗ Redis EOL"
188
- elsif days < 180 && !status_text.include?("EOL")
189
- status_text = "⚠ Redis ending soon"
190
- end
191
- end
192
- end
193
-
194
- if mongo_present
195
- mongo_eol = fetcher.eol_date_for("mongodb", mongo_version)
196
- if mongo_eol
197
- days = days_until(mongo_eol)
198
- status = eol_color(days)
199
- worst_status = status if status_priority(status) > status_priority(worst_status)
200
- if days.negative?
201
- status_text = "✗ MongoDB EOL"
202
- elsif days < 180 && !status_text.include?("EOL")
203
- status_text = "⚠ MongoDB ending soon"
204
- end
205
- end
206
- end
142
+ ecosystem_projects = group_projects_by_ecosystem(projects)
207
143
 
208
- rows << {
209
- name: name,
210
- path: File.dirname(data["path"] || ""),
211
- ruby: ruby_present ? ruby_version : "-",
212
- rails: rails_present ? rails_version : "-",
213
- postgres: postgres_present ? postgres_version : "-",
214
- mysql: mysql_present ? mysql_version : "-",
215
- redis: redis_present ? redis_version : "-",
216
- mongo: mongo_present ? mongo_version : "-",
217
- status: colorize_status(status_text, worst_status),
218
- status_raw: status_text
219
- }
220
- end
221
-
222
- if rows.empty?
144
+ # Check if any projects have a programming language
145
+ if ecosystem_projects.empty?
223
146
  say "No projects with detected versions.", :yellow
224
147
  say "Use 'harbinger scan --save' to add projects", :cyan
225
148
  return
226
149
  end
227
150
 
228
- say "Tracked Projects (#{rows.size})", :cyan
229
- say "=" * 80, :cyan
230
-
231
- # Sort by status priority (worst first), then by name
232
- rows.sort_by! do |row|
233
- priority = if row[:status_raw].include?("✗")
234
- 0
235
- elsif row[:status_raw].include?("⚠")
236
- 1
237
- else
238
- 2
239
- end
240
- [priority, row[:name]]
241
- end
151
+ # Display header with total project count
152
+ total_projects = ecosystem_projects.values.sum(&:size)
153
+ say "Tracked Projects (#{total_projects})", :cyan
242
154
 
243
- # Build dynamic headers and rows
244
- headers = ["Project"]
245
- headers << "Path" if options[:verbose]
246
- headers << "Ruby" if has_ruby
247
- headers << "Rails" if has_rails
248
- headers << "PostgreSQL" if has_postgres
249
- headers << "MySQL" if has_mysql
250
- headers << "Redis" if has_redis
251
- headers << "MongoDB" if has_mongo
252
- headers << "Status"
155
+ # Render each ecosystem table
156
+ ECOSYSTEMS.keys.each do |ecosystem_key|
157
+ projects_in_ecosystem = ecosystem_projects[ecosystem_key]
158
+ next if projects_in_ecosystem.empty? # Hide empty tables
253
159
 
254
- table_rows = rows.map do |row|
255
- table_row = [row[:name]]
256
- table_row << row[:path] if options[:verbose]
257
- table_row << row[:ruby] if has_ruby
258
- table_row << row[:rails] if has_rails
259
- table_row << row[:postgres] if has_postgres
260
- table_row << row[:mysql] if has_mysql
261
- table_row << row[:redis] if has_redis
262
- table_row << row[:mongo] if has_mongo
263
- table_row << row[:status]
264
- table_row
160
+ render_ecosystem_table(
161
+ ecosystem_key,
162
+ projects_in_ecosystem,
163
+ fetcher,
164
+ verbose: options[:verbose]
165
+ )
265
166
  end
266
167
 
267
- table = TTY::Table.new(
268
- header: headers,
269
- rows: table_rows
270
- )
271
-
272
- puts table.render(:unicode, padding: [0, 1], resize: false)
273
-
274
168
  say "\nUse 'harbinger scan --path <project>' to update a project", :cyan
275
169
  end
276
170
 
@@ -279,7 +173,7 @@ module Harbinger
279
173
  say "Updating EOL data...", :cyan
280
174
 
281
175
  fetcher = EolFetcher.new
282
- products = %w[ruby rails postgresql mysql redis mongodb]
176
+ products = %w[ruby rails postgresql mysql redis mongodb python nodejs rust go]
283
177
 
284
178
  products.each do |product|
285
179
  say "Fetching #{product}...", :white
@@ -352,6 +246,10 @@ module Harbinger
352
246
  mysql_detector = Analyzers::MysqlDetector.new(project_path)
353
247
  redis_detector = Analyzers::RedisDetector.new(project_path)
354
248
  mongo_detector = Analyzers::MongoDetector.new(project_path)
249
+ python_detector = Analyzers::PythonDetector.new(project_path)
250
+ node_detector = Analyzers::NodeDetector.new(project_path)
251
+ rust_detector = Analyzers::RustDetector.new(project_path)
252
+ go_detector = Analyzers::GoDetector.new(project_path)
355
253
 
356
254
  ruby_version = ruby_detector.detect
357
255
  rails_version = rails_analyzer.detect
@@ -359,13 +257,27 @@ module Harbinger
359
257
  mysql_version = mysql_detector.detect
360
258
  redis_version = redis_detector.detect
361
259
  mongo_version = mongo_detector.detect
260
+ python_version = python_detector.detect
261
+ nodejs_version = node_detector.detect
262
+ rust_version = rust_detector.detect
263
+ go_version = go_detector.detect
362
264
 
363
265
  # Save to config
364
266
  config_manager.save_project(
365
267
  name: name,
366
268
  path: project_path,
367
- versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version,
368
- mysql: mysql_version, redis: redis_version, mongo: mongo_version }.compact
269
+ versions: {
270
+ ruby: ruby_version,
271
+ rails: rails_version,
272
+ postgres: postgres_version,
273
+ mysql: mysql_version,
274
+ redis: redis_version,
275
+ mongo: mongo_version,
276
+ python: python_version,
277
+ nodejs: nodejs_version,
278
+ rust: rust_version,
279
+ go: go_version
280
+ }.compact
369
281
  )
370
282
  end
371
283
 
@@ -384,6 +296,178 @@ module Harbinger
384
296
 
385
297
  private
386
298
 
299
+ # Calculate EOL status for a single component (e.g., ruby, rails, postgres)
300
+ # Returns: { status: :red/:yellow/:green, text: "✗ Ruby EOL", days: -30 } or nil
301
+ def calculate_component_status(component, version, fetcher)
302
+ return nil unless version && !version.empty?
303
+
304
+ product_name = PRODUCT_NAME_MAP[component]
305
+ eol_date = fetcher.eol_date_for(product_name, version)
306
+ return nil unless eol_date
307
+
308
+ days = days_until(eol_date)
309
+ status = eol_color(days)
310
+
311
+ component_display = COMPONENT_DISPLAY_NAMES[component] || component.capitalize
312
+
313
+ text = if days.negative?
314
+ "✗ #{component_display} EOL"
315
+ elsif days < 180
316
+ "⚠ #{component_display} ending soon"
317
+ else
318
+ "✓ Current"
319
+ end
320
+
321
+ { status: status, text: text, days: days }
322
+ end
323
+
324
+ # Determine overall status for a project across specified components
325
+ # Returns: { status: :red/:yellow/:green, text: "✗ Ruby EOL" }
326
+ def determine_overall_status(project_data, components, fetcher)
327
+ worst_status = :green
328
+ status_text = "✓ Current"
329
+
330
+ components.each do |component|
331
+ version = project_data[component]
332
+ next unless version && !version.empty?
333
+
334
+ # Filter out gem-only database versions
335
+ if %w[postgres mysql redis mongo].include?(component) && version&.include?("gem")
336
+ next
337
+ end
338
+
339
+ component_status = calculate_component_status(component, version, fetcher)
340
+ next unless component_status
341
+
342
+ if status_priority(component_status[:status]) > status_priority(worst_status)
343
+ worst_status = component_status[:status]
344
+ status_text = component_status[:text]
345
+ end
346
+ end
347
+
348
+ { status: worst_status, text: status_text }
349
+ end
350
+
351
+ # Build a row hash for a single project with all its component versions
352
+ # Returns: { name: "project", path: "/path", ruby: "3.2.0", ..., status: colored_text }
353
+ def build_project_row(name, data, components, fetcher, verbose: false)
354
+ row = { name: name }
355
+ row[:path] = File.dirname(data["path"] || "") if verbose
356
+
357
+ # Add component versions
358
+ components.each do |component|
359
+ version = data[component]
360
+ # Filter out gem-only database versions
361
+ if %w[postgres mysql redis mongo].include?(component) && version&.include?("gem")
362
+ version = nil
363
+ end
364
+ row[component.to_sym] = (version && !version.empty?) ? version : "-"
365
+ end
366
+
367
+ # Calculate overall status
368
+ status_info = determine_overall_status(data, components, fetcher)
369
+ row[:status] = colorize_status(status_info[:text], status_info[:status])
370
+ row[:status_raw] = status_info[:text]
371
+
372
+ row
373
+ end
374
+
375
+ # Determine the primary ecosystem for a project based on detected languages
376
+ # Returns: "ruby", "python", "rust", "go", "nodejs", or nil
377
+ def determine_primary_ecosystem(data)
378
+ ECOSYSTEM_PRIORITY.each do |lang|
379
+ version = data[lang]
380
+ return lang if version && !version.empty?
381
+ end
382
+ nil # No language detected
383
+ end
384
+
385
+ # Group projects by their primary ecosystem
386
+ # Returns: { "ruby" => [[name, data], ...], "python" => [[name, data], ...] }
387
+ def group_projects_by_ecosystem(projects)
388
+ ecosystem_projects = Hash.new { |h, k| h[k] = [] }
389
+
390
+ projects.each do |name, data|
391
+ primary = determine_primary_ecosystem(data)
392
+
393
+ # Skip projects with no programming language detected
394
+ next unless primary
395
+
396
+ ecosystem_projects[primary] << [name, data]
397
+ end
398
+
399
+ ecosystem_projects
400
+ end
401
+
402
+ # Render a table for a single ecosystem with its projects
403
+ def render_ecosystem_table(ecosystem_key, projects, fetcher, verbose: false)
404
+ config = ECOSYSTEMS[ecosystem_key]
405
+ components = config[:languages] + config[:databases]
406
+
407
+ # Track which columns have data in this ecosystem
408
+ has_columns = Hash.new(false)
409
+ rows = []
410
+
411
+ projects.each do |name, data|
412
+ row = build_project_row(name, data, components, fetcher, verbose: verbose)
413
+
414
+ # Track columns with data
415
+ components.each do |component|
416
+ has_columns[component] = true if row[component.to_sym] != "-"
417
+ end
418
+
419
+ rows << row
420
+ end
421
+
422
+ return if rows.empty? # Skip empty ecosystems
423
+
424
+ # Sort by status priority (worst first)
425
+ rows.sort_by! do |row|
426
+ priority = if row[:status_raw].include?("✗")
427
+ 0
428
+ elsif row[:status_raw].include?("⚠")
429
+ 1
430
+ else
431
+ 2
432
+ end
433
+ [priority, row[:name]]
434
+ end
435
+
436
+ # Build dynamic headers
437
+ headers = ["Project"]
438
+ headers << "Path" if verbose
439
+
440
+ components.each do |component|
441
+ next unless has_columns[component]
442
+
443
+ headers << COMPONENT_DISPLAY_NAMES[component]
444
+ end
445
+
446
+ headers << "Status"
447
+
448
+ # Build table rows matching headers
449
+ table_rows = rows.map do |row|
450
+ table_row = [row[:name]]
451
+ table_row << row[:path] if verbose
452
+
453
+ components.each do |component|
454
+ next unless has_columns[component]
455
+
456
+ table_row << row[component.to_sym]
457
+ end
458
+
459
+ table_row << row[:status]
460
+ table_row
461
+ end
462
+
463
+ # Render the table
464
+ say "\n#{config[:name]} (#{rows.size})", :cyan
465
+ say "=" * 80, :cyan
466
+
467
+ table = TTY::Table.new(header: headers, rows: table_rows)
468
+ puts table.render(:unicode, padding: [0, 1], resize: false)
469
+ end
470
+
387
471
  def scan_recursive(base_path)
388
472
  say "Scanning #{base_path} recursively for Ruby projects...", :cyan
389
473
 
@@ -433,6 +517,10 @@ module Harbinger
433
517
  mysql_detector = Analyzers::MysqlDetector.new(project_path)
434
518
  redis_detector = Analyzers::RedisDetector.new(project_path)
435
519
  mongo_detector = Analyzers::MongoDetector.new(project_path)
520
+ python_detector = Analyzers::PythonDetector.new(project_path)
521
+ node_detector = Analyzers::NodeDetector.new(project_path)
522
+ rust_detector = Analyzers::RustDetector.new(project_path)
523
+ go_detector = Analyzers::GoDetector.new(project_path)
436
524
 
437
525
  ruby_version = ruby_detector.detect
438
526
  rails_version = rails_analyzer.detect
@@ -440,80 +528,101 @@ module Harbinger
440
528
  mysql_version = mysql_detector.detect
441
529
  redis_version = redis_detector.detect
442
530
  mongo_version = mongo_detector.detect
531
+ python_version = python_detector.detect
532
+ nodejs_version = node_detector.detect
533
+ rust_version = rust_detector.detect
534
+ go_version = go_detector.detect
535
+
536
+ # Prepare data for ecosystem detection
537
+ data = {
538
+ "ruby" => ruby_version,
539
+ "rails" => rails_version,
540
+ "postgres" => postgres_version,
541
+ "mysql" => mysql_version,
542
+ "redis" => redis_version,
543
+ "mongo" => mongo_version,
544
+ "python" => python_version,
545
+ "nodejs" => nodejs_version,
546
+ "rust" => rust_version,
547
+ "go" => go_version
548
+ }
549
+
550
+ # Determine primary ecosystem
551
+ primary_ecosystem = determine_primary_ecosystem(data)
552
+
553
+ if primary_ecosystem.nil?
554
+ say "\nNo programming language detected", :yellow
555
+ say "This appears to be a database-only or infrastructure project", :yellow
556
+ return
557
+ end
443
558
 
444
- ruby_present = ruby_detector.ruby_detected?
445
- rails_present = rails_analyzer.rails_detected?
446
- postgres_present = postgres_detector.database_detected?
447
- mysql_present = mysql_detector.database_detected?
448
- redis_present = redis_detector.redis_detected?
449
- mongo_present = mongo_detector.mongo_detected?
559
+ # Get components to display for this ecosystem
560
+ config = ECOSYSTEMS[primary_ecosystem]
561
+ components_to_display = config[:languages] + config[:databases]
450
562
 
451
563
  # Display results
452
564
  say "\nDetected versions:", :green
453
- if ruby_version
454
- say " Ruby: #{ruby_version}", :white
455
- elsif ruby_present
456
- say " Ruby: Present (version not specified - add .ruby-version or ruby declaration in Gemfile)", :yellow
457
- else
458
- say " Ruby: Not a Ruby project", :red
459
- end
460
565
 
461
- if rails_version
462
- say " Rails: #{rails_version}", :white
463
- elsif rails_present
464
- say " Rails: Present (version not found in Gemfile.lock)", :yellow
465
- else
466
- say " Rails: Not detected", :yellow
467
- end
468
-
469
- if postgres_version
470
- say " PostgreSQL: #{postgres_version}", :white
471
- elsif postgres_present
472
- say " PostgreSQL: Present (version not detected)", :yellow
473
- end
474
-
475
- if mysql_version
476
- say " MySQL: #{mysql_version}", :white
477
- elsif mysql_present
478
- say " MySQL: Present (version not detected)", :yellow
566
+ components_to_display.each do |component|
567
+ version = data[component]
568
+ display_name = COMPONENT_DISPLAY_NAMES[component]
569
+ detector_present = case component
570
+ when "ruby" then ruby_detector.ruby_detected?
571
+ when "rails" then rails_analyzer.rails_detected?
572
+ when "postgres" then postgres_detector.database_detected?
573
+ when "mysql" then mysql_detector.database_detected?
574
+ when "redis" then redis_detector.redis_detected?
575
+ when "mongo" then mongo_detector.mongo_detected?
576
+ when "python" then python_detector.python_detected?
577
+ when "nodejs" then node_detector.nodejs_detected?
578
+ when "rust" then rust_detector.rust_detected?
579
+ when "go" then go_detector.go_detected?
580
+ else false
581
+ end
582
+
583
+ if version && !version.empty?
584
+ say " #{display_name.ljust(12)} #{version}", :white
585
+ elsif detector_present
586
+ say " #{display_name.ljust(12)} Present (version not detected)", :yellow
587
+ end
479
588
  end
480
589
 
481
- if redis_version
482
- say " Redis: #{redis_version}", :white
483
- elsif redis_present
484
- say " Redis: Present (version not detected)", :yellow
485
- end
590
+ # Fetch and display EOL dates
591
+ versions_to_check = components_to_display.filter_map do |component|
592
+ version = data[component]
593
+ next unless version && !version.empty?
594
+ # Filter out gem-only database versions
595
+ next if %w[postgres mysql redis mongo].include?(component) && version.include?("gem")
486
596
 
487
- if mongo_version
488
- say " MongoDB: #{mongo_version}", :white
489
- elsif mongo_present
490
- say " MongoDB: Present (version not detected)", :yellow
597
+ [component, version]
491
598
  end
492
599
 
493
- # Fetch and display EOL dates
494
- if ruby_version || rails_version || postgres_version || mysql_version || redis_version || mongo_version
600
+ if versions_to_check.any?
495
601
  say "\nFetching EOL data...", :cyan
496
602
  fetcher = EolFetcher.new
497
603
 
498
- display_eol_info(fetcher, "Ruby", ruby_version) if ruby_version
499
-
500
- display_eol_info(fetcher, "Rails", rails_version) if rails_version
501
-
502
- if postgres_version && !postgres_version.include?("gem")
503
- display_eol_info(fetcher, "PostgreSQL", postgres_version)
604
+ versions_to_check.each do |component, version|
605
+ display_name = COMPONENT_DISPLAY_NAMES[component]
606
+ display_eol_info(fetcher, display_name, version)
504
607
  end
505
-
506
- display_eol_info(fetcher, "MySQL", mysql_version) if mysql_version && !mysql_version.include?("gem")
507
-
508
- display_eol_info(fetcher, "Redis", redis_version) if redis_version && !redis_version.include?("gem")
509
-
510
- display_eol_info(fetcher, "MongoDB", mongo_version) if mongo_version && !mongo_version.include?("gem")
511
608
  end
512
609
 
513
610
  # Save to config if --save flag is used
611
+ versions = {
612
+ ruby: ruby_version,
613
+ rails: rails_version,
614
+ postgres: postgres_version,
615
+ mysql: mysql_version,
616
+ redis: redis_version,
617
+ mongo: mongo_version,
618
+ python: python_version,
619
+ nodejs: nodejs_version,
620
+ rust: rust_version,
621
+ go: go_version
622
+ }.compact
623
+
514
624
  if options[:save] && !options[:recursive]
515
- save_project_to_config(project_path, ruby_version, rails_version, postgres_version, mysql_version,
516
- redis_version, mongo_version)
625
+ save_project_to_config(project_path, versions)
517
626
  elsif options[:save] && options[:recursive]
518
627
  # In recursive mode, save without the confirmation message for each project
519
628
  config_manager = ConfigManager.new
@@ -521,22 +630,19 @@ module Harbinger
521
630
  config_manager.save_project(
522
631
  name: project_name,
523
632
  path: project_path,
524
- versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version,
525
- mysql: mysql_version, redis: redis_version, mongo: mongo_version }.compact
633
+ versions: versions
526
634
  )
527
635
  end
528
636
  end
529
637
 
530
- def save_project_to_config(project_path, ruby_version, rails_version, postgres_version, mysql_version,
531
- redis_version, mongo_version)
638
+ def save_project_to_config(project_path, versions)
532
639
  config_manager = ConfigManager.new
533
640
  project_name = File.basename(project_path)
534
641
 
535
642
  config_manager.save_project(
536
643
  name: project_name,
537
644
  path: project_path,
538
- versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version,
539
- mysql: mysql_version, redis: redis_version, mongo: mongo_version }.compact
645
+ versions: versions
540
646
  )
541
647
 
542
648
  say "\n✓ Saved to config as '#{project_name}'", :green
@@ -544,7 +650,14 @@ module Harbinger
544
650
  end
545
651
 
546
652
  def display_eol_info(fetcher, product, version)
547
- product_key = product.downcase
653
+ # Map display name to EOL API key
654
+ product_key = case product.downcase
655
+ when "node.js" then "nodejs"
656
+ when "postgresql" then "postgresql"
657
+ when "mongodb" then "mongodb"
658
+ else product.downcase
659
+ end
660
+
548
661
  eol_date = fetcher.eol_date_for(product_key, version)
549
662
 
550
663
  if eol_date