stackharbinger 0.4.0 → 1.0.1

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,66 @@ 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
+ "go" => "go"
87
+ }.freeze
88
+
25
89
  desc "scan", "Scan a project directory and detect versions"
26
90
  option :path, type: :string, aliases: "-p", desc: "Path to project directory (defaults to current directory)"
27
91
  option :save, type: :boolean, aliases: "-s", desc: "Save project to config for dashboard"
@@ -74,203 +138,34 @@ module Harbinger
74
138
  return
75
139
  end
76
140
 
141
+ # Group projects by ecosystem
77
142
  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
143
+ ecosystem_projects = group_projects_by_ecosystem(projects)
151
144
 
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
207
-
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?
145
+ # Check if any projects have a programming language
146
+ if ecosystem_projects.empty?
223
147
  say "No projects with detected versions.", :yellow
224
148
  say "Use 'harbinger scan --save' to add projects", :cyan
225
149
  return
226
150
  end
227
151
 
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
152
+ # Display header with total project count
153
+ total_projects = ecosystem_projects.values.sum(&:size)
154
+ say "Tracked Projects (#{total_projects})", :cyan
242
155
 
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"
156
+ # Render each ecosystem table
157
+ ECOSYSTEMS.keys.each do |ecosystem_key|
158
+ projects_in_ecosystem = ecosystem_projects[ecosystem_key]
159
+ next if projects_in_ecosystem.empty? # Hide empty tables
253
160
 
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
161
+ render_ecosystem_table(
162
+ ecosystem_key,
163
+ projects_in_ecosystem,
164
+ fetcher,
165
+ verbose: options[:verbose]
166
+ )
265
167
  end
266
168
 
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
169
  say "\nUse 'harbinger scan --path <project>' to update a project", :cyan
275
170
  end
276
171
 
@@ -279,7 +174,7 @@ module Harbinger
279
174
  say "Updating EOL data...", :cyan
280
175
 
281
176
  fetcher = EolFetcher.new
282
- products = %w[ruby rails postgresql mysql redis mongodb]
177
+ products = %w[ruby rails postgresql mysql redis mongodb python nodejs rust go]
283
178
 
284
179
  products.each do |product|
285
180
  say "Fetching #{product}...", :white
@@ -352,6 +247,10 @@ module Harbinger
352
247
  mysql_detector = Analyzers::MysqlDetector.new(project_path)
353
248
  redis_detector = Analyzers::RedisDetector.new(project_path)
354
249
  mongo_detector = Analyzers::MongoDetector.new(project_path)
250
+ python_detector = Analyzers::PythonDetector.new(project_path)
251
+ node_detector = Analyzers::NodeDetector.new(project_path)
252
+ rust_detector = Analyzers::RustDetector.new(project_path)
253
+ go_detector = Analyzers::GoDetector.new(project_path)
355
254
 
356
255
  ruby_version = ruby_detector.detect
357
256
  rails_version = rails_analyzer.detect
@@ -359,13 +258,27 @@ module Harbinger
359
258
  mysql_version = mysql_detector.detect
360
259
  redis_version = redis_detector.detect
361
260
  mongo_version = mongo_detector.detect
261
+ python_version = python_detector.detect
262
+ nodejs_version = node_detector.detect
263
+ rust_version = rust_detector.detect
264
+ go_version = go_detector.detect
362
265
 
363
266
  # Save to config
364
267
  config_manager.save_project(
365
268
  name: name,
366
269
  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
270
+ versions: {
271
+ ruby: ruby_version,
272
+ rails: rails_version,
273
+ postgres: postgres_version,
274
+ mysql: mysql_version,
275
+ redis: redis_version,
276
+ mongo: mongo_version,
277
+ python: python_version,
278
+ nodejs: nodejs_version,
279
+ rust: rust_version,
280
+ go: go_version
281
+ }.compact
369
282
  )
370
283
  end
371
284
 
@@ -384,6 +297,184 @@ module Harbinger
384
297
 
385
298
  private
386
299
 
300
+ # Calculate EOL status for a single component (e.g., ruby, rails, postgres)
301
+ # Returns: { status: :red/:yellow/:green, text: "✗ Ruby EOL", days: -30 } or nil
302
+ def calculate_component_status(component, version, fetcher)
303
+ return nil unless version && !version.empty?
304
+
305
+ product_name = PRODUCT_NAME_MAP[component]
306
+ eol_date = fetcher.eol_date_for(product_name, version)
307
+ return nil if eol_date.nil?
308
+
309
+ # Handle actively supported versions (eol = false)
310
+ if eol_date == false
311
+ component_display = COMPONENT_DISPLAY_NAMES[component] || component.capitalize
312
+ return { status: :green, text: "✓ Current", days: Float::INFINITY }
313
+ end
314
+
315
+ days = days_until(eol_date)
316
+ status = eol_color(days)
317
+
318
+ component_display = COMPONENT_DISPLAY_NAMES[component] || component.capitalize
319
+
320
+ text = if days.negative?
321
+ "✗ #{component_display} EOL"
322
+ elsif days < 180
323
+ "⚠ #{component_display} ending soon"
324
+ else
325
+ "✓ Current"
326
+ end
327
+
328
+ { status: status, text: text, days: days }
329
+ end
330
+
331
+ # Determine overall status for a project across specified components
332
+ # Returns: { status: :red/:yellow/:green, text: "✗ Ruby EOL" }
333
+ def determine_overall_status(project_data, components, fetcher)
334
+ worst_status = :green
335
+ status_text = "✓ Current"
336
+
337
+ components.each do |component|
338
+ version = project_data[component]
339
+ next unless version && !version.empty?
340
+
341
+ # Filter out gem-only database versions
342
+ if %w[postgres mysql redis mongo].include?(component) && version&.include?("gem")
343
+ next
344
+ end
345
+
346
+ component_status = calculate_component_status(component, version, fetcher)
347
+ next unless component_status
348
+
349
+ if status_priority(component_status[:status]) > status_priority(worst_status)
350
+ worst_status = component_status[:status]
351
+ status_text = component_status[:text]
352
+ end
353
+ end
354
+
355
+ { status: worst_status, text: status_text }
356
+ end
357
+
358
+ # Build a row hash for a single project with all its component versions
359
+ # Returns: { name: "project", path: "/path", ruby: "3.2.0", ..., status: colored_text }
360
+ def build_project_row(name, data, components, fetcher, verbose: false)
361
+ row = { name: name }
362
+ row[:path] = File.dirname(data["path"] || "") if verbose
363
+
364
+ # Add component versions
365
+ components.each do |component|
366
+ version = data[component]
367
+ # Filter out gem-only database versions
368
+ if %w[postgres mysql redis mongo].include?(component) && version&.include?("gem")
369
+ version = nil
370
+ end
371
+ row[component.to_sym] = (version && !version.empty?) ? version : "-"
372
+ end
373
+
374
+ # Calculate overall status
375
+ status_info = determine_overall_status(data, components, fetcher)
376
+ row[:status] = colorize_status(status_info[:text], status_info[:status])
377
+ row[:status_raw] = status_info[:text]
378
+
379
+ row
380
+ end
381
+
382
+ # Determine the primary ecosystem for a project based on detected languages
383
+ # Returns: "ruby", "python", "rust", "go", "nodejs", or nil
384
+ def determine_primary_ecosystem(data)
385
+ ECOSYSTEM_PRIORITY.each do |lang|
386
+ version = data[lang]
387
+ return lang if version && !version.empty?
388
+ end
389
+ nil # No language detected
390
+ end
391
+
392
+ # Group projects by their primary ecosystem
393
+ # Returns: { "ruby" => [[name, data], ...], "python" => [[name, data], ...] }
394
+ def group_projects_by_ecosystem(projects)
395
+ ecosystem_projects = Hash.new { |h, k| h[k] = [] }
396
+
397
+ projects.each do |name, data|
398
+ primary = determine_primary_ecosystem(data)
399
+
400
+ # Skip projects with no programming language detected
401
+ next unless primary
402
+
403
+ ecosystem_projects[primary] << [name, data]
404
+ end
405
+
406
+ ecosystem_projects
407
+ end
408
+
409
+ # Render a table for a single ecosystem with its projects
410
+ def render_ecosystem_table(ecosystem_key, projects, fetcher, verbose: false)
411
+ config = ECOSYSTEMS[ecosystem_key]
412
+ components = config[:languages] + config[:databases]
413
+
414
+ # Track which columns have data in this ecosystem
415
+ has_columns = Hash.new(false)
416
+ rows = []
417
+
418
+ projects.each do |name, data|
419
+ row = build_project_row(name, data, components, fetcher, verbose: verbose)
420
+
421
+ # Track columns with data
422
+ components.each do |component|
423
+ has_columns[component] = true if row[component.to_sym] != "-"
424
+ end
425
+
426
+ rows << row
427
+ end
428
+
429
+ return if rows.empty? # Skip empty ecosystems
430
+
431
+ # Sort by status priority (worst first)
432
+ rows.sort_by! do |row|
433
+ priority = if row[:status_raw].include?("✗")
434
+ 0
435
+ elsif row[:status_raw].include?("⚠")
436
+ 1
437
+ else
438
+ 2
439
+ end
440
+ [priority, row[:name]]
441
+ end
442
+
443
+ # Build dynamic headers
444
+ headers = ["Project"]
445
+ headers << "Path" if verbose
446
+
447
+ components.each do |component|
448
+ next unless has_columns[component]
449
+
450
+ headers << COMPONENT_DISPLAY_NAMES[component]
451
+ end
452
+
453
+ headers << "Status"
454
+
455
+ # Build table rows matching headers
456
+ table_rows = rows.map do |row|
457
+ table_row = [row[:name]]
458
+ table_row << row[:path] if verbose
459
+
460
+ components.each do |component|
461
+ next unless has_columns[component]
462
+
463
+ table_row << row[component.to_sym]
464
+ end
465
+
466
+ table_row << row[:status]
467
+ table_row
468
+ end
469
+
470
+ # Render the table
471
+ say "\n#{config[:name]} (#{rows.size})", :cyan
472
+ say "=" * 80, :cyan
473
+
474
+ table = TTY::Table.new(header: headers, rows: table_rows)
475
+ puts table.render(:unicode, padding: [0, 1], resize: false)
476
+ end
477
+
387
478
  def scan_recursive(base_path)
388
479
  say "Scanning #{base_path} recursively for Ruby projects...", :cyan
389
480
 
@@ -433,6 +524,10 @@ module Harbinger
433
524
  mysql_detector = Analyzers::MysqlDetector.new(project_path)
434
525
  redis_detector = Analyzers::RedisDetector.new(project_path)
435
526
  mongo_detector = Analyzers::MongoDetector.new(project_path)
527
+ python_detector = Analyzers::PythonDetector.new(project_path)
528
+ node_detector = Analyzers::NodeDetector.new(project_path)
529
+ rust_detector = Analyzers::RustDetector.new(project_path)
530
+ go_detector = Analyzers::GoDetector.new(project_path)
436
531
 
437
532
  ruby_version = ruby_detector.detect
438
533
  rails_version = rails_analyzer.detect
@@ -440,80 +535,101 @@ module Harbinger
440
535
  mysql_version = mysql_detector.detect
441
536
  redis_version = redis_detector.detect
442
537
  mongo_version = mongo_detector.detect
538
+ python_version = python_detector.detect
539
+ nodejs_version = node_detector.detect
540
+ rust_version = rust_detector.detect
541
+ go_version = go_detector.detect
542
+
543
+ # Prepare data for ecosystem detection
544
+ data = {
545
+ "ruby" => ruby_version,
546
+ "rails" => rails_version,
547
+ "postgres" => postgres_version,
548
+ "mysql" => mysql_version,
549
+ "redis" => redis_version,
550
+ "mongo" => mongo_version,
551
+ "python" => python_version,
552
+ "nodejs" => nodejs_version,
553
+ "rust" => rust_version,
554
+ "go" => go_version
555
+ }
556
+
557
+ # Determine primary ecosystem
558
+ primary_ecosystem = determine_primary_ecosystem(data)
559
+
560
+ if primary_ecosystem.nil?
561
+ say "\nNo programming language detected", :yellow
562
+ say "This appears to be a database-only or infrastructure project", :yellow
563
+ return
564
+ end
443
565
 
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?
566
+ # Get components to display for this ecosystem
567
+ config = ECOSYSTEMS[primary_ecosystem]
568
+ components_to_display = config[:languages] + config[:databases]
450
569
 
451
570
  # Display results
452
571
  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
-
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
572
 
475
- if mysql_version
476
- say " MySQL: #{mysql_version}", :white
477
- elsif mysql_present
478
- say " MySQL: Present (version not detected)", :yellow
573
+ components_to_display.each do |component|
574
+ version = data[component]
575
+ display_name = COMPONENT_DISPLAY_NAMES[component]
576
+ detector_present = case component
577
+ when "ruby" then ruby_detector.ruby_detected?
578
+ when "rails" then rails_analyzer.rails_detected?
579
+ when "postgres" then postgres_detector.database_detected?
580
+ when "mysql" then mysql_detector.database_detected?
581
+ when "redis" then redis_detector.redis_detected?
582
+ when "mongo" then mongo_detector.mongo_detected?
583
+ when "python" then python_detector.python_detected?
584
+ when "nodejs" then node_detector.nodejs_detected?
585
+ when "rust" then rust_detector.rust_detected?
586
+ when "go" then go_detector.go_detected?
587
+ else false
588
+ end
589
+
590
+ if version && !version.empty?
591
+ say " #{display_name.ljust(12)} #{version}", :white
592
+ elsif detector_present
593
+ say " #{display_name.ljust(12)} Present (version not detected)", :yellow
594
+ end
479
595
  end
480
596
 
481
- if redis_version
482
- say " Redis: #{redis_version}", :white
483
- elsif redis_present
484
- say " Redis: Present (version not detected)", :yellow
485
- end
597
+ # Fetch and display EOL dates
598
+ versions_to_check = components_to_display.filter_map do |component|
599
+ version = data[component]
600
+ next unless version && !version.empty?
601
+ # Filter out gem-only database versions
602
+ next if %w[postgres mysql redis mongo].include?(component) && version.include?("gem")
486
603
 
487
- if mongo_version
488
- say " MongoDB: #{mongo_version}", :white
489
- elsif mongo_present
490
- say " MongoDB: Present (version not detected)", :yellow
604
+ [component, version]
491
605
  end
492
606
 
493
- # Fetch and display EOL dates
494
- if ruby_version || rails_version || postgres_version || mysql_version || redis_version || mongo_version
607
+ if versions_to_check.any?
495
608
  say "\nFetching EOL data...", :cyan
496
609
  fetcher = EolFetcher.new
497
610
 
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)
611
+ versions_to_check.each do |component, version|
612
+ display_name = COMPONENT_DISPLAY_NAMES[component]
613
+ display_eol_info(fetcher, display_name, version)
504
614
  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
615
  end
512
616
 
513
617
  # Save to config if --save flag is used
618
+ versions = {
619
+ ruby: ruby_version,
620
+ rails: rails_version,
621
+ postgres: postgres_version,
622
+ mysql: mysql_version,
623
+ redis: redis_version,
624
+ mongo: mongo_version,
625
+ python: python_version,
626
+ nodejs: nodejs_version,
627
+ rust: rust_version,
628
+ go: go_version
629
+ }.compact
630
+
514
631
  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)
632
+ save_project_to_config(project_path, versions)
517
633
  elsif options[:save] && options[:recursive]
518
634
  # In recursive mode, save without the confirmation message for each project
519
635
  config_manager = ConfigManager.new
@@ -521,22 +637,19 @@ module Harbinger
521
637
  config_manager.save_project(
522
638
  name: project_name,
523
639
  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
640
+ versions: versions
526
641
  )
527
642
  end
528
643
  end
529
644
 
530
- def save_project_to_config(project_path, ruby_version, rails_version, postgres_version, mysql_version,
531
- redis_version, mongo_version)
645
+ def save_project_to_config(project_path, versions)
532
646
  config_manager = ConfigManager.new
533
647
  project_name = File.basename(project_path)
534
648
 
535
649
  config_manager.save_project(
536
650
  name: project_name,
537
651
  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
652
+ versions: versions
540
653
  )
541
654
 
542
655
  say "\n✓ Saved to config as '#{project_name}'", :green
@@ -544,19 +657,30 @@ module Harbinger
544
657
  end
545
658
 
546
659
  def display_eol_info(fetcher, product, version)
547
- product_key = product.downcase
660
+ # Map display name to EOL API key
661
+ product_key = case product.downcase
662
+ when "node.js" then "nodejs"
663
+ when "postgresql" then "postgresql"
664
+ when "mongodb" then "mongodb"
665
+ else product.downcase
666
+ end
667
+
548
668
  eol_date = fetcher.eol_date_for(product_key, version)
549
669
 
550
- if eol_date
670
+ if eol_date.nil?
671
+ say "\n#{product} #{version}:", :white
672
+ say " EOL Date: Unknown (version not found in database)", :yellow
673
+ elsif eol_date == false
674
+ say "\n#{product} #{version}:", :white
675
+ say " EOL Date: N/A (currently supported)", :green
676
+ say " Status: Active support", :green
677
+ else
551
678
  days_until_eol = days_until(eol_date)
552
679
  color = eol_color(days_until_eol)
553
680
 
554
681
  say "\n#{product} #{version}:", :white
555
682
  say " EOL Date: #{eol_date}", color
556
683
  say " Status: #{eol_status(days_until_eol)}", color
557
- else
558
- say "\n#{product} #{version}:", :white
559
- say " EOL Date: Unknown (version not found in database)", :yellow
560
684
  end
561
685
  end
562
686