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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 663f135c9d728ba59e2f33d33ddd73e3be1b37143c7533db0534db995301aa8e
4
- data.tar.gz: de00d8ba69996d960b57bc96d49c79da0f9221a90ac870805fd9f5aad16b506f
3
+ metadata.gz: 1d46315e9aab151406a3993d75a034eaf28f16d1ce92f44b6344438ba8b9c482
4
+ data.tar.gz: b4d3c35c6578600ed85d1a387b1cfe13ce5873c256dce6d20267c0366e329052
5
5
  SHA512:
6
- metadata.gz: 2505777f55661fe67224a94351e8eb00d56cb2ef834c19a86f00e9be6fd45cf4aea5abb872242750c5f4dc3608e4707e0ecb8899538407d348ff7fdec7254a3c
7
- data.tar.gz: 1ee3dcbca6a3cd75ede6a720fdb50bc8969a070c4bbb0c8a17cb4abacc0b9cb4536dd78fe0b78cb7609d56e15fc5affdcebac1ec5a906911cd3e323341b882bf
6
+ metadata.gz: a6d586f4ccc611050bff15516e5ce10601b21b530c51a899742dd8fb8a167cb2d297d0d182fb3a4536b78d85db09b084c25c646fbf3249fd1120125a614aa50b
7
+ data.tar.gz: a1a0aa1900b07a3be28862e6ad4e3196e05735dc338c22ed592f6ad6d3af6d925764b0c2d957a96fa3d48b9f995e500edcfbb932897237a1a4c44e8bd1d16228
data/README.md CHANGED
@@ -2,17 +2,19 @@
2
2
 
3
3
  **Track End-of-Life dates for your tech stack and stay ahead of deprecations.**
4
4
 
5
- Harbinger is a CLI tool that scans your Ruby, Rails, PostgreSQL, and MySQL versions, and warns you about upcoming EOL (End-of-Life) dates. Never get caught off-guard by unsupported dependencies again.
5
+ Harbinger is a CLI tool that scans your Ruby, Rails, Python, Node.js, Rust, Go, PostgreSQL, MySQL, Redis, and MongoDB versions, and warns you about upcoming EOL (End-of-Life) dates. Never get caught off-guard by unsupported dependencies again.
6
6
 
7
7
  ## Features
8
8
 
9
- - 🔍 **Auto-detects versions** from `.ruby-version`, `Gemfile`, `Gemfile.lock`, and `config/database.yml`
10
- - 🐘 **Database detection** for PostgreSQL and MySQL (mysql2/trilogy adapters)
9
+ - 🔍 **Auto-detects versions** from `.ruby-version`, `Gemfile`, `Gemfile.lock`, `.nvmrc`, `.python-version`, `pyproject.toml`, `package.json`, `rust-toolchain`, `Cargo.toml`, `go.mod`, `config/database.yml`, and `docker-compose.yml`
10
+ - 🐘 **Database detection** for PostgreSQL, MySQL, Redis, and MongoDB
11
+ - 🌐 **Multi-language support** - Ruby, Python, Node.js, Rust, Go
12
+ - 📊 **Ecosystem-grouped dashboard** - Projects organized by language ecosystem with relevant components only
11
13
  - 📅 **Fetches EOL data** from [endoflife.date](https://endoflife.date)
12
14
  - 🎨 **Color-coded warnings** (red: already EOL, yellow: <6 months, green: safe)
13
15
  - ⚡ **Smart caching** (24-hour cache, works offline after first fetch)
14
- - 📊 **Track multiple projects** with `--save` and view dashboard with `harbinger show`
15
16
  - 🔄 **Bulk operations** with `--recursive` scan and `rescan` command
17
+ - 📤 **Export to JSON/CSV** for reporting and automation
16
18
  - 🚀 **Zero configuration** - just run `harbinger scan`
17
19
 
18
20
  ## Installation
@@ -116,18 +118,42 @@ harbinger show myproject --format json
116
118
  **Example output:**
117
119
 
118
120
  ```
119
- Tracked Projects (10)
121
+ Tracked Projects (12)
122
+
123
+ Ruby Ecosystem (7)
124
+ ================================================================================
125
+ ┌─────────────────┬───────┬──────────┬────────────┬───────┬─────────────────┐
126
+ │ Project │ Ruby │ Rails │ PostgreSQL │ Redis │ Status │
127
+ ├─────────────────┼───────┼──────────┼────────────┼───────┼─────────────────┤
128
+ │ shop-api │ 3.2.0 │ 6.1.7 │ 15.0 │ - │ ✗ Rails EOL │
129
+ │ blog-engine │ 3.3.0 │ 7.0.8 │ 16.0 │ 7.0 │ ✗ Rails EOL │
130
+ │ analytics-app │ 3.3.0 │ 8.0.1 │ 16.0 │ - │ ✓ Current │
131
+ │ admin-portal │ 3.3.0 │ 8.0.4 │ 16.11 │ 7.2 │ ✓ Current │
132
+ │ billing-service │ 3.4.1 │ 8.1.0 │ 17.0 │ - │ ✓ Current │
133
+ └─────────────────┴───────┴──────────┴────────────┴───────┴─────────────────┘
134
+
135
+ Python Ecosystem (3)
136
+ ================================================================================
137
+ ┌──────────────┬────────┬────────────┬─────────┐
138
+ │ Project │ Python │ PostgreSQL │ Status │
139
+ ├──────────────┼────────┼────────────┼─────────┤
140
+ │ ml-pipeline │ 3.11 │ 16.0 │ ✓ Current │
141
+ │ data-scraper │ 3.12 │ - │ ✓ Current │
142
+ │ ai-worker │ 3.13 │ 15.0 │ ✓ Current │
143
+ └──────────────┴────────┴────────────┴─────────┘
144
+
145
+ Node.js Ecosystem (2)
120
146
  ================================================================================
121
- ┌───────────────────┬───────┬──────────┬────────────┬───────┬─────────────┐
122
- │ Project Ruby │ Rails │ PostgreSQL │ MySQL │ Status
123
- ├───────────────────┼───────┼──────────┼────────────┼───────┼─────────────┤
124
- ledger 3.3.0 6.1.7.10 │ - │ - │ Rails EOL
125
- option_tracker 3.3.0 7.0.8.7 - │ - ✗ Rails EOL │
126
- │ CarCal │ - │ 8.0.2 │ - │ - │ ✓ Current │
127
- │ job_tracker │ 3.3.0 │ 8.0.4 │ 16.11 │ - │ ✓ Current │
128
- └───────────────────┴───────┴──────────┴────────────┴───────┴─────────────┘
147
+ ┌───────────────┬─────────┬────────────┬──────────────────────┐
148
+ │ Project Node.js │ PostgreSQL │ Status
149
+ ├───────────────┼─────────┼────────────┼──────────────────────┤
150
+ frontend-app 18.0 │ - │ Node.js ending soon
151
+ realtime-api 22.0 16.0 Current
152
+ └───────────────┴─────────┴────────────┴──────────────────────┘
129
153
  ```
130
154
 
155
+ Projects are grouped by their primary programming language ecosystem. Each ecosystem only displays relevant components (e.g., Python projects don't show Ruby/Rails columns).
156
+
131
157
  ### Re-scan all tracked projects
132
158
 
133
159
  ```bash
@@ -163,8 +189,14 @@ harbinger version
163
189
  1. **Detection**: Harbinger looks for version info in your project:
164
190
  - Ruby: `.ruby-version`, `Gemfile` (`ruby "x.x.x"`), `Gemfile.lock` (RUBY VERSION)
165
191
  - Rails: `Gemfile.lock` (rails gem)
192
+ - Python: `.python-version`, `pyproject.toml`, `docker-compose.yml`, or `python --version`
193
+ - Node.js: `.nvmrc`, `.node-version`, `package.json` engines, `docker-compose.yml`, or `node --version`
194
+ - Rust: `rust-toolchain`, `rust-toolchain.toml`, `Cargo.toml` (rust-version), `docker-compose.yml`, or `rustc --version`
195
+ - Go: `go.mod`, `go.work`, `.go-version`, `docker-compose.yml`, or `go version`
166
196
  - PostgreSQL: `config/database.yml` (adapter check) + `psql --version` or `pg` gem
167
197
  - MySQL: `config/database.yml` (mysql2/trilogy adapter) + `mysql --version` or gem version
198
+ - Redis: `docker-compose.yml` + `redis-server --version` or `redis` gem
199
+ - MongoDB: `docker-compose.yml` + `mongod --version` or `mongoid`/`mongo` gem
168
200
 
169
201
  2. **EOL Data**: Fetches official EOL dates from [endoflife.date](https://endoflife.date) API
170
202
 
@@ -205,6 +237,53 @@ Parses `Gemfile.lock` for the rails gem version.
205
237
 
206
238
  **Supported adapters**: `mysql2` (traditional) and `trilogy` (Rails 7.1+)
207
239
 
240
+ ### Redis Detection
241
+
242
+ 1. Checks `docker-compose.yml` for redis image with version tag
243
+ 2. Tries `redis-server --version` for local installations
244
+ 3. Falls back to `redis` gem version from `Gemfile.lock`
245
+
246
+ ### MongoDB Detection
247
+
248
+ 1. Checks `docker-compose.yml` for mongo image with version tag
249
+ 2. Tries `mongod --version` for local installations
250
+ 3. Falls back to `mongoid` or `mongo` gem version from `Gemfile.lock`
251
+
252
+ ### Python Detection
253
+
254
+ 1. `.python-version` file (highest priority)
255
+ 2. `pyproject.toml` (`requires-python` field)
256
+ 3. Docker Compose `python:*` images
257
+ 4. `python --version` for system installation
258
+
259
+ ### Node.js Detection
260
+
261
+ 1. `.nvmrc` or `.node-version` files (highest priority - explicit version specification)
262
+ 2. `package.json` `engines.node` field (e.g., ">=18.0.0")
263
+ 3. Docker Compose `node:*` images
264
+ 4. `node --version` for system installation
265
+
266
+ **Version Normalization**: Handles constraint operators (`>=`, `^`, `~`), LTS names (`lts/hydrogen`), and version ranges
267
+
268
+ ### Rust Detection
269
+
270
+ 1. `rust-toolchain` or `rust-toolchain.toml` files (highest priority - explicit toolchain specification)
271
+ 2. `Cargo.toml` `rust-version` field (MSRV - Minimum Supported Rust Version)
272
+ 3. Docker Compose `rust:*` images
273
+ 4. `rustc --version` for system installation (only when Rust project files exist)
274
+
275
+ **Note:** Symbolic channels like "stable", "beta", "nightly" are skipped as they don't provide specific versions.
276
+
277
+ ### Go Detection
278
+
279
+ 1. `go.mod` file (highest priority - standard Go module version specification)
280
+ 2. `go.work` file (Go workspace format)
281
+ 3. `.go-version` file
282
+ 4. Docker Compose `golang:*` images
283
+ 5. `go version` for system installation (only when Go project files exist)
284
+
285
+ **Note:** Shell fallback only executes when Go project files are detected, preventing unnecessary command execution.
286
+
208
287
  ## Requirements
209
288
 
210
289
  - Ruby >= 3.1.0
@@ -229,30 +308,27 @@ bundle exec exe/harbinger scan .
229
308
 
230
309
  ## Roadmap
231
310
 
232
- ### V0.4.0 - Current
311
+ ### V1.0 - Current (Ready for Release!)
312
+ - ✅ Ruby, Rails, Python, Node.js, Rust, Go version detection
313
+ - ✅ PostgreSQL, MySQL, Redis, MongoDB version detection
314
+ - ✅ Ecosystem-grouped dashboard with smart component display
233
315
  - ✅ Export reports to JSON/CSV
234
- - ✅ Docker Compose database version detection
235
- - ✅ Redis version detection
236
- - ✅ MongoDB version detection
237
-
238
- ### V0.3.0
239
- - PostgreSQL version detection with local/remote database handling
240
- - MySQL version detection (mysql2 and trilogy adapters)
241
- - Rescan command to update all tracked projects
242
- - Enhanced dashboard with database columns
243
- - ✅ EOL tracking for PostgreSQL and MySQL
244
-
245
- ### V1.0 - Future
246
- - 🐍 Python support (pyproject.toml, requirements.txt)
247
- - 📦 Node.js support (package.json, .nvmrc)
248
- - 🦀 Rust support (Cargo.toml)
249
- - 🐘 Go support (go.mod)
316
+ - ✅ Bulk scanning with `--recursive` and `rescan` commands
317
+ - ✅ 24-hour smart caching for offline support
318
+ - ✅ Color-coded EOL warnings
319
+
320
+ ### V1.1 - Next
321
+ - 🔷 TypeScript version detection
322
+ - 🎯 Framework detection (Django, Flask, Express, Gin)
323
+ - 📦 Package manager detection (npm, yarn, pip, bundler, go modules versions)
324
+ - 🔍 Dependency vulnerability scanning integration
250
325
 
251
326
  ### V2.0 - Vision
252
- - 🤖 AI-powered upgrade summaries
253
- - 📧 Email/Slack notifications
254
- - ☁️ Cloud platform detection (AWS, Heroku, etc.)
255
- - 👥 Team collaboration features
327
+ - 🤖 AI-powered upgrade summaries and breaking change analysis
328
+ - 📧 Email/Slack notifications for approaching EOL dates
329
+ - ☁️ Cloud platform detection (AWS, Heroku, Render)
330
+ - 👥 Team collaboration features (shared dashboards)
331
+ - 📈 Historical tracking and trends
256
332
 
257
333
  ## Contributing
258
334
 
data/docs/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Harbinger - Track EOL Dates for Your Tech Stack</title>
7
- <meta name="description" content="Never get caught off-guard by unsupported dependencies. Harbinger tracks End-of-Life dates for Ruby, Rails, and more.">
7
+ <meta name="description" content="Never get caught off-guard by unsupported dependencies. Harbinger tracks End-of-Life dates for Ruby, Rails, PostgreSQL, MySQL, Redis, MongoDB, Python, Node.js, and Rust.">
8
8
  <script src="https://cdn.tailwindcss.com"></script>
9
9
  <style>
10
10
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&display=swap');
@@ -17,7 +17,7 @@
17
17
  <div class="max-w-6xl mx-auto px-4 py-20">
18
18
  <h1 class="text-5xl md:text-7xl font-black mb-6">Harbinger</h1>
19
19
  <p class="text-2xl md:text-3xl mb-8 text-blue-100">Track End-of-Life dates for your tech stack</p>
20
- <p class="text-xl mb-12 text-blue-100 max-w-2xl">Never get caught off-guard by unsupported dependencies. Harbinger scans your Ruby, Rails, PostgreSQL, and MySQL versions and warns you before support ends.</p>
20
+ <p class="text-xl mb-12 text-blue-100 max-w-2xl">Never get caught off-guard by unsupported dependencies. Harbinger scans your Ruby, Rails, Python, Node.js, Rust, Go, PostgreSQL, MySQL, Redis, and MongoDB versions and warns you before support ends.</p>
21
21
 
22
22
  <div class="flex flex-col sm:flex-row gap-4">
23
23
  <div class="bg-gray-900 rounded-lg p-4 font-mono text-sm">
@@ -38,7 +38,7 @@
38
38
  <div class="bg-white p-6 rounded-lg shadow-sm">
39
39
  <div class="text-4xl mb-4">🔍</div>
40
40
  <h3 class="text-xl font-semibold mb-2">Auto-Detection</h3>
41
- <p class="text-gray-600">Detects Ruby, Rails, PostgreSQL, and MySQL versions from project files and database configs</p>
41
+ <p class="text-gray-600">Detects Ruby, Rails, Python, Node.js, Rust, Go, PostgreSQL, MySQL, Redis, and MongoDB versions from project files and Docker configs</p>
42
42
  </div>
43
43
  <div class="bg-white p-6 rounded-lg shadow-sm">
44
44
  <div class="text-4xl mb-4">📅</div>
@@ -62,8 +62,8 @@
62
62
  </div>
63
63
  <div class="bg-white p-6 rounded-lg shadow-sm">
64
64
  <div class="text-4xl mb-4">📊</div>
65
- <h3 class="text-xl font-semibold mb-2">Smart Dashboard</h3>
66
- <p class="text-gray-600">Dynamic columns show only relevant data - columns and projects without matches are hidden automatically</p>
65
+ <h3 class="text-xl font-semibold mb-2">Ecosystem Grouping</h3>
66
+ <p class="text-gray-600">Projects organized by language ecosystem (Ruby, Python, Node.js, Rust) with only relevant components displayed</p>
67
67
  </div>
68
68
  <div class="bg-white p-6 rounded-lg shadow-sm">
69
69
  <div class="text-4xl mb-4">🔄</div>
@@ -73,7 +73,7 @@
73
73
  <div class="bg-white p-6 rounded-lg shadow-sm">
74
74
  <div class="text-4xl mb-4">🧪</div>
75
75
  <h3 class="text-xl font-semibold mb-2">Well Tested</h3>
76
- <p class="text-gray-600">116 RSpec tests with 100% pass rate, built with TDD</p>
76
+ <p class="text-gray-600">146 RSpec tests with 100% pass rate, built with TDD</p>
77
77
  </div>
78
78
  </div>
79
79
  </div>
@@ -109,7 +109,49 @@
109
109
  </div>
110
110
 
111
111
  <div class="prose max-w-none">
112
- <h3 class="text-2xl font-semibold mb-4">Commands</h3>
112
+ <h3 class="text-2xl font-semibold mb-4">Dashboard with Ecosystem Grouping</h3>
113
+ <p class="text-gray-600 mb-4">Projects are organized by their primary programming language ecosystem, showing only relevant components:</p>
114
+
115
+ <div class="bg-gray-900 rounded-lg p-6 font-mono text-xs overflow-x-auto mb-8">
116
+ <div class="text-gray-400 mb-1">$ harbinger show</div>
117
+ <div class="text-white mb-2">Tracked Projects (12)</div>
118
+
119
+ <div class="text-cyan-400 mt-3 mb-1">Ruby Ecosystem (7)</div>
120
+ <div class="text-gray-400">================================================================================</div>
121
+ <div class="text-white">
122
+ <div>┌─────────────────┬───────┬──────────┬────────────┬───────┬─────────────────┐</div>
123
+ <div>│ Project │ Ruby │ Rails │ PostgreSQL │ Redis │ Status │</div>
124
+ <div>├─────────────────┼───────┼──────────┼────────────┼───────┼─────────────────┤</div>
125
+ <div>│ shop-api │ 3.2.0 │ 6.1.7 │ 15.0 │ - │ <span class="text-red-400">✗ Rails EOL</span> │</div>
126
+ <div>│ blog-engine │ 3.3.0 │ 7.0.8 │ 16.0 │ 7.0 │ <span class="text-red-400">✗ Rails EOL</span> │</div>
127
+ <div>│ admin-portal │ 3.3.0 │ 8.0.4 │ 16.11 │ 7.2 │ <span class="text-green-400">✓ Current</span> │</div>
128
+ <div>└─────────────────┴───────┴──────────┴────────────┴───────┴─────────────────┘</div>
129
+ </div>
130
+
131
+ <div class="text-cyan-400 mt-3 mb-1">Python Ecosystem (3)</div>
132
+ <div class="text-gray-400">================================================================================</div>
133
+ <div class="text-white">
134
+ <div>┌──────────────┬────────┬────────────┬─────────┐</div>
135
+ <div>│ Project │ Python │ PostgreSQL │ Status │</div>
136
+ <div>├──────────────┼────────┼────────────┼─────────┤</div>
137
+ <div>│ ml-pipeline │ 3.11 │ 16.0 │ <span class="text-green-400">✓ Current</span> │</div>
138
+ <div>│ data-scraper │ 3.12 │ - │ <span class="text-green-400">✓ Current</span> │</div>
139
+ <div>└──────────────┴────────┴────────────┴─────────┘</div>
140
+ </div>
141
+
142
+ <div class="text-cyan-400 mt-3 mb-1">Node.js Ecosystem (2)</div>
143
+ <div class="text-gray-400">================================================================================</div>
144
+ <div class="text-white">
145
+ <div>┌──────────────┬─────────┬────────────┬─────────┐</div>
146
+ <div>│ Project │ Node.js │ PostgreSQL │ Status │</div>
147
+ <div>├──────────────┼─────────┼────────────┼─────────┤</div>
148
+ <div>│ frontend-app │ 18.0 │ - │ <span class="text-yellow-400">⚠ Node.js ending soon</span> │</div>
149
+ <div>│ realtime-api │ 22.0 │ 16.0 │ <span class="text-green-400">✓ Current</span> │</div>
150
+ <div>└──────────────┴─────────┴────────────┴─────────┘</div>
151
+ </div>
152
+ </div>
153
+
154
+ <h3 class="text-2xl font-semibold mb-4 mt-8">Commands</h3>
113
155
  <ul class="space-y-3 text-gray-700">
114
156
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --path [PATH]</code> - Scan a project and show EOL status</li>
115
157
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --save</code> - Save project for tracking</li>
@@ -131,30 +173,31 @@
131
173
  <h2 class="text-3xl font-bold mb-8 text-center">Roadmap</h2>
132
174
  <div class="grid md:grid-cols-3 gap-8">
133
175
  <div>
134
- <h3 class="text-xl font-semibold mb-4 text-green-600">✅ V0.4.0 (Current)</h3>
176
+ <h3 class="text-xl font-semibold mb-4 text-green-600">✅ V1.0 (Current)</h3>
135
177
  <ul class="space-y-2 text-gray-600">
178
+ <li>• Ruby, Rails, Python, Node.js, Rust, Go</li>
179
+ <li>• PostgreSQL, MySQL, Redis, MongoDB</li>
180
+ <li>• Ecosystem-grouped dashboard</li>
136
181
  <li>• Export to JSON/CSV</li>
137
- <li>• Docker Compose detection</li>
138
- <li>• Redis detection</li>
139
- <li>• MongoDB detection</li>
182
+ <li>• Bulk scanning & rescan</li>
140
183
  </ul>
141
184
  </div>
142
185
  <div>
143
- <h3 class="text-xl font-semibold mb-4 text-blue-600">📋 V0.3.0</h3>
186
+ <h3 class="text-xl font-semibold mb-4 text-blue-600">📋 V1.1 (Next)</h3>
144
187
  <ul class="space-y-2 text-gray-600">
145
- <li>• PostgreSQL detection</li>
146
- <li>• MySQL detection</li>
147
- <li>• Rescan command</li>
148
- <li>• Smart dashboard filtering</li>
188
+ <li>• TypeScript detection</li>
189
+ <li>• Framework detection (Django, Flask, Express, Gin)</li>
190
+ <li>• Package manager versions</li>
191
+ <li>• Vulnerability scanning integration</li>
149
192
  </ul>
150
193
  </div>
151
194
  <div>
152
- <h3 class="text-xl font-semibold mb-4 text-purple-600">🚀 V1.0 (Future)</h3>
195
+ <h3 class="text-xl font-semibold mb-4 text-purple-600">🚀 V2.0 (Vision)</h3>
153
196
  <ul class="space-y-2 text-gray-600">
154
- <li>• Python support</li>
155
- <li>• Node.js support</li>
156
- <li>• Go support</li>
157
- <li>• Rust support</li>
197
+ <li>• AI-powered upgrade summaries</li>
198
+ <li>• Email/Slack notifications</li>
199
+ <li>• Cloud platform detection</li>
200
+ <li>• Team collaboration</li>
158
201
  </ul>
159
202
  </div>
160
203
  </div>
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Harbinger
4
+ module Analyzers
5
+ class GoDetector
6
+ def initialize(project_path)
7
+ @project_path = project_path
8
+ end
9
+
10
+ def detect
11
+ version = detect_from_go_mod ||
12
+ detect_from_go_work ||
13
+ detect_from_go_version_file ||
14
+ detect_from_docker_compose ||
15
+ detect_from_shell
16
+
17
+ normalize_version(version) if version
18
+ end
19
+
20
+ def go_detected?
21
+ File.exist?(File.join(@project_path, "go.mod")) ||
22
+ File.exist?(File.join(@project_path, "go.work")) ||
23
+ File.exist?(File.join(@project_path, ".go-version"))
24
+ end
25
+
26
+ private
27
+
28
+ def detect_from_go_mod
29
+ go_mod_path = File.join(@project_path, "go.mod")
30
+ return nil unless File.exist?(go_mod_path)
31
+
32
+ content = File.read(go_mod_path)
33
+ # Match "go 1.21" or "go 1.21.0" format
34
+ match = content.match(/^go\s+([\d.]+)/m)
35
+ match[1] if match
36
+ end
37
+
38
+ def detect_from_go_work
39
+ go_work_path = File.join(@project_path, "go.work")
40
+ return nil unless File.exist?(go_work_path)
41
+
42
+ content = File.read(go_work_path)
43
+ # Match "go 1.21" or "go 1.21.0" format
44
+ match = content.match(/^go\s+([\d.]+)/m)
45
+ match[1] if match
46
+ end
47
+
48
+ def detect_from_go_version_file
49
+ go_version_path = File.join(@project_path, ".go-version")
50
+ return nil unless File.exist?(go_version_path)
51
+
52
+ version = File.read(go_version_path).strip
53
+ version unless version.empty?
54
+ end
55
+
56
+ def detect_from_docker_compose
57
+ docker_compose_path = File.join(@project_path, "docker-compose.yml")
58
+ return nil unless File.exist?(docker_compose_path)
59
+
60
+ content = File.read(docker_compose_path)
61
+ # Match golang:1.21, golang:1.21.0, golang:1.21-alpine, etc.
62
+ match = content.match(/golang:([\d.]+)/)
63
+ match[1] if match
64
+ end
65
+
66
+ def detect_from_shell
67
+ # Only try shell detection if Go project files exist
68
+ return nil unless go_detected?
69
+
70
+ version_output = `go version 2>/dev/null`.strip
71
+ return nil if version_output.empty?
72
+
73
+ # Parse "go version go1.21.0 darwin/amd64" format
74
+ match = version_output.match(/go version go([\d.]+)/)
75
+ match[1] if match
76
+ rescue StandardError
77
+ nil
78
+ end
79
+
80
+ def normalize_version(version)
81
+ # Remove any trailing qualifiers like -alpine, -rc1, etc.
82
+ version.gsub(/-(alpine|rc\d+|beta\d*).*/, "")
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "docker_compose_detector"
4
+ require "json"
5
+
6
+ module Harbinger
7
+ module Analyzers
8
+ class NodeDetector
9
+ def initialize(project_path)
10
+ @project_path = project_path
11
+ end
12
+
13
+ def detect
14
+ detect_from_version_files ||
15
+ detect_from_package_json ||
16
+ detect_from_docker_compose ||
17
+ (nodejs_detected? ? detect_from_shell : nil)
18
+ end
19
+
20
+ def nodejs_detected?
21
+ File.exist?(File.join(project_path, "package.json")) ||
22
+ File.exist?(File.join(project_path, "package-lock.json")) ||
23
+ File.exist?(File.join(project_path, ".nvmrc")) ||
24
+ File.exist?(File.join(project_path, ".node-version")) ||
25
+ File.exist?(File.join(project_path, "node_modules"))
26
+ rescue StandardError
27
+ false
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :project_path
33
+
34
+ def detect_from_version_files
35
+ [".nvmrc", ".node-version"].each do |filename|
36
+ file_path = File.join(project_path, filename)
37
+ next unless File.exist?(file_path)
38
+
39
+ content = File.read(file_path).strip
40
+ next if content.empty?
41
+
42
+ return extract_version(content)
43
+ end
44
+
45
+ nil
46
+ rescue StandardError
47
+ nil
48
+ end
49
+
50
+ def detect_from_package_json
51
+ file_path = File.join(project_path, "package.json")
52
+ return nil unless File.exist?(file_path)
53
+
54
+ content = File.read(file_path)
55
+ package = JSON.parse(content)
56
+
57
+ engines = package.dig("engines", "node")
58
+ return nil unless engines
59
+
60
+ extract_version(engines)
61
+ rescue StandardError
62
+ nil
63
+ end
64
+
65
+ def detect_from_docker_compose
66
+ docker = DockerComposeDetector.new(project_path)
67
+ docker.image_version("node")
68
+ end
69
+
70
+ def detect_from_shell
71
+ output = `node --version 2>&1`.strip
72
+ return nil unless $CHILD_STATUS.success?
73
+
74
+ # Parse: "v18.16.0" (note the 'v' prefix)
75
+ match = output.match(/^v?(\d+\.\d+(?:\.\d+)?)/)
76
+ match[1] if match
77
+ rescue StandardError
78
+ nil
79
+ end
80
+
81
+ def extract_version(version_string)
82
+ # Remove 'v' prefix (Node.js convention)
83
+ version = version_string.sub(/^v/, "")
84
+
85
+ # Strip constraint operators: >=18.0.0, ^18.0.0, ~18.0.0
86
+ version = version.gsub(/^[><=~^!\s]+/, "")
87
+
88
+ # Handle ranges like ">=14.0.0 <20.0.0" - extract first version
89
+ version = version.split(/\s+/).first if version.include?(" ")
90
+
91
+ # Handle .x suffix (e.g., "18.x" => "18")
92
+ version = version.sub(/\.x$/, "")
93
+
94
+ # Handle LTS names like "lts/hydrogen" (hydrogen=18, gallium=16, fermium=14)
95
+ if version =~ /lts\/(hydrogen|gallium|fermium)/i
96
+ lts_name = $1.downcase
97
+ return case lts_name
98
+ when "hydrogen" then "18"
99
+ when "gallium" then "16"
100
+ when "fermium" then "14"
101
+ else nil
102
+ end
103
+ end
104
+
105
+ version
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Harbinger
4
+ module Analyzers
5
+ class PythonDetector
6
+ def initialize(project_path)
7
+ @project_path = project_path
8
+ end
9
+
10
+ def detect
11
+ detect_from_pyproject_toml ||
12
+ detect_from_python_version ||
13
+ detect_from_pyvenv_cfg ||
14
+ (python_detected? ? detect_from_shell : nil)
15
+ end
16
+
17
+ def python_detected?
18
+ File.exist?(File.join(project_path, "pyproject.toml")) ||
19
+ File.exist?(File.join(project_path, "requirements.txt")) ||
20
+ File.exist?(File.join(project_path, ".python-version")) ||
21
+ File.exist?(File.join(project_path, "setup.py")) ||
22
+ File.exist?(File.join(project_path, "setup.cfg")) ||
23
+ venv_exists?
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :project_path
29
+
30
+ def detect_from_pyproject_toml
31
+ file_path = File.join(project_path, "pyproject.toml")
32
+ return nil unless File.exist?(file_path)
33
+
34
+ content = File.read(file_path)
35
+
36
+ # Try [project] requires-python = ">=3.11"
37
+ match = content.match(/requires-python\s*=\s*["']([^"']+)["']/)
38
+ return extract_version(match[1]) if match
39
+
40
+ # Try [tool.poetry.dependencies] python = "^3.11"
41
+ match = content.match(/\[tool\.poetry\.dependencies\].*?python\s*=\s*["']([^"']+)["']/m)
42
+ return extract_version(match[1]) if match
43
+
44
+ nil
45
+ rescue StandardError
46
+ nil
47
+ end
48
+
49
+ def detect_from_python_version
50
+ file_path = File.join(project_path, ".python-version")
51
+ return nil unless File.exist?(file_path)
52
+
53
+ content = File.read(file_path).strip
54
+ extract_version(content)
55
+ rescue StandardError
56
+ nil
57
+ end
58
+
59
+ def detect_from_pyvenv_cfg
60
+ ["venv/pyvenv.cfg", ".venv/pyvenv.cfg"].each do |cfg_path|
61
+ file_path = File.join(project_path, cfg_path)
62
+ next unless File.exist?(file_path)
63
+
64
+ content = File.read(file_path)
65
+ # Parse: "version = 3.11.5"
66
+ match = content.match(/version\s*=\s*(\d+\.\d+(?:\.\d+)?)/)
67
+ return match[1] if match
68
+ end
69
+
70
+ nil
71
+ rescue StandardError
72
+ nil
73
+ end
74
+
75
+ def detect_from_shell
76
+ # Try python3 first (more reliable on multi-Python systems)
77
+ output = `python3 --version 2>&1`.strip
78
+ if $CHILD_STATUS.success?
79
+ match = output.match(/Python\s+(\d+\.\d+(?:\.\d+)?)/)
80
+ return match[1] if match
81
+ end
82
+
83
+ # Fall back to python
84
+ output = `python --version 2>&1`.strip
85
+ return nil unless $CHILD_STATUS.success?
86
+
87
+ match = output.match(/Python\s+(\d+\.\d+(?:\.\d+)?)/)
88
+ match[1] if match
89
+ rescue StandardError
90
+ nil
91
+ end
92
+
93
+ def extract_version(version_string)
94
+ # Strip constraint operators: >=3.11, ^3.11, ~3.11, >3.11, <4.0
95
+ version = version_string.gsub(/^[><=~^!\s]+/, "")
96
+
97
+ # Handle ranges like ">=3.9,<4.0" - extract first version
98
+ version = version.split(",").first.strip if version.include?(",")
99
+
100
+ # Remove "python-" prefix if present (from .python-version)
101
+ version.sub(/^python-/, "")
102
+ end
103
+
104
+ def venv_exists?
105
+ File.exist?(File.join(project_path, "venv/pyvenv.cfg")) ||
106
+ File.exist?(File.join(project_path, ".venv/pyvenv.cfg"))
107
+ end
108
+ end
109
+ end
110
+ end