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.
- checksums.yaml +4 -4
- data/README.md +110 -34
- data/docs/index.html +58 -21
- data/lib/harbinger/analyzers/go_detector.rb +86 -0
- data/lib/harbinger/analyzers/node_detector.rb +109 -0
- data/lib/harbinger/analyzers/python_detector.rb +110 -0
- data/lib/harbinger/analyzers/rails_analyzer.rb +5 -1
- data/lib/harbinger/analyzers/rust_detector.rb +116 -0
- data/lib/harbinger/cli.rb +380 -256
- data/lib/harbinger/exporters/base_exporter.rb +5 -2
- data/lib/harbinger/version.rb +1 -1
- metadata +11 -8
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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: {
|
|
368
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
|