stackharbinger 0.1.0 → 0.3.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 +62 -0
- data/README.md +91 -17
- data/docs/CNAME +1 -0
- data/docs/index.html +179 -0
- data/lib/harbinger/analyzers/database_detector.rb +123 -0
- data/lib/harbinger/analyzers/mysql_detector.rb +83 -0
- data/lib/harbinger/analyzers/postgres_detector.rb +65 -0
- data/lib/harbinger/cli.rb +332 -40
- data/lib/harbinger/config_manager.rb +71 -0
- data/lib/harbinger/eol_fetcher.rb +6 -1
- data/lib/harbinger/version.rb +1 -1
- data/lib/harbinger.rb +3 -0
- metadata +9 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0625ffb650537306abc4297407d0373c204c69412c7610254488c1006461bbcc
|
|
4
|
+
data.tar.gz: 65106dadaa487141d44e7772c57d9b0c59e18ab478517f0e94cad1440b8ab8b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3922e4f871e0782ebf821ef3e01f91aebba249c908f81a3b8f94e550d0d7d68a8525118d61f78b326b801aeecb801fbd89f975b48c2c95bbafd12c7703ebcf7a
|
|
7
|
+
data.tar.gz: c50866bca2f2b69d995d01735d62da1cb44f436aa546e5a069d7f8b50b8d25e63d95969f4df85ed008cd7d893a7e8d92959efe12e29e7834199f7322455a9cb9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-01-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **PostgreSQL detection**: Detects PostgreSQL versions from Rails projects
|
|
14
|
+
- Checks `config/database.yml` for `adapter: postgresql`
|
|
15
|
+
- Runs `psql --version` for local databases
|
|
16
|
+
- Falls back to `pg` gem version from `Gemfile.lock`
|
|
17
|
+
- Skips shell commands for remote databases (AWS RDS, etc.) to avoid client vs server version mismatch
|
|
18
|
+
- **MySQL detection**: Detects MySQL versions from Rails projects
|
|
19
|
+
- Supports both `mysql2` and `trilogy` adapters (Rails 7.1+)
|
|
20
|
+
- Runs `mysql --version` or `mysqld --version` for local databases
|
|
21
|
+
- Falls back to gem version from `Gemfile.lock`
|
|
22
|
+
- Smart remote database detection
|
|
23
|
+
- **Rescan command**: `harbinger rescan` to bulk update all tracked projects
|
|
24
|
+
- Updates all projects with latest detected versions
|
|
25
|
+
- Automatically removes projects with missing directories
|
|
26
|
+
- `--verbose` flag for detailed output
|
|
27
|
+
- Progress counter shows scan status
|
|
28
|
+
- **Enhanced dashboard**: PostgreSQL and MySQL columns in `harbinger show`
|
|
29
|
+
- Database versions displayed in table
|
|
30
|
+
- Database EOL dates included in status calculation
|
|
31
|
+
- Dashboard prioritizes database EOL issues
|
|
32
|
+
- **Database EOL tracking**: Fetches EOL data for PostgreSQL and MySQL
|
|
33
|
+
- `harbinger update` now fetches database EOL data
|
|
34
|
+
- Supports major-only version cycles (PostgreSQL) and major.minor cycles (MySQL)
|
|
35
|
+
- **Multi-database support**: Handles Rails 6+ multi-database configurations
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- `EolFetcher` now supports major-only version matching (PostgreSQL uses "16", not "16.11")
|
|
39
|
+
- Scan output includes database versions with aligned formatting
|
|
40
|
+
- Status calculation includes database EOL dates
|
|
41
|
+
|
|
42
|
+
### Technical
|
|
43
|
+
- Added `DatabaseDetector` abstract base class for database detection
|
|
44
|
+
- Added `PostgresDetector` with local/remote detection (116 total tests passing)
|
|
45
|
+
- Added `MysqlDetector` with trilogy and mysql2 support
|
|
46
|
+
- Test fixtures for various database.yml configurations
|
|
47
|
+
- Remote database detection to avoid shell command mismatches
|
|
48
|
+
- Support for single and multi-database Rails configurations
|
|
49
|
+
|
|
50
|
+
## [0.2.0] - 2026-01-18
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- **Project tracking**: Save and track multiple projects with `--save` flag
|
|
54
|
+
- **Dashboard view**: `harbinger show` command displays all tracked projects in a table
|
|
55
|
+
- **Recursive scanning**: `--recursive` flag to scan all subdirectories with Gemfiles
|
|
56
|
+
- **Config management**: Projects stored in `~/.harbinger/config.yml`
|
|
57
|
+
- **Bulk operations**: Scan entire directories like `~/Projects` and save all at once
|
|
58
|
+
- **Enhanced UI**: TTY::Table for beautiful table formatting in dashboard
|
|
59
|
+
- **Color-coded dashboard**: Red for EOL projects, yellow for ending soon, green for current
|
|
60
|
+
- **Smart sorting**: Dashboard prioritizes EOL projects at the top
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- `scan` command now uses `--path` flag instead of positional argument for consistency
|
|
64
|
+
- ConfigManager API uses extensible `versions: {}` hash for future product support
|
|
65
|
+
- Enhanced test coverage (52 passing tests)
|
|
66
|
+
|
|
67
|
+
### Technical
|
|
68
|
+
- Added ConfigManager with YAML persistence
|
|
69
|
+
- ISO8601 timestamp format for YAML safety
|
|
70
|
+
- Extensible architecture for future language/database support
|
|
71
|
+
|
|
10
72
|
## [0.1.0] - 2026-01-18
|
|
11
73
|
|
|
12
74
|
### Added
|
data/README.md
CHANGED
|
@@ -2,18 +2,30 @@
|
|
|
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
|
|
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.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- 🔍 **Auto-detects versions** from `.ruby-version`, `Gemfile`,
|
|
9
|
+
- 🔍 **Auto-detects versions** from `.ruby-version`, `Gemfile`, `Gemfile.lock`, and `config/database.yml`
|
|
10
|
+
- 🐘 **Database detection** for PostgreSQL and MySQL (mysql2/trilogy adapters)
|
|
10
11
|
- 📅 **Fetches EOL data** from [endoflife.date](https://endoflife.date)
|
|
11
12
|
- 🎨 **Color-coded warnings** (red: already EOL, yellow: <6 months, green: safe)
|
|
12
13
|
- ⚡ **Smart caching** (24-hour cache, works offline after first fetch)
|
|
14
|
+
- 📊 **Track multiple projects** with `--save` and view dashboard with `harbinger show`
|
|
15
|
+
- 🔄 **Bulk operations** with `--recursive` scan and `rescan` command
|
|
13
16
|
- 🚀 **Zero configuration** - just run `harbinger scan`
|
|
14
17
|
|
|
15
18
|
## Installation
|
|
16
19
|
|
|
20
|
+
### Homebrew (macOS)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
brew tap RichD/harbinger
|
|
24
|
+
brew install stackharbinger
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### RubyGems
|
|
28
|
+
|
|
17
29
|
```bash
|
|
18
30
|
gem install stackharbinger
|
|
19
31
|
```
|
|
@@ -24,7 +36,7 @@ Or add to your Gemfile:
|
|
|
24
36
|
gem 'stackharbinger'
|
|
25
37
|
```
|
|
26
38
|
|
|
27
|
-
The command is
|
|
39
|
+
The command is `harbinger` (shorter to type).
|
|
28
40
|
|
|
29
41
|
## Usage
|
|
30
42
|
|
|
@@ -35,7 +47,13 @@ The command is still `harbinger` (shorter to type).
|
|
|
35
47
|
harbinger scan
|
|
36
48
|
|
|
37
49
|
# Scan specific project
|
|
38
|
-
harbinger scan ~/Projects/my-rails-app
|
|
50
|
+
harbinger scan --path ~/Projects/my-rails-app
|
|
51
|
+
|
|
52
|
+
# Save project for tracking
|
|
53
|
+
harbinger scan --save
|
|
54
|
+
|
|
55
|
+
# Scan all Ruby projects in a directory recursively
|
|
56
|
+
harbinger scan --path ~/Projects --recursive --save
|
|
39
57
|
```
|
|
40
58
|
|
|
41
59
|
**Example output:**
|
|
@@ -44,8 +62,9 @@ harbinger scan ~/Projects/my-rails-app
|
|
|
44
62
|
Scanning /Users/you/Projects/my-app...
|
|
45
63
|
|
|
46
64
|
Detected versions:
|
|
47
|
-
Ruby:
|
|
48
|
-
Rails:
|
|
65
|
+
Ruby: 3.2.0
|
|
66
|
+
Rails: 7.0.8
|
|
67
|
+
PostgreSQL: 16.11
|
|
49
68
|
|
|
50
69
|
Fetching EOL data...
|
|
51
70
|
|
|
@@ -56,6 +75,42 @@ Ruby 3.2.0:
|
|
|
56
75
|
Rails 7.0.8:
|
|
57
76
|
EOL Date: 2025-06-01
|
|
58
77
|
Status: ALREADY EOL (474 days ago)
|
|
78
|
+
|
|
79
|
+
PostgreSQL 16.11:
|
|
80
|
+
EOL Date: 2028-11-09
|
|
81
|
+
Status: 1026 days remaining
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### View tracked projects
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Show dashboard of all tracked projects
|
|
88
|
+
harbinger show
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Example output:**
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Tracked Projects (10)
|
|
95
|
+
================================================================================
|
|
96
|
+
┌───────────────────┬───────┬──────────┬────────────┬───────┬─────────────┐
|
|
97
|
+
│ Project │ Ruby │ Rails │ PostgreSQL │ MySQL │ Status │
|
|
98
|
+
├───────────────────┼───────┼──────────┼────────────┼───────┼─────────────┤
|
|
99
|
+
│ ledger │ 3.3.0 │ 6.1.7.10 │ - │ - │ ✗ Rails EOL │
|
|
100
|
+
│ option_tracker │ 3.3.0 │ 7.0.8.7 │ - │ - │ ✗ Rails EOL │
|
|
101
|
+
│ CarCal │ - │ 8.0.2 │ - │ - │ ✓ Current │
|
|
102
|
+
│ job_tracker │ 3.3.0 │ 8.0.4 │ 16.11 │ - │ ✓ Current │
|
|
103
|
+
└───────────────────┴───────┴──────────┴────────────┴───────┴─────────────┘
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Re-scan all tracked projects
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Update all tracked projects with latest versions
|
|
110
|
+
harbinger rescan
|
|
111
|
+
|
|
112
|
+
# Show detailed output for each project
|
|
113
|
+
harbinger rescan --verbose
|
|
59
114
|
```
|
|
60
115
|
|
|
61
116
|
### Update EOL data
|
|
@@ -76,6 +131,8 @@ harbinger version
|
|
|
76
131
|
1. **Detection**: Harbinger looks for version info in your project:
|
|
77
132
|
- Ruby: `.ruby-version`, `Gemfile` (`ruby "x.x.x"`), `Gemfile.lock` (RUBY VERSION)
|
|
78
133
|
- Rails: `Gemfile.lock` (rails gem)
|
|
134
|
+
- PostgreSQL: `config/database.yml` (adapter check) + `psql --version` or `pg` gem
|
|
135
|
+
- MySQL: `config/database.yml` (mysql2/trilogy adapter) + `mysql --version` or gem version
|
|
79
136
|
|
|
80
137
|
2. **EOL Data**: Fetches official EOL dates from [endoflife.date](https://endoflife.date) API
|
|
81
138
|
|
|
@@ -100,6 +157,22 @@ Ruby: Present (version not specified - add .ruby-version or ruby declaration in
|
|
|
100
157
|
|
|
101
158
|
Parses `Gemfile.lock` for the rails gem version.
|
|
102
159
|
|
|
160
|
+
### PostgreSQL Detection
|
|
161
|
+
|
|
162
|
+
1. Checks `config/database.yml` for `adapter: postgresql`
|
|
163
|
+
2. Tries `psql --version` for local databases (skips for remote hosts)
|
|
164
|
+
3. Falls back to `pg` gem version from `Gemfile.lock`
|
|
165
|
+
|
|
166
|
+
**Note**: For remote databases (AWS RDS, etc.), shows gem version since shell command would give local client version, not server version.
|
|
167
|
+
|
|
168
|
+
### MySQL Detection
|
|
169
|
+
|
|
170
|
+
1. Checks `config/database.yml` for `adapter: mysql2` or `adapter: trilogy`
|
|
171
|
+
2. Tries `mysql --version` or `mysqld --version` for local databases
|
|
172
|
+
3. Falls back to `mysql2` or `trilogy` gem version from `Gemfile.lock`
|
|
173
|
+
|
|
174
|
+
**Supported adapters**: `mysql2` (traditional) and `trilogy` (Rails 7.1+)
|
|
175
|
+
|
|
103
176
|
## Requirements
|
|
104
177
|
|
|
105
178
|
- Ruby >= 3.1.0
|
|
@@ -124,23 +197,24 @@ bundle exec exe/harbinger scan .
|
|
|
124
197
|
|
|
125
198
|
## Roadmap
|
|
126
199
|
|
|
127
|
-
### V0.
|
|
128
|
-
- ✅
|
|
129
|
-
- ✅
|
|
130
|
-
- ✅
|
|
131
|
-
- ✅
|
|
200
|
+
### V0.3.0 - Current
|
|
201
|
+
- ✅ PostgreSQL version detection with local/remote database handling
|
|
202
|
+
- ✅ MySQL version detection (mysql2 and trilogy adapters)
|
|
203
|
+
- ✅ Rescan command to update all tracked projects
|
|
204
|
+
- ✅ Enhanced dashboard with database columns
|
|
205
|
+
- ✅ EOL tracking for PostgreSQL and MySQL
|
|
132
206
|
|
|
133
|
-
### V0.
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
207
|
+
### V0.4.0 - Planned
|
|
208
|
+
- 📋 Export reports to JSON/CSV
|
|
209
|
+
- 🐳 Docker Compose database version detection
|
|
210
|
+
- 🔴 Redis version detection
|
|
211
|
+
- 🍃 MongoDB version detection
|
|
138
212
|
|
|
139
213
|
### V1.0 - Future
|
|
140
214
|
- 🐍 Python support (pyproject.toml, requirements.txt)
|
|
141
215
|
- 📦 Node.js support (package.json, .nvmrc)
|
|
142
216
|
- 🦀 Rust support (Cargo.toml)
|
|
143
|
-
-
|
|
217
|
+
- 🐘 Go support (go.mod)
|
|
144
218
|
|
|
145
219
|
### V2.0 - Vision
|
|
146
220
|
- 🤖 AI-powered upgrade summaries
|
data/docs/CNAME
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stackharbinger.com
|
data/docs/index.html
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
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.">
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
<style>
|
|
10
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&display=swap');
|
|
11
|
+
body { font-family: 'Inter', sans-serif; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body class="bg-gray-50">
|
|
15
|
+
<!-- Hero Section -->
|
|
16
|
+
<div class="bg-gradient-to-br from-blue-600 to-blue-800 text-white">
|
|
17
|
+
<div class="max-w-6xl mx-auto px-4 py-20">
|
|
18
|
+
<h1 class="text-5xl md:text-7xl font-black mb-6">Harbinger</h1>
|
|
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 and Rails projects and warns you before support ends.</p>
|
|
21
|
+
|
|
22
|
+
<div class="flex flex-col sm:flex-row gap-4">
|
|
23
|
+
<div class="bg-gray-900 rounded-lg p-4 font-mono text-sm">
|
|
24
|
+
<div class="mb-2"><span class="text-gray-400">$</span> <span class="text-green-400">brew tap RichD/harbinger</span></div>
|
|
25
|
+
<div><span class="text-gray-400">$</span> <span class="text-green-400">brew install stackharbinger</span></div>
|
|
26
|
+
</div>
|
|
27
|
+
<a href="https://github.com/RichD/harbinger" class="inline-block bg-white text-blue-600 px-8 py-4 rounded-lg font-semibold hover:bg-blue-50 transition text-center">
|
|
28
|
+
View on GitHub →
|
|
29
|
+
</a>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Features Section -->
|
|
35
|
+
<div class="max-w-6xl mx-auto px-4 py-16">
|
|
36
|
+
<h2 class="text-3xl font-bold mb-12 text-center">Features</h2>
|
|
37
|
+
<div class="grid md:grid-cols-3 gap-8">
|
|
38
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
39
|
+
<div class="text-4xl mb-4">🔍</div>
|
|
40
|
+
<h3 class="text-xl font-semibold mb-2">Auto-Detection</h3>
|
|
41
|
+
<p class="text-gray-600">Automatically detects Ruby and Rails versions from .ruby-version, Gemfile, and Gemfile.lock</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
44
|
+
<div class="text-4xl mb-4">📅</div>
|
|
45
|
+
<h3 class="text-xl font-semibold mb-2">EOL Data</h3>
|
|
46
|
+
<p class="text-gray-600">Fetches official EOL dates from endoflife.date with smart 24-hour caching</p>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
49
|
+
<div class="text-4xl mb-4">🎨</div>
|
|
50
|
+
<h3 class="text-xl font-semibold mb-2">Color-Coded</h3>
|
|
51
|
+
<p class="text-gray-600">Visual warnings: red for EOL, yellow for <6 months, green for safe</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
54
|
+
<div class="text-4xl mb-4">⚡</div>
|
|
55
|
+
<h3 class="text-xl font-semibold mb-2">Zero Config</h3>
|
|
56
|
+
<p class="text-gray-600">Just run harbinger scan - no setup or configuration required</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
59
|
+
<div class="text-4xl mb-4">💾</div>
|
|
60
|
+
<h3 class="text-xl font-semibold mb-2">Works Offline</h3>
|
|
61
|
+
<p class="text-gray-600">Smart caching means it works offline after initial data fetch</p>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
64
|
+
<div class="text-4xl mb-4">📊</div>
|
|
65
|
+
<h3 class="text-xl font-semibold mb-2">Dashboard View</h3>
|
|
66
|
+
<p class="text-gray-600">Track multiple projects and see EOL status at a glance with harbinger show</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
69
|
+
<div class="text-4xl mb-4">🔄</div>
|
|
70
|
+
<h3 class="text-xl font-semibold mb-2">Bulk Scanning</h3>
|
|
71
|
+
<p class="text-gray-600">Recursively scan entire directories to find and track all your projects</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="bg-white p-6 rounded-lg shadow-sm">
|
|
74
|
+
<div class="text-4xl mb-4">🧪</div>
|
|
75
|
+
<h3 class="text-xl font-semibold mb-2">Well Tested</h3>
|
|
76
|
+
<p class="text-gray-600">52 RSpec tests with 100% pass rate, built with TDD</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Usage Section -->
|
|
82
|
+
<div class="bg-white py-16">
|
|
83
|
+
<div class="max-w-4xl mx-auto px-4">
|
|
84
|
+
<h2 class="text-3xl font-bold mb-8">Quick Start</h2>
|
|
85
|
+
|
|
86
|
+
<div class="bg-gray-900 rounded-lg p-6 mb-8 font-mono text-sm overflow-x-auto">
|
|
87
|
+
<div class="text-gray-400"># Install via Homebrew</div>
|
|
88
|
+
<div class="mb-1"><span class="text-gray-400">$</span> <span class="text-green-400">brew tap RichD/harbinger</span></div>
|
|
89
|
+
<div class="mb-4"><span class="text-gray-400">$</span> <span class="text-green-400">brew install stackharbinger</span></div>
|
|
90
|
+
|
|
91
|
+
<div class="text-gray-400"># Or via RubyGems</div>
|
|
92
|
+
<div class="mb-4"><span class="text-gray-400">$</span> <span class="text-green-400">gem install stackharbinger</span></div>
|
|
93
|
+
|
|
94
|
+
<div class="text-gray-400"># Scan your project</div>
|
|
95
|
+
<div class="mb-4"><span class="text-gray-400">$</span> <span class="text-green-400">harbinger scan</span></div>
|
|
96
|
+
|
|
97
|
+
<div class="text-gray-400"># Output:</div>
|
|
98
|
+
<div class="text-white">
|
|
99
|
+
<div class="mb-2">Detected versions:</div>
|
|
100
|
+
<div class="ml-4 text-white">Ruby: 3.2.0</div>
|
|
101
|
+
<div class="ml-4 text-white mb-2">Rails: 7.0.8</div>
|
|
102
|
+
<div class="mb-2">Ruby 3.2.0:</div>
|
|
103
|
+
<div class="ml-4 text-green-400">EOL Date: 2026-03-31</div>
|
|
104
|
+
<div class="ml-4 text-green-400 mb-2">Status: 437 days remaining</div>
|
|
105
|
+
<div class="mb-2">Rails 7.0.8:</div>
|
|
106
|
+
<div class="ml-4 text-red-400">EOL Date: 2025-06-01</div>
|
|
107
|
+
<div class="ml-4 text-red-400">Status: ALREADY EOL</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="prose max-w-none">
|
|
112
|
+
<h3 class="text-2xl font-semibold mb-4">Commands</h3>
|
|
113
|
+
<ul class="space-y-3 text-gray-700">
|
|
114
|
+
<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
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --save</code> - Save project for tracking</li>
|
|
116
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --recursive</code> - Scan all projects in a directory</li>
|
|
117
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show</code> - View dashboard of all tracked projects</li>
|
|
118
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">harbinger update</code> - Force refresh EOL data from API</li>
|
|
119
|
+
<li><code class="bg-gray-100 px-2 py-1 rounded">harbinger version</code> - Show harbinger version</li>
|
|
120
|
+
</ul>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Roadmap Section -->
|
|
126
|
+
<div class="max-w-6xl mx-auto px-4 py-16">
|
|
127
|
+
<h2 class="text-3xl font-bold mb-8 text-center">Roadmap</h2>
|
|
128
|
+
<div class="grid md:grid-cols-3 gap-8">
|
|
129
|
+
<div>
|
|
130
|
+
<h3 class="text-xl font-semibold mb-4 text-green-600">✅ V0.2.0 (Current)</h3>
|
|
131
|
+
<ul class="space-y-2 text-gray-600">
|
|
132
|
+
<li>• Dashboard view</li>
|
|
133
|
+
<li>• Project tracking</li>
|
|
134
|
+
<li>• Recursive scanning</li>
|
|
135
|
+
<li>• Homebrew distribution</li>
|
|
136
|
+
</ul>
|
|
137
|
+
</div>
|
|
138
|
+
<div>
|
|
139
|
+
<h3 class="text-xl font-semibold mb-4 text-blue-600">📋 V0.3.0 (Planned)</h3>
|
|
140
|
+
<ul class="space-y-2 text-gray-600">
|
|
141
|
+
<li>• PostgreSQL detection</li>
|
|
142
|
+
<li>• MySQL detection</li>
|
|
143
|
+
<li>• Rescan command</li>
|
|
144
|
+
<li>• Export to JSON/CSV</li>
|
|
145
|
+
</ul>
|
|
146
|
+
</div>
|
|
147
|
+
<div>
|
|
148
|
+
<h3 class="text-xl font-semibold mb-4 text-purple-600">🚀 V1.0 (Future)</h3>
|
|
149
|
+
<ul class="space-y-2 text-gray-600">
|
|
150
|
+
<li>• Python support</li>
|
|
151
|
+
<li>• Node.js support</li>
|
|
152
|
+
<li>• Go support</li>
|
|
153
|
+
<li>• Rust support</li>
|
|
154
|
+
</ul>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Links Section -->
|
|
160
|
+
<div class="bg-gray-900 text-white py-12">
|
|
161
|
+
<div class="max-w-6xl mx-auto px-4 text-center">
|
|
162
|
+
<div class="flex flex-col sm:flex-row justify-center gap-6 mb-8">
|
|
163
|
+
<a href="https://github.com/RichD/harbinger" class="bg-white text-gray-900 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 transition">
|
|
164
|
+
GitHub
|
|
165
|
+
</a>
|
|
166
|
+
<a href="https://rubygems.org/gems/stackharbinger" class="bg-red-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-red-700 transition">
|
|
167
|
+
RubyGems
|
|
168
|
+
</a>
|
|
169
|
+
<a href="https://github.com/RichD/harbinger/blob/main/README.md" class="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700 transition">
|
|
170
|
+
Documentation
|
|
171
|
+
</a>
|
|
172
|
+
</div>
|
|
173
|
+
<p class="text-gray-400 text-sm">
|
|
174
|
+
Built with ❤️ using Ruby and Thor | EOL data from <a href="https://endoflife.date" class="underline hover:text-white">endoflife.date</a>
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Harbinger
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Abstract base class for database version detection in Rails projects
|
|
8
|
+
# Provides common functionality for detecting database versions from Rails projects
|
|
9
|
+
class DatabaseDetector
|
|
10
|
+
attr_reader :project_path
|
|
11
|
+
|
|
12
|
+
def initialize(project_path)
|
|
13
|
+
@project_path = project_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Main detection method - returns version string or nil
|
|
17
|
+
def detect
|
|
18
|
+
return nil unless database_detected?
|
|
19
|
+
|
|
20
|
+
# Try shell command first (actual database version)
|
|
21
|
+
version = detect_from_shell
|
|
22
|
+
return version if version
|
|
23
|
+
|
|
24
|
+
# Fallback to gem version from Gemfile.lock
|
|
25
|
+
detect_from_gemfile_lock
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if database.yml indicates this database is used
|
|
29
|
+
def database_detected?
|
|
30
|
+
return false unless database_yml_exists?
|
|
31
|
+
|
|
32
|
+
config = parse_database_yml
|
|
33
|
+
return false unless config
|
|
34
|
+
|
|
35
|
+
# Check production or default section
|
|
36
|
+
section = config["production"] || config["default"] || config[config.keys.first]
|
|
37
|
+
return false unless section
|
|
38
|
+
|
|
39
|
+
adapter = extract_adapter_from_section(section)
|
|
40
|
+
return false unless adapter
|
|
41
|
+
|
|
42
|
+
Array(adapter_name).any? { |name| adapter == name }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
# Extract adapter from database config section
|
|
48
|
+
# Handles both single-database and multi-database (Rails 6+) configurations
|
|
49
|
+
def extract_adapter_from_section(section)
|
|
50
|
+
# Single database: { "adapter" => "postgresql", ... }
|
|
51
|
+
return section["adapter"] if section["adapter"]
|
|
52
|
+
|
|
53
|
+
# Multi-database: { "primary" => { "adapter" => "postgresql", ... }, "cache" => { ... } }
|
|
54
|
+
# Check primary first, then fall back to first nested config
|
|
55
|
+
nested_config = section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
|
|
56
|
+
nested_config["adapter"] if nested_config
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Abstract method - must be implemented by subclasses
|
|
60
|
+
# Returns the adapter name(s) to look for in database.yml
|
|
61
|
+
def adapter_name
|
|
62
|
+
raise NotImplementedError, "Subclasses must implement adapter_name"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Abstract method - must be implemented by subclasses
|
|
66
|
+
# Detects version from shell command (e.g., psql --version)
|
|
67
|
+
def detect_from_shell
|
|
68
|
+
raise NotImplementedError, "Subclasses must implement detect_from_shell"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Abstract method - must be implemented by subclasses
|
|
72
|
+
# Detects version from Gemfile.lock gem version
|
|
73
|
+
def detect_from_gemfile_lock
|
|
74
|
+
raise NotImplementedError, "Subclasses must implement detect_from_gemfile_lock"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Read and parse config/database.yml
|
|
78
|
+
def parse_database_yml
|
|
79
|
+
database_yml_path = File.join(project_path, "config", "database.yml")
|
|
80
|
+
return nil unless File.exist?(database_yml_path)
|
|
81
|
+
|
|
82
|
+
content = File.read(database_yml_path)
|
|
83
|
+
YAML.safe_load(content, aliases: true)
|
|
84
|
+
rescue Psych::SyntaxError, StandardError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if database.yml exists
|
|
89
|
+
def database_yml_exists?
|
|
90
|
+
File.exist?(File.join(project_path, "config", "database.yml"))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Read and parse Gemfile.lock
|
|
94
|
+
def parse_gemfile_lock
|
|
95
|
+
gemfile_lock_path = File.join(project_path, "Gemfile.lock")
|
|
96
|
+
return nil unless File.exist?(gemfile_lock_path)
|
|
97
|
+
|
|
98
|
+
File.read(gemfile_lock_path)
|
|
99
|
+
rescue StandardError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Extract gem version from Gemfile.lock content
|
|
104
|
+
def extract_gem_version(gemfile_lock_content, gem_name)
|
|
105
|
+
return nil unless gemfile_lock_content
|
|
106
|
+
|
|
107
|
+
# Match pattern like: " pg (1.5.4)"
|
|
108
|
+
match = gemfile_lock_content.match(/^\s{4}#{Regexp.escape(gem_name)}\s+\(([^)]+)\)/)
|
|
109
|
+
match[1] if match
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Execute shell command safely
|
|
113
|
+
def execute_command(command)
|
|
114
|
+
output = `#{command} 2>&1`.strip
|
|
115
|
+
return nil unless $?.success?
|
|
116
|
+
|
|
117
|
+
output
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "database_detector"
|
|
4
|
+
|
|
5
|
+
module Harbinger
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Detects MySQL version from Rails projects
|
|
8
|
+
# Supports both mysql2 and trilogy adapters
|
|
9
|
+
class MysqlDetector < DatabaseDetector
|
|
10
|
+
protected
|
|
11
|
+
|
|
12
|
+
def adapter_name
|
|
13
|
+
["mysql2", "trilogy"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def detect_from_shell
|
|
17
|
+
# Skip shell command if database is remote
|
|
18
|
+
return nil if remote_database?
|
|
19
|
+
|
|
20
|
+
# Try mysql command first, then mysqld
|
|
21
|
+
output = execute_command("mysql --version") || execute_command("mysqld --version")
|
|
22
|
+
return nil unless output
|
|
23
|
+
|
|
24
|
+
# Parse: "mysql Ver 8.0.33" or "mysqld Ver 8.0.33"
|
|
25
|
+
# Also handles MariaDB: "mysql Ver 15.1 Distrib 10.11.2-MariaDB"
|
|
26
|
+
match = output.match(/Ver\s+(?:\d+\.\d+\s+Distrib\s+)?(\d+\.\d+\.\d+)/)
|
|
27
|
+
match[1] if match
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def detect_from_gemfile_lock
|
|
31
|
+
content = parse_gemfile_lock
|
|
32
|
+
return nil unless content
|
|
33
|
+
|
|
34
|
+
# Check which adapter is being used
|
|
35
|
+
config = parse_database_yml
|
|
36
|
+
return nil unless config
|
|
37
|
+
|
|
38
|
+
section = config["production"] || config["default"] || config[config.keys.first]
|
|
39
|
+
return nil unless section
|
|
40
|
+
|
|
41
|
+
adapter = extract_adapter_from_section(section)
|
|
42
|
+
|
|
43
|
+
# Return appropriate gem version based on adapter
|
|
44
|
+
if adapter == "trilogy"
|
|
45
|
+
version = extract_gem_version(content, "trilogy")
|
|
46
|
+
version ? "#{version} (trilogy gem)" : nil
|
|
47
|
+
else
|
|
48
|
+
version = extract_gem_version(content, "mysql2")
|
|
49
|
+
version ? "#{version} (mysql2 gem)" : nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Check if database configuration indicates a remote database
|
|
56
|
+
# (same logic as PostgresDetector)
|
|
57
|
+
def remote_database?
|
|
58
|
+
config = parse_database_yml
|
|
59
|
+
return false unless config
|
|
60
|
+
|
|
61
|
+
section = config["production"] || config["default"] || config[config.keys.first]
|
|
62
|
+
return false unless section
|
|
63
|
+
|
|
64
|
+
db_config = if section["adapter"]
|
|
65
|
+
section
|
|
66
|
+
else
|
|
67
|
+
section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return false unless db_config
|
|
71
|
+
|
|
72
|
+
host = db_config["host"]
|
|
73
|
+
|
|
74
|
+
# No host specified = localhost (Unix socket)
|
|
75
|
+
return false if host.nil? || host.empty?
|
|
76
|
+
|
|
77
|
+
# Explicit localhost indicators
|
|
78
|
+
local_hosts = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]
|
|
79
|
+
!local_hosts.include?(host.to_s.downcase)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "database_detector"
|
|
4
|
+
|
|
5
|
+
module Harbinger
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Detects PostgreSQL version from Rails projects
|
|
8
|
+
class PostgresDetector < DatabaseDetector
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def adapter_name
|
|
12
|
+
"postgresql"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def detect_from_shell
|
|
16
|
+
# Skip shell command if database is remote
|
|
17
|
+
# (shell gives client version, not server version)
|
|
18
|
+
return nil if remote_database?
|
|
19
|
+
|
|
20
|
+
output = execute_command("psql --version")
|
|
21
|
+
return nil unless output
|
|
22
|
+
|
|
23
|
+
# Parse: "psql (PostgreSQL) 15.3" or "psql (PostgreSQL) 15.3 (Ubuntu 15.3-1)"
|
|
24
|
+
match = output.match(/PostgreSQL\)\s+(\d+\.\d+)/)
|
|
25
|
+
match[1] if match
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def detect_from_gemfile_lock
|
|
29
|
+
content = parse_gemfile_lock
|
|
30
|
+
version = extract_gem_version(content, "pg")
|
|
31
|
+
version ? "#{version} (pg gem)" : nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Check if database configuration indicates a remote database
|
|
37
|
+
def remote_database?
|
|
38
|
+
config = parse_database_yml
|
|
39
|
+
return false unless config
|
|
40
|
+
|
|
41
|
+
# Get the section with database config
|
|
42
|
+
section = config["production"] || config["default"] || config[config.keys.first]
|
|
43
|
+
return false unless section
|
|
44
|
+
|
|
45
|
+
# Handle multi-database config
|
|
46
|
+
db_config = if section["adapter"]
|
|
47
|
+
section
|
|
48
|
+
else
|
|
49
|
+
section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return false unless db_config
|
|
53
|
+
|
|
54
|
+
host = db_config["host"]
|
|
55
|
+
|
|
56
|
+
# No host specified = localhost (Unix socket)
|
|
57
|
+
return false if host.nil? || host.empty?
|
|
58
|
+
|
|
59
|
+
# Explicit localhost indicators
|
|
60
|
+
local_hosts = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]
|
|
61
|
+
!local_hosts.include?(host.to_s.downcase)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/harbinger/cli.rb
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "date"
|
|
5
|
+
require "tty-table"
|
|
5
6
|
require_relative "version"
|
|
6
7
|
require "harbinger/analyzers/ruby_detector"
|
|
7
8
|
require "harbinger/analyzers/rails_analyzer"
|
|
9
|
+
require "harbinger/analyzers/database_detector"
|
|
10
|
+
require "harbinger/analyzers/postgres_detector"
|
|
11
|
+
require "harbinger/analyzers/mysql_detector"
|
|
8
12
|
require "harbinger/eol_fetcher"
|
|
13
|
+
require "harbinger/config_manager"
|
|
9
14
|
|
|
10
15
|
module Harbinger
|
|
11
16
|
class CLI < Thor
|
|
@@ -13,48 +18,309 @@ module Harbinger
|
|
|
13
18
|
true
|
|
14
19
|
end
|
|
15
20
|
|
|
16
|
-
desc "scan
|
|
17
|
-
option :path, type: :string, aliases: "-p", desc: "Path to project directory"
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
desc "scan", "Scan a project directory and detect versions"
|
|
22
|
+
option :path, type: :string, aliases: "-p", desc: "Path to project directory (defaults to current directory)"
|
|
23
|
+
option :save, type: :boolean, aliases: "-s", desc: "Save project to config for dashboard"
|
|
24
|
+
option :recursive, type: :boolean, aliases: "-r", desc: "Recursively scan all subdirectories with Gemfiles"
|
|
25
|
+
def scan
|
|
26
|
+
project_path = options[:path] || Dir.pwd
|
|
20
27
|
|
|
21
28
|
unless File.directory?(project_path)
|
|
22
29
|
say "Error: #{project_path} is not a valid directory", :red
|
|
23
30
|
exit 1
|
|
24
31
|
end
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
if options[:recursive]
|
|
34
|
+
scan_recursive(project_path)
|
|
35
|
+
else
|
|
36
|
+
scan_single(project_path)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "show", "Show EOL status for tracked projects"
|
|
41
|
+
def show
|
|
42
|
+
config_manager = ConfigManager.new
|
|
43
|
+
projects = config_manager.list_projects
|
|
44
|
+
|
|
45
|
+
if projects.empty?
|
|
46
|
+
say "No projects tracked yet.", :yellow
|
|
47
|
+
say "Use 'harbinger scan --save' to add projects", :cyan
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
say "Tracked Projects (#{projects.size})", :cyan
|
|
52
|
+
say "=" * 80, :cyan
|
|
53
|
+
|
|
54
|
+
fetcher = EolFetcher.new
|
|
55
|
+
rows = []
|
|
56
|
+
|
|
57
|
+
projects.each do |name, data|
|
|
58
|
+
ruby_version = data["ruby"]
|
|
59
|
+
rails_version = data["rails"]
|
|
60
|
+
postgres_version = data["postgres"]
|
|
61
|
+
mysql_version = data["mysql"]
|
|
62
|
+
|
|
63
|
+
# Determine worst EOL status
|
|
64
|
+
worst_status = :green
|
|
65
|
+
status_text = "✓ Current"
|
|
66
|
+
|
|
67
|
+
if ruby_version && !ruby_version.empty?
|
|
68
|
+
ruby_eol = fetcher.eol_date_for("ruby", ruby_version)
|
|
69
|
+
if ruby_eol
|
|
70
|
+
days = days_until(ruby_eol)
|
|
71
|
+
status = eol_color(days)
|
|
72
|
+
worst_status = status if status_priority(status) > status_priority(worst_status)
|
|
73
|
+
if days < 0
|
|
74
|
+
status_text = "✗ Ruby EOL"
|
|
75
|
+
elsif days < 180
|
|
76
|
+
status_text = "⚠ Ruby ending soon"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if rails_version && !rails_version.empty?
|
|
82
|
+
rails_eol = fetcher.eol_date_for("rails", rails_version)
|
|
83
|
+
if rails_eol
|
|
84
|
+
days = days_until(rails_eol)
|
|
85
|
+
status = eol_color(days)
|
|
86
|
+
worst_status = status if status_priority(status) > status_priority(worst_status)
|
|
87
|
+
if days < 0
|
|
88
|
+
status_text = "✗ Rails EOL"
|
|
89
|
+
elsif days < 180 && !status_text.include?("EOL")
|
|
90
|
+
status_text = "⚠ Rails ending soon"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if postgres_version && !postgres_version.empty? && !postgres_version.include?("gem")
|
|
96
|
+
postgres_eol = fetcher.eol_date_for("postgresql", postgres_version)
|
|
97
|
+
if postgres_eol
|
|
98
|
+
days = days_until(postgres_eol)
|
|
99
|
+
status = eol_color(days)
|
|
100
|
+
worst_status = status if status_priority(status) > status_priority(worst_status)
|
|
101
|
+
if days < 0
|
|
102
|
+
status_text = "✗ PostgreSQL EOL"
|
|
103
|
+
elsif days < 180 && !status_text.include?("EOL")
|
|
104
|
+
status_text = "⚠ PostgreSQL ending soon"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if mysql_version && !mysql_version.empty? && !mysql_version.include?("gem")
|
|
110
|
+
mysql_eol = fetcher.eol_date_for("mysql", mysql_version)
|
|
111
|
+
if mysql_eol
|
|
112
|
+
days = days_until(mysql_eol)
|
|
113
|
+
status = eol_color(days)
|
|
114
|
+
worst_status = status if status_priority(status) > status_priority(worst_status)
|
|
115
|
+
if days < 0
|
|
116
|
+
status_text = "✗ MySQL EOL"
|
|
117
|
+
elsif days < 180 && !status_text.include?("EOL")
|
|
118
|
+
status_text = "⚠ MySQL ending soon"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
ruby_display = ruby_version && !ruby_version.empty? ? ruby_version : "-"
|
|
124
|
+
rails_display = rails_version && !rails_version.empty? ? rails_version : "-"
|
|
125
|
+
postgres_display = postgres_version && !postgres_version.empty? ? postgres_version : "-"
|
|
126
|
+
mysql_display = mysql_version && !mysql_version.empty? ? mysql_version : "-"
|
|
127
|
+
|
|
128
|
+
rows << [name, ruby_display, rails_display, postgres_display, mysql_display, colorize_status(status_text, worst_status)]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Sort by status priority (worst first), then by name
|
|
132
|
+
rows.sort_by! do |row|
|
|
133
|
+
status = row[5] # Status is now in column 5 (0-indexed)
|
|
134
|
+
priority = if status.include?("✗")
|
|
135
|
+
0
|
|
136
|
+
elsif status.include?("⚠")
|
|
137
|
+
1
|
|
138
|
+
else
|
|
139
|
+
2
|
|
140
|
+
end
|
|
141
|
+
[priority, row[0]]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
table = TTY::Table.new(
|
|
145
|
+
header: ["Project", "Ruby", "Rails", "PostgreSQL", "MySQL", "Status"],
|
|
146
|
+
rows: rows
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
puts table.render(:unicode, padding: [0, 1])
|
|
150
|
+
|
|
151
|
+
say "\nUse 'harbinger scan --path <project>' to update a project", :cyan
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
desc "update", "Force refresh EOL data from endoflife.date"
|
|
155
|
+
def update
|
|
156
|
+
say "Updating EOL data...", :cyan
|
|
157
|
+
|
|
158
|
+
fetcher = EolFetcher.new
|
|
159
|
+
products = %w[ruby rails postgresql mysql]
|
|
160
|
+
|
|
161
|
+
products.each do |product|
|
|
162
|
+
say "Fetching #{product}...", :white
|
|
163
|
+
data = fetcher.fetch(product)
|
|
164
|
+
|
|
165
|
+
if data
|
|
166
|
+
say " ✓ #{product.capitalize}: #{data.length} versions cached", :green
|
|
167
|
+
else
|
|
168
|
+
say " ✗ #{product.capitalize}: Failed to fetch", :red
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
say "\nEOL data updated successfully!", :green
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
desc "rescan", "Re-scan all tracked projects and update versions"
|
|
176
|
+
option :verbose, type: :boolean, aliases: "-v", desc: "Show detailed output for each project"
|
|
177
|
+
def rescan
|
|
178
|
+
config_manager = ConfigManager.new
|
|
179
|
+
projects = config_manager.list_projects
|
|
180
|
+
|
|
181
|
+
if projects.empty?
|
|
182
|
+
say "No projects tracked yet.", :yellow
|
|
183
|
+
say "Use 'harbinger scan --save' to add projects", :cyan
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
say "Re-scanning #{projects.size} tracked project(s)...\n\n", :cyan
|
|
188
|
+
|
|
189
|
+
updated_count = 0
|
|
190
|
+
removed_count = 0
|
|
191
|
+
|
|
192
|
+
projects.each_with_index do |(name, data), index|
|
|
193
|
+
project_path = data["path"]
|
|
194
|
+
|
|
195
|
+
unless File.directory?(project_path)
|
|
196
|
+
say "[#{index + 1}/#{projects.size}] #{name}: Path not found, removing from config", :yellow
|
|
197
|
+
config_manager.remove_project(name)
|
|
198
|
+
removed_count += 1
|
|
199
|
+
next
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if options[:verbose]
|
|
203
|
+
say "=" * 60, :cyan
|
|
204
|
+
say "[#{index + 1}/#{projects.size}] Re-scanning #{name}", :cyan
|
|
205
|
+
say "=" * 60, :cyan
|
|
206
|
+
scan_single(project_path)
|
|
207
|
+
else
|
|
208
|
+
say "[#{index + 1}/#{projects.size}] #{name}...", :white
|
|
209
|
+
|
|
210
|
+
# Detect versions quietly
|
|
211
|
+
ruby_detector = Analyzers::RubyDetector.new(project_path)
|
|
212
|
+
rails_analyzer = Analyzers::RailsAnalyzer.new(project_path)
|
|
213
|
+
postgres_detector = Analyzers::PostgresDetector.new(project_path)
|
|
214
|
+
mysql_detector = Analyzers::MysqlDetector.new(project_path)
|
|
215
|
+
|
|
216
|
+
ruby_version = ruby_detector.detect
|
|
217
|
+
rails_version = rails_analyzer.detect
|
|
218
|
+
postgres_version = postgres_detector.detect
|
|
219
|
+
mysql_version = mysql_detector.detect
|
|
220
|
+
|
|
221
|
+
# Save to config
|
|
222
|
+
config_manager.save_project(
|
|
223
|
+
name: name,
|
|
224
|
+
path: project_path,
|
|
225
|
+
versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version, mysql: mysql_version }.compact
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
updated_count += 1
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
say "\n✓ Updated #{updated_count} project(s)", :green
|
|
233
|
+
say "✓ Removed #{removed_count} project(s) with missing directories", :yellow if removed_count > 0
|
|
234
|
+
say "\nView updated projects with: harbinger show", :cyan
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
desc "version", "Show harbinger version"
|
|
238
|
+
def version
|
|
239
|
+
say "Harbinger version #{Harbinger::VERSION}", :cyan
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def scan_recursive(base_path)
|
|
245
|
+
say "Scanning #{base_path} recursively for Ruby projects...", :cyan
|
|
246
|
+
|
|
247
|
+
# Find all directories with Gemfiles
|
|
248
|
+
gemfile_dirs = Dir.glob(File.join(base_path, "**/Gemfile"))
|
|
249
|
+
.map { |f| File.dirname(f) }
|
|
250
|
+
.sort
|
|
251
|
+
|
|
252
|
+
if gemfile_dirs.empty?
|
|
253
|
+
say "\nNo Ruby projects found (no Gemfile detected)", :yellow
|
|
254
|
+
return
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
say "Found #{gemfile_dirs.length} project(s)\n\n", :green
|
|
258
|
+
|
|
259
|
+
gemfile_dirs.each_with_index do |project_path, index|
|
|
260
|
+
say "=" * 60, :cyan
|
|
261
|
+
say "[#{index + 1}/#{gemfile_dirs.length}] #{project_path}", :cyan
|
|
262
|
+
say "=" * 60, :cyan
|
|
263
|
+
scan_single(project_path)
|
|
264
|
+
say "\n" unless index == gemfile_dirs.length - 1
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if options[:save]
|
|
268
|
+
say "\n✓ Saved #{gemfile_dirs.length} project(s) to config", :green
|
|
269
|
+
say "View all tracked projects with: harbinger show", :cyan
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def scan_single(project_path)
|
|
274
|
+
say "Scanning #{project_path}...", :cyan unless options[:recursive]
|
|
27
275
|
|
|
28
276
|
# Detect versions
|
|
29
277
|
ruby_detector = Analyzers::RubyDetector.new(project_path)
|
|
30
278
|
rails_analyzer = Analyzers::RailsAnalyzer.new(project_path)
|
|
279
|
+
postgres_detector = Analyzers::PostgresDetector.new(project_path)
|
|
280
|
+
mysql_detector = Analyzers::MysqlDetector.new(project_path)
|
|
31
281
|
|
|
32
282
|
ruby_version = ruby_detector.detect
|
|
33
283
|
rails_version = rails_analyzer.detect
|
|
284
|
+
postgres_version = postgres_detector.detect
|
|
285
|
+
mysql_version = mysql_detector.detect
|
|
34
286
|
|
|
35
287
|
ruby_present = ruby_detector.ruby_detected?
|
|
36
288
|
rails_present = rails_analyzer.rails_detected?
|
|
289
|
+
postgres_present = postgres_detector.database_detected?
|
|
290
|
+
mysql_present = mysql_detector.database_detected?
|
|
37
291
|
|
|
38
292
|
# Display results
|
|
39
293
|
say "\nDetected versions:", :green
|
|
40
294
|
if ruby_version
|
|
41
|
-
say " Ruby:
|
|
295
|
+
say " Ruby: #{ruby_version}", :white
|
|
42
296
|
elsif ruby_present
|
|
43
|
-
say " Ruby:
|
|
297
|
+
say " Ruby: Present (version not specified - add .ruby-version or ruby declaration in Gemfile)", :yellow
|
|
44
298
|
else
|
|
45
|
-
say " Ruby:
|
|
299
|
+
say " Ruby: Not a Ruby project", :red
|
|
46
300
|
end
|
|
47
301
|
|
|
48
302
|
if rails_version
|
|
49
|
-
say " Rails:
|
|
303
|
+
say " Rails: #{rails_version}", :white
|
|
50
304
|
elsif rails_present
|
|
51
|
-
say " Rails:
|
|
305
|
+
say " Rails: Present (version not found in Gemfile.lock)", :yellow
|
|
52
306
|
else
|
|
53
|
-
say " Rails:
|
|
307
|
+
say " Rails: Not detected", :yellow
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
if postgres_version
|
|
311
|
+
say " PostgreSQL: #{postgres_version}", :white
|
|
312
|
+
elsif postgres_present
|
|
313
|
+
say " PostgreSQL: Present (version not detected)", :yellow
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
if mysql_version
|
|
317
|
+
say " MySQL: #{mysql_version}", :white
|
|
318
|
+
elsif mysql_present
|
|
319
|
+
say " MySQL: Present (version not detected)", :yellow
|
|
54
320
|
end
|
|
55
321
|
|
|
56
322
|
# Fetch and display EOL dates
|
|
57
|
-
if ruby_version || rails_version
|
|
323
|
+
if ruby_version || rails_version || postgres_version || mysql_version
|
|
58
324
|
say "\nFetching EOL data...", :cyan
|
|
59
325
|
fetcher = EolFetcher.new
|
|
60
326
|
|
|
@@ -65,42 +331,44 @@ module Harbinger
|
|
|
65
331
|
if rails_version
|
|
66
332
|
display_eol_info(fetcher, "Rails", rails_version)
|
|
67
333
|
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
desc "show", "Show EOL status for tracked projects"
|
|
72
|
-
def show
|
|
73
|
-
say "Show command coming soon!", :yellow
|
|
74
|
-
say "Use 'harbinger scan' to check a project's EOL status", :white
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
desc "update", "Force refresh EOL data from endoflife.date"
|
|
78
|
-
def update
|
|
79
|
-
say "Updating EOL data...", :cyan
|
|
80
|
-
|
|
81
|
-
fetcher = EolFetcher.new
|
|
82
|
-
products = %w[ruby rails]
|
|
83
334
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
335
|
+
if postgres_version && !postgres_version.include?("gem")
|
|
336
|
+
display_eol_info(fetcher, "PostgreSQL", postgres_version)
|
|
337
|
+
end
|
|
87
338
|
|
|
88
|
-
if
|
|
89
|
-
|
|
90
|
-
else
|
|
91
|
-
say " ✗ #{product.capitalize}: Failed to fetch", :red
|
|
339
|
+
if mysql_version && !mysql_version.include?("gem")
|
|
340
|
+
display_eol_info(fetcher, "MySQL", mysql_version)
|
|
92
341
|
end
|
|
93
342
|
end
|
|
94
343
|
|
|
95
|
-
|
|
344
|
+
# Save to config if --save flag is used
|
|
345
|
+
if options[:save] && !options[:recursive]
|
|
346
|
+
save_to_config(project_path, ruby_version, rails_version, postgres_version, mysql_version)
|
|
347
|
+
elsif options[:save] && options[:recursive]
|
|
348
|
+
# In recursive mode, save without the confirmation message for each project
|
|
349
|
+
config_manager = ConfigManager.new
|
|
350
|
+
project_name = File.basename(project_path)
|
|
351
|
+
config_manager.save_project(
|
|
352
|
+
name: project_name,
|
|
353
|
+
path: project_path,
|
|
354
|
+
versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version, mysql: mysql_version }.compact
|
|
355
|
+
)
|
|
356
|
+
end
|
|
96
357
|
end
|
|
97
358
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
end
|
|
359
|
+
def save_to_config(project_path, ruby_version, rails_version, postgres_version = nil, mysql_version = nil)
|
|
360
|
+
config_manager = ConfigManager.new
|
|
361
|
+
project_name = File.basename(project_path)
|
|
102
362
|
|
|
103
|
-
|
|
363
|
+
config_manager.save_project(
|
|
364
|
+
name: project_name,
|
|
365
|
+
path: project_path,
|
|
366
|
+
versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version, mysql: mysql_version }.compact
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
say "\n✓ Saved to config as '#{project_name}'", :green
|
|
370
|
+
say "View all tracked projects with: harbinger show", :cyan
|
|
371
|
+
end
|
|
104
372
|
|
|
105
373
|
def display_eol_info(fetcher, product, version)
|
|
106
374
|
product_key = product.downcase
|
|
@@ -145,5 +413,29 @@ module Harbinger
|
|
|
145
413
|
"#{days} days remaining"
|
|
146
414
|
end
|
|
147
415
|
end
|
|
416
|
+
|
|
417
|
+
def status_priority(color)
|
|
418
|
+
case color
|
|
419
|
+
when :red
|
|
420
|
+
2
|
|
421
|
+
when :yellow
|
|
422
|
+
1
|
|
423
|
+
else
|
|
424
|
+
0
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def colorize_status(text, color)
|
|
429
|
+
case color
|
|
430
|
+
when :red
|
|
431
|
+
"\e[31m#{text}\e[0m"
|
|
432
|
+
when :yellow
|
|
433
|
+
"\e[33m#{text}\e[0m"
|
|
434
|
+
when :green
|
|
435
|
+
"\e[32m#{text}\e[0m"
|
|
436
|
+
else
|
|
437
|
+
text
|
|
438
|
+
end
|
|
439
|
+
end
|
|
148
440
|
end
|
|
149
441
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Harbinger
|
|
8
|
+
class ConfigManager
|
|
9
|
+
def initialize(config_dir: default_config_dir)
|
|
10
|
+
@config_dir = config_dir
|
|
11
|
+
@config_file = File.join(@config_dir, "config.yml")
|
|
12
|
+
FileUtils.mkdir_p(@config_dir)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def save_project(name:, path:, versions: {})
|
|
16
|
+
config = load_config
|
|
17
|
+
config["projects"] ||= {}
|
|
18
|
+
|
|
19
|
+
config["projects"][name] = {
|
|
20
|
+
"path" => path,
|
|
21
|
+
"last_scanned" => Time.now.iso8601
|
|
22
|
+
}.merge(versions.transform_keys(&:to_s))
|
|
23
|
+
|
|
24
|
+
write_config(config)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def list_projects
|
|
28
|
+
config = load_config
|
|
29
|
+
config["projects"] || {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get_project(name)
|
|
33
|
+
list_projects[name]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remove_project(name)
|
|
37
|
+
config = load_config
|
|
38
|
+
return unless config["projects"]
|
|
39
|
+
|
|
40
|
+
config["projects"].delete(name)
|
|
41
|
+
write_config(config)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def project_count
|
|
45
|
+
list_projects.size
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :config_dir, :config_file
|
|
51
|
+
|
|
52
|
+
def default_config_dir
|
|
53
|
+
File.join(Dir.home, ".harbinger")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_config
|
|
57
|
+
return {} unless File.exist?(config_file)
|
|
58
|
+
|
|
59
|
+
YAML.load_file(config_file) || {}
|
|
60
|
+
rescue Psych::SyntaxError, StandardError
|
|
61
|
+
{}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def write_config(config)
|
|
65
|
+
File.write(config_file, YAML.dump(config))
|
|
66
|
+
rescue StandardError
|
|
67
|
+
# Silently fail if we can't write
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -40,9 +40,14 @@ module Harbinger
|
|
|
40
40
|
# Extract major.minor from version (e.g., "3.2.1" -> "3.2")
|
|
41
41
|
version_parts = version.split(".")
|
|
42
42
|
major_minor = "#{version_parts[0]}.#{version_parts[1]}"
|
|
43
|
+
major = version_parts[0]
|
|
43
44
|
|
|
44
|
-
#
|
|
45
|
+
# Try major.minor first (e.g., "8.0" for MySQL, "3.2" for Ruby)
|
|
45
46
|
entry = data.find { |item| item["cycle"] == major_minor }
|
|
47
|
+
return entry["eol"] if entry
|
|
48
|
+
|
|
49
|
+
# Fall back to major only (e.g., "16" for PostgreSQL)
|
|
50
|
+
entry = data.find { |item| item["cycle"] == major }
|
|
46
51
|
entry ? entry["eol"] : nil
|
|
47
52
|
end
|
|
48
53
|
|
data/lib/harbinger/version.rb
CHANGED
data/lib/harbinger.rb
CHANGED
|
@@ -4,6 +4,9 @@ require_relative "harbinger/version"
|
|
|
4
4
|
require_relative "harbinger/cli"
|
|
5
5
|
require_relative "harbinger/analyzers/ruby_detector"
|
|
6
6
|
require_relative "harbinger/analyzers/rails_analyzer"
|
|
7
|
+
require_relative "harbinger/analyzers/database_detector"
|
|
8
|
+
require_relative "harbinger/analyzers/postgres_detector"
|
|
9
|
+
require_relative "harbinger/analyzers/mysql_detector"
|
|
7
10
|
require_relative "harbinger/eol_fetcher"
|
|
8
11
|
|
|
9
12
|
module Harbinger
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stackharbinger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rich Dabrowski
|
|
@@ -79,9 +79,8 @@ dependencies:
|
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0.21'
|
|
82
|
-
description: Harbinger monitors EOL dates for Ruby
|
|
83
|
-
|
|
84
|
-
ends.
|
|
82
|
+
description: Harbinger monitors EOL dates for Ruby and Rails. Auto-detects versions
|
|
83
|
+
from your projects and alerts you before support ends.
|
|
85
84
|
email:
|
|
86
85
|
- engineering@richd.net
|
|
87
86
|
executables:
|
|
@@ -92,11 +91,17 @@ files:
|
|
|
92
91
|
- CHANGELOG.md
|
|
93
92
|
- README.md
|
|
94
93
|
- Rakefile
|
|
94
|
+
- docs/CNAME
|
|
95
|
+
- docs/index.html
|
|
95
96
|
- exe/harbinger
|
|
96
97
|
- lib/harbinger.rb
|
|
98
|
+
- lib/harbinger/analyzers/database_detector.rb
|
|
99
|
+
- lib/harbinger/analyzers/mysql_detector.rb
|
|
100
|
+
- lib/harbinger/analyzers/postgres_detector.rb
|
|
97
101
|
- lib/harbinger/analyzers/rails_analyzer.rb
|
|
98
102
|
- lib/harbinger/analyzers/ruby_detector.rb
|
|
99
103
|
- lib/harbinger/cli.rb
|
|
104
|
+
- lib/harbinger/config_manager.rb
|
|
100
105
|
- lib/harbinger/eol_fetcher.rb
|
|
101
106
|
- lib/harbinger/version.rb
|
|
102
107
|
- sig/harbinger.rbs
|