stackharbinger 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +101 -25
- data/docs/index.html +38 -16
- data/lib/harbinger/analyzers/database_detector.rb +133 -0
- data/lib/harbinger/analyzers/docker_compose_detector.rb +121 -0
- data/lib/harbinger/analyzers/mongo_detector.rb +104 -0
- data/lib/harbinger/analyzers/mysql_detector.rb +90 -0
- data/lib/harbinger/analyzers/postgres_detector.rb +71 -0
- data/lib/harbinger/analyzers/redis_detector.rb +98 -0
- data/lib/harbinger/analyzers/ruby_detector.rb +9 -1
- data/lib/harbinger/cli.rb +362 -48
- data/lib/harbinger/eol_fetcher.rb +22 -8
- data/lib/harbinger/exporters/base_exporter.rb +97 -0
- data/lib/harbinger/exporters/csv_exporter.rb +36 -0
- data/lib/harbinger/exporters/json_exporter.rb +21 -0
- data/lib/harbinger/version.rb +1 -1
- data/lib/harbinger.rb +3 -0
- metadata +15 -3
data/lib/harbinger/cli.rb
CHANGED
|
@@ -6,8 +6,15 @@ require "tty-table"
|
|
|
6
6
|
require_relative "version"
|
|
7
7
|
require "harbinger/analyzers/ruby_detector"
|
|
8
8
|
require "harbinger/analyzers/rails_analyzer"
|
|
9
|
+
require "harbinger/analyzers/database_detector"
|
|
10
|
+
require "harbinger/analyzers/postgres_detector"
|
|
11
|
+
require "harbinger/analyzers/mysql_detector"
|
|
12
|
+
require "harbinger/analyzers/redis_detector"
|
|
13
|
+
require "harbinger/analyzers/mongo_detector"
|
|
9
14
|
require "harbinger/eol_fetcher"
|
|
10
15
|
require "harbinger/config_manager"
|
|
16
|
+
require "harbinger/exporters/json_exporter"
|
|
17
|
+
require "harbinger/exporters/csv_exporter"
|
|
11
18
|
|
|
12
19
|
module Harbinger
|
|
13
20
|
class CLI < Thor
|
|
@@ -20,7 +27,7 @@ module Harbinger
|
|
|
20
27
|
option :save, type: :boolean, aliases: "-s", desc: "Save project to config for dashboard"
|
|
21
28
|
option :recursive, type: :boolean, aliases: "-r", desc: "Recursively scan all subdirectories with Gemfiles"
|
|
22
29
|
def scan
|
|
23
|
-
project_path = options[:path] || Dir.pwd
|
|
30
|
+
project_path = File.expand_path(options[:path] || Dir.pwd)
|
|
24
31
|
|
|
25
32
|
unless File.directory?(project_path)
|
|
26
33
|
say "Error: #{project_path} is not a valid directory", :red
|
|
@@ -34,8 +41,11 @@ module Harbinger
|
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
43
|
|
|
37
|
-
desc "show", "Show EOL status for tracked projects"
|
|
38
|
-
|
|
44
|
+
desc "show [PROJECT]", "Show EOL status for tracked projects"
|
|
45
|
+
option :verbose, type: :boolean, aliases: "-v", desc: "Show project paths"
|
|
46
|
+
option :format, type: :string, enum: %w[table json csv], default: "table", desc: "Output format (table, json, csv)"
|
|
47
|
+
option :output, type: :string, aliases: "-o", desc: "Output file path"
|
|
48
|
+
def show(project_filter = nil)
|
|
39
49
|
config_manager = ConfigManager.new
|
|
40
50
|
projects = config_manager.list_projects
|
|
41
51
|
|
|
@@ -45,27 +55,79 @@ module Harbinger
|
|
|
45
55
|
return
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
# Filter by project name or path if specified
|
|
59
|
+
if project_filter
|
|
60
|
+
projects = projects.select do |name, data|
|
|
61
|
+
name.downcase.include?(project_filter.downcase) ||
|
|
62
|
+
data["path"]&.downcase&.include?(project_filter.downcase)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if projects.empty?
|
|
66
|
+
say "No projects matching '#{project_filter}'", :yellow
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Handle export formats
|
|
72
|
+
if options[:format] != "table"
|
|
73
|
+
export_data(projects, options[:format], options[:output])
|
|
74
|
+
return
|
|
75
|
+
end
|
|
50
76
|
|
|
51
77
|
fetcher = EolFetcher.new
|
|
52
78
|
rows = []
|
|
53
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
|
+
|
|
54
88
|
projects.each do |name, data|
|
|
55
89
|
ruby_version = data["ruby"]
|
|
56
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
|
|
57
119
|
|
|
58
120
|
# Determine worst EOL status
|
|
59
121
|
worst_status = :green
|
|
60
122
|
status_text = "✓ Current"
|
|
61
123
|
|
|
62
|
-
if
|
|
124
|
+
if ruby_present
|
|
63
125
|
ruby_eol = fetcher.eol_date_for("ruby", ruby_version)
|
|
64
126
|
if ruby_eol
|
|
65
127
|
days = days_until(ruby_eol)
|
|
66
128
|
status = eol_color(days)
|
|
67
129
|
worst_status = status if status_priority(status) > status_priority(worst_status)
|
|
68
|
-
if days
|
|
130
|
+
if days.negative?
|
|
69
131
|
status_text = "✗ Ruby EOL"
|
|
70
132
|
elsif days < 180
|
|
71
133
|
status_text = "⚠ Ruby ending soon"
|
|
@@ -73,13 +135,13 @@ module Harbinger
|
|
|
73
135
|
end
|
|
74
136
|
end
|
|
75
137
|
|
|
76
|
-
if
|
|
138
|
+
if rails_present
|
|
77
139
|
rails_eol = fetcher.eol_date_for("rails", rails_version)
|
|
78
140
|
if rails_eol
|
|
79
141
|
days = days_until(rails_eol)
|
|
80
142
|
status = eol_color(days)
|
|
81
143
|
worst_status = status if status_priority(status) > status_priority(worst_status)
|
|
82
|
-
if days
|
|
144
|
+
if days.negative?
|
|
83
145
|
status_text = "✗ Rails EOL"
|
|
84
146
|
elsif days < 180 && !status_text.include?("EOL")
|
|
85
147
|
status_text = "⚠ Rails ending soon"
|
|
@@ -87,31 +149,127 @@ module Harbinger
|
|
|
87
149
|
end
|
|
88
150
|
end
|
|
89
151
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
92
221
|
|
|
93
|
-
|
|
222
|
+
if rows.empty?
|
|
223
|
+
say "No projects with detected versions.", :yellow
|
|
224
|
+
say "Use 'harbinger scan --save' to add projects", :cyan
|
|
225
|
+
return
|
|
94
226
|
end
|
|
95
227
|
|
|
228
|
+
say "Tracked Projects (#{rows.size})", :cyan
|
|
229
|
+
say "=" * 80, :cyan
|
|
230
|
+
|
|
96
231
|
# Sort by status priority (worst first), then by name
|
|
97
232
|
rows.sort_by! do |row|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
242
|
+
|
|
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"
|
|
253
|
+
|
|
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
|
|
107
265
|
end
|
|
108
266
|
|
|
109
267
|
table = TTY::Table.new(
|
|
110
|
-
header:
|
|
111
|
-
rows:
|
|
268
|
+
header: headers,
|
|
269
|
+
rows: table_rows
|
|
112
270
|
)
|
|
113
271
|
|
|
114
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
272
|
+
puts table.render(:unicode, padding: [0, 1], resize: false)
|
|
115
273
|
|
|
116
274
|
say "\nUse 'harbinger scan --path <project>' to update a project", :cyan
|
|
117
275
|
end
|
|
@@ -121,7 +279,7 @@ module Harbinger
|
|
|
121
279
|
say "Updating EOL data...", :cyan
|
|
122
280
|
|
|
123
281
|
fetcher = EolFetcher.new
|
|
124
|
-
products = %w[ruby rails]
|
|
282
|
+
products = %w[ruby rails postgresql mysql redis mongodb]
|
|
125
283
|
|
|
126
284
|
products.each do |product|
|
|
127
285
|
say "Fetching #{product}...", :white
|
|
@@ -137,6 +295,88 @@ module Harbinger
|
|
|
137
295
|
say "\nEOL data updated successfully!", :green
|
|
138
296
|
end
|
|
139
297
|
|
|
298
|
+
desc "remove PROJECT", "Remove a project from tracking"
|
|
299
|
+
def remove(project_name)
|
|
300
|
+
config_manager = ConfigManager.new
|
|
301
|
+
project = config_manager.get_project(project_name)
|
|
302
|
+
|
|
303
|
+
if project
|
|
304
|
+
config_manager.remove_project(project_name)
|
|
305
|
+
say "Removed '#{project_name}' (#{project["path"]})", :green
|
|
306
|
+
else
|
|
307
|
+
say "Project '#{project_name}' not found", :yellow
|
|
308
|
+
say "\nTracked projects:", :cyan
|
|
309
|
+
config_manager.list_projects.keys.sort.each { |name| say " #{name}" }
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
desc "rescan", "Re-scan all tracked projects and update versions"
|
|
314
|
+
option :verbose, type: :boolean, aliases: "-v", desc: "Show detailed output for each project"
|
|
315
|
+
def rescan
|
|
316
|
+
config_manager = ConfigManager.new
|
|
317
|
+
projects = config_manager.list_projects
|
|
318
|
+
|
|
319
|
+
if projects.empty?
|
|
320
|
+
say "No projects tracked yet.", :yellow
|
|
321
|
+
say "Use 'harbinger scan --save' to add projects", :cyan
|
|
322
|
+
return
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
say "Re-scanning #{projects.size} tracked project(s)...\n\n", :cyan
|
|
326
|
+
|
|
327
|
+
updated_count = 0
|
|
328
|
+
removed_count = 0
|
|
329
|
+
|
|
330
|
+
projects.each_with_index do |(name, data), index|
|
|
331
|
+
project_path = data["path"]
|
|
332
|
+
|
|
333
|
+
unless File.directory?(project_path)
|
|
334
|
+
say "[#{index + 1}/#{projects.size}] #{name}: Path not found, removing from config", :yellow
|
|
335
|
+
config_manager.remove_project(name)
|
|
336
|
+
removed_count += 1
|
|
337
|
+
next
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
if options[:verbose]
|
|
341
|
+
say "=" * 60, :cyan
|
|
342
|
+
say "[#{index + 1}/#{projects.size}] Re-scanning #{name}", :cyan
|
|
343
|
+
say "=" * 60, :cyan
|
|
344
|
+
scan_single(project_path)
|
|
345
|
+
else
|
|
346
|
+
say "[#{index + 1}/#{projects.size}] #{name}...", :white
|
|
347
|
+
|
|
348
|
+
# Detect versions quietly
|
|
349
|
+
ruby_detector = Analyzers::RubyDetector.new(project_path)
|
|
350
|
+
rails_analyzer = Analyzers::RailsAnalyzer.new(project_path)
|
|
351
|
+
postgres_detector = Analyzers::PostgresDetector.new(project_path)
|
|
352
|
+
mysql_detector = Analyzers::MysqlDetector.new(project_path)
|
|
353
|
+
redis_detector = Analyzers::RedisDetector.new(project_path)
|
|
354
|
+
mongo_detector = Analyzers::MongoDetector.new(project_path)
|
|
355
|
+
|
|
356
|
+
ruby_version = ruby_detector.detect
|
|
357
|
+
rails_version = rails_analyzer.detect
|
|
358
|
+
postgres_version = postgres_detector.detect
|
|
359
|
+
mysql_version = mysql_detector.detect
|
|
360
|
+
redis_version = redis_detector.detect
|
|
361
|
+
mongo_version = mongo_detector.detect
|
|
362
|
+
|
|
363
|
+
# Save to config
|
|
364
|
+
config_manager.save_project(
|
|
365
|
+
name: name,
|
|
366
|
+
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
|
|
369
|
+
)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
updated_count += 1
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
say "\n✓ Updated #{updated_count} project(s)", :green
|
|
376
|
+
say "✓ Removed #{removed_count} project(s) with missing directories", :yellow if removed_count.positive?
|
|
377
|
+
say "\nView updated projects with: harbinger show", :cyan
|
|
378
|
+
end
|
|
379
|
+
|
|
140
380
|
desc "version", "Show harbinger version"
|
|
141
381
|
def version
|
|
142
382
|
say "Harbinger version #{Harbinger::VERSION}", :cyan
|
|
@@ -147,9 +387,19 @@ module Harbinger
|
|
|
147
387
|
def scan_recursive(base_path)
|
|
148
388
|
say "Scanning #{base_path} recursively for Ruby projects...", :cyan
|
|
149
389
|
|
|
150
|
-
# Find all directories with Gemfiles
|
|
390
|
+
# Find all directories with Gemfiles, excluding common non-project directories
|
|
391
|
+
excluded_patterns = %w[
|
|
392
|
+
vendor/
|
|
393
|
+
node_modules/
|
|
394
|
+
tmp/
|
|
395
|
+
.git/
|
|
396
|
+
spec/fixtures/
|
|
397
|
+
test/fixtures/
|
|
398
|
+
]
|
|
399
|
+
|
|
151
400
|
gemfile_dirs = Dir.glob(File.join(base_path, "**/Gemfile"))
|
|
152
401
|
.map { |f| File.dirname(f) }
|
|
402
|
+
.reject { |dir| excluded_patterns.any? { |pattern| dir.include?("/#{pattern}") } }
|
|
153
403
|
.sort
|
|
154
404
|
|
|
155
405
|
if gemfile_dirs.empty?
|
|
@@ -167,10 +417,10 @@ module Harbinger
|
|
|
167
417
|
say "\n" unless index == gemfile_dirs.length - 1
|
|
168
418
|
end
|
|
169
419
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
420
|
+
return unless options[:save]
|
|
421
|
+
|
|
422
|
+
say "\n✓ Saved #{gemfile_dirs.length} project(s) to config", :green
|
|
423
|
+
say "View all tracked projects with: harbinger show", :cyan
|
|
174
424
|
end
|
|
175
425
|
|
|
176
426
|
def scan_single(project_path)
|
|
@@ -179,48 +429,91 @@ module Harbinger
|
|
|
179
429
|
# Detect versions
|
|
180
430
|
ruby_detector = Analyzers::RubyDetector.new(project_path)
|
|
181
431
|
rails_analyzer = Analyzers::RailsAnalyzer.new(project_path)
|
|
432
|
+
postgres_detector = Analyzers::PostgresDetector.new(project_path)
|
|
433
|
+
mysql_detector = Analyzers::MysqlDetector.new(project_path)
|
|
434
|
+
redis_detector = Analyzers::RedisDetector.new(project_path)
|
|
435
|
+
mongo_detector = Analyzers::MongoDetector.new(project_path)
|
|
182
436
|
|
|
183
437
|
ruby_version = ruby_detector.detect
|
|
184
438
|
rails_version = rails_analyzer.detect
|
|
439
|
+
postgres_version = postgres_detector.detect
|
|
440
|
+
mysql_version = mysql_detector.detect
|
|
441
|
+
redis_version = redis_detector.detect
|
|
442
|
+
mongo_version = mongo_detector.detect
|
|
185
443
|
|
|
186
444
|
ruby_present = ruby_detector.ruby_detected?
|
|
187
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?
|
|
188
450
|
|
|
189
451
|
# Display results
|
|
190
452
|
say "\nDetected versions:", :green
|
|
191
453
|
if ruby_version
|
|
192
|
-
say " Ruby:
|
|
454
|
+
say " Ruby: #{ruby_version}", :white
|
|
193
455
|
elsif ruby_present
|
|
194
|
-
say " Ruby:
|
|
456
|
+
say " Ruby: Present (version not specified - add .ruby-version or ruby declaration in Gemfile)", :yellow
|
|
195
457
|
else
|
|
196
|
-
say " Ruby:
|
|
458
|
+
say " Ruby: Not a Ruby project", :red
|
|
197
459
|
end
|
|
198
460
|
|
|
199
461
|
if rails_version
|
|
200
|
-
say " Rails:
|
|
462
|
+
say " Rails: #{rails_version}", :white
|
|
201
463
|
elsif rails_present
|
|
202
|
-
say " Rails:
|
|
464
|
+
say " Rails: Present (version not found in Gemfile.lock)", :yellow
|
|
203
465
|
else
|
|
204
|
-
say " Rails:
|
|
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
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
if redis_version
|
|
482
|
+
say " Redis: #{redis_version}", :white
|
|
483
|
+
elsif redis_present
|
|
484
|
+
say " Redis: Present (version not detected)", :yellow
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
if mongo_version
|
|
488
|
+
say " MongoDB: #{mongo_version}", :white
|
|
489
|
+
elsif mongo_present
|
|
490
|
+
say " MongoDB: Present (version not detected)", :yellow
|
|
205
491
|
end
|
|
206
492
|
|
|
207
493
|
# Fetch and display EOL dates
|
|
208
|
-
if ruby_version || rails_version
|
|
494
|
+
if ruby_version || rails_version || postgres_version || mysql_version || redis_version || mongo_version
|
|
209
495
|
say "\nFetching EOL data...", :cyan
|
|
210
496
|
fetcher = EolFetcher.new
|
|
211
497
|
|
|
212
|
-
if ruby_version
|
|
213
|
-
|
|
214
|
-
|
|
498
|
+
display_eol_info(fetcher, "Ruby", ruby_version) if ruby_version
|
|
499
|
+
|
|
500
|
+
display_eol_info(fetcher, "Rails", rails_version) if rails_version
|
|
215
501
|
|
|
216
|
-
if
|
|
217
|
-
display_eol_info(fetcher, "
|
|
502
|
+
if postgres_version && !postgres_version.include?("gem")
|
|
503
|
+
display_eol_info(fetcher, "PostgreSQL", postgres_version)
|
|
218
504
|
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")
|
|
219
511
|
end
|
|
220
512
|
|
|
221
513
|
# Save to config if --save flag is used
|
|
222
514
|
if options[:save] && !options[:recursive]
|
|
223
|
-
|
|
515
|
+
save_project_to_config(project_path, ruby_version, rails_version, postgres_version, mysql_version,
|
|
516
|
+
redis_version, mongo_version)
|
|
224
517
|
elsif options[:save] && options[:recursive]
|
|
225
518
|
# In recursive mode, save without the confirmation message for each project
|
|
226
519
|
config_manager = ConfigManager.new
|
|
@@ -228,19 +521,22 @@ module Harbinger
|
|
|
228
521
|
config_manager.save_project(
|
|
229
522
|
name: project_name,
|
|
230
523
|
path: project_path,
|
|
231
|
-
versions: { ruby: ruby_version, rails: rails_version
|
|
524
|
+
versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version,
|
|
525
|
+
mysql: mysql_version, redis: redis_version, mongo: mongo_version }.compact
|
|
232
526
|
)
|
|
233
527
|
end
|
|
234
528
|
end
|
|
235
529
|
|
|
236
|
-
def
|
|
530
|
+
def save_project_to_config(project_path, ruby_version, rails_version, postgres_version, mysql_version,
|
|
531
|
+
redis_version, mongo_version)
|
|
237
532
|
config_manager = ConfigManager.new
|
|
238
533
|
project_name = File.basename(project_path)
|
|
239
534
|
|
|
240
535
|
config_manager.save_project(
|
|
241
536
|
name: project_name,
|
|
242
537
|
path: project_path,
|
|
243
|
-
versions: { ruby: ruby_version, rails: rails_version
|
|
538
|
+
versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version,
|
|
539
|
+
mysql: mysql_version, redis: redis_version, mongo: mongo_version }.compact
|
|
244
540
|
)
|
|
245
541
|
|
|
246
542
|
say "\n✓ Saved to config as '#{project_name}'", :green
|
|
@@ -270,7 +566,7 @@ module Harbinger
|
|
|
270
566
|
end
|
|
271
567
|
|
|
272
568
|
def eol_color(days)
|
|
273
|
-
if days
|
|
569
|
+
if days.negative?
|
|
274
570
|
:red
|
|
275
571
|
elsif days < 180 # < 6 months
|
|
276
572
|
:yellow
|
|
@@ -280,7 +576,7 @@ module Harbinger
|
|
|
280
576
|
end
|
|
281
577
|
|
|
282
578
|
def eol_status(days)
|
|
283
|
-
if days
|
|
579
|
+
if days.negative?
|
|
284
580
|
"ALREADY EOL (#{days.abs} days ago)"
|
|
285
581
|
elsif days < 30
|
|
286
582
|
"ENDING SOON (#{days} days remaining)"
|
|
@@ -314,5 +610,23 @@ module Harbinger
|
|
|
314
610
|
text
|
|
315
611
|
end
|
|
316
612
|
end
|
|
613
|
+
|
|
614
|
+
def export_data(projects, format, output_path)
|
|
615
|
+
exporter = case format
|
|
616
|
+
when "json"
|
|
617
|
+
Exporters::JsonExporter.new(projects)
|
|
618
|
+
when "csv"
|
|
619
|
+
Exporters::CsvExporter.new(projects)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
result = exporter.export
|
|
623
|
+
|
|
624
|
+
if output_path
|
|
625
|
+
File.write(output_path, result)
|
|
626
|
+
say "Exported to #{output_path}", :green
|
|
627
|
+
else
|
|
628
|
+
puts result
|
|
629
|
+
end
|
|
630
|
+
end
|
|
317
631
|
end
|
|
318
632
|
end
|
|
@@ -18,16 +18,14 @@ module Harbinger
|
|
|
18
18
|
cache_file = cache_file_path(product)
|
|
19
19
|
|
|
20
20
|
# Return fresh cache if available
|
|
21
|
-
if cache_fresh?(cache_file)
|
|
22
|
-
return read_cache(cache_file)
|
|
23
|
-
end
|
|
21
|
+
return read_cache(cache_file) if cache_fresh?(cache_file)
|
|
24
22
|
|
|
25
23
|
# Try to fetch from API
|
|
26
24
|
begin
|
|
27
25
|
data = fetch_from_api(product)
|
|
28
26
|
write_cache(cache_file, data)
|
|
29
27
|
data
|
|
30
|
-
rescue StandardError
|
|
28
|
+
rescue StandardError
|
|
31
29
|
# Fall back to stale cache if API fails
|
|
32
30
|
read_cache(cache_file) if File.exist?(cache_file)
|
|
33
31
|
end
|
|
@@ -39,11 +37,27 @@ module Harbinger
|
|
|
39
37
|
|
|
40
38
|
# Extract major.minor from version (e.g., "3.2.1" -> "3.2")
|
|
41
39
|
version_parts = version.split(".")
|
|
42
|
-
|
|
40
|
+
major = version_parts[0]
|
|
41
|
+
major_minor = version_parts[1] ? "#{major}.#{version_parts[1]}" : nil
|
|
42
|
+
|
|
43
|
+
# Try exact major.minor first (e.g., "8.0" for MySQL, "3.2" for Ruby)
|
|
44
|
+
if major_minor
|
|
45
|
+
entry = data.find { |item| item["cycle"] == major_minor }
|
|
46
|
+
return entry["eol"] if entry
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Try major only (e.g., "16" for PostgreSQL)
|
|
50
|
+
entry = data.find { |item| item["cycle"] == major }
|
|
51
|
+
return entry["eol"] if entry
|
|
52
|
+
|
|
53
|
+
# For major-only versions, find the latest minor version in that major series
|
|
54
|
+
# (e.g., version "7" should match "7.4" which is the latest 7.x)
|
|
55
|
+
matching_entries = data.select { |item| item["cycle"].to_s.start_with?("#{major}.") }
|
|
56
|
+
return nil if matching_entries.empty?
|
|
43
57
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
# Sort by cycle version and get the latest (highest minor version)
|
|
59
|
+
latest = matching_entries.max_by { |item| item["cycle"].to_s.split(".").map(&:to_i) }
|
|
60
|
+
latest ? latest["eol"] : nil
|
|
47
61
|
end
|
|
48
62
|
|
|
49
63
|
private
|