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.
- checksums.yaml +4 -4
- data/README.md +110 -34
- data/docs/index.html +64 -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 +365 -252
- data/lib/harbinger/exporters/base_exporter.rb +5 -2
- data/lib/harbinger/version.rb +1 -1
- metadata +7 -6
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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"
|
|
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
|
-
|
|
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
|
|
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: {
|
|
368
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|