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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78f93c3813cf5f8ec4540a11d5901f4832667fc76ac449c2cbc61899d61844af
4
- data.tar.gz: a713dcab3bf69bd8ba1bf3aa7ff26d841e2beff17a15ba5a4b33918338b4d093
3
+ metadata.gz: 0625ffb650537306abc4297407d0373c204c69412c7610254488c1006461bbcc
4
+ data.tar.gz: 65106dadaa487141d44e7772c57d9b0c59e18ab478517f0e94cad1440b8ab8b4
5
5
  SHA512:
6
- metadata.gz: '07888386c6149bbef7a607766cd92c59d34c2f38bdddd6dc06a97fb1734f21b9b5775c47fe5f54c81e71a459931b24dac9f3c3dbd30d49d63bdca8bd19fed3cc'
7
- data.tar.gz: 223b899768d2c1d6e7ad2bbfd5df74679e119e8d0d79b235b401035587e23dba52c624b86f831686f5d93fbd91177b245b60cb582291d22c5b3520ff11733f3e
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 and Rails projects, detects 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, 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`, and `Gemfile.lock`
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 still `harbinger` (shorter to type).
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: 3.2.0
48
- Rails: 7.0.8
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.1.0 (Beta) - Current
128
- - ✅ Ruby and Rails version detection
129
- - ✅ EOL data fetching and caching
130
- - ✅ CLI with scan and update commands
131
- - ✅ Color-coded status display
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.2.0 - Planned
134
- - 📊 Dashboard: `harbinger show` to see all tracked projects
135
- - 💾 Config management: Save and track multiple projects
136
- - 🐘 PostgreSQL version detection
137
- - 🗄️ MySQL version detection
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
- - 🏠 Homebrew distribution: `brew install harbinger`
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 [PATH]", "Scan a project directory and detect versions"
17
- option :path, type: :string, aliases: "-p", desc: "Path to project directory"
18
- def scan(path = nil)
19
- project_path = path || options[:path] || Dir.pwd
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
- say "Scanning #{project_path}...", :cyan
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: #{ruby_version}", :white
295
+ say " Ruby: #{ruby_version}", :white
42
296
  elsif ruby_present
43
- say " Ruby: Present (version not specified - add .ruby-version or ruby declaration in Gemfile)", :yellow
297
+ say " Ruby: Present (version not specified - add .ruby-version or ruby declaration in Gemfile)", :yellow
44
298
  else
45
- say " Ruby: Not a Ruby project", :red
299
+ say " Ruby: Not a Ruby project", :red
46
300
  end
47
301
 
48
302
  if rails_version
49
- say " Rails: #{rails_version}", :white
303
+ say " Rails: #{rails_version}", :white
50
304
  elsif rails_present
51
- say " Rails: Present (version not found in Gemfile.lock)", :yellow
305
+ say " Rails: Present (version not found in Gemfile.lock)", :yellow
52
306
  else
53
- say " Rails: Not detected", :yellow
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
- products.each do |product|
85
- say "Fetching #{product}...", :white
86
- data = fetcher.fetch(product)
335
+ if postgres_version && !postgres_version.include?("gem")
336
+ display_eol_info(fetcher, "PostgreSQL", postgres_version)
337
+ end
87
338
 
88
- if data
89
- say " ✓ #{product.capitalize}: #{data.length} versions cached", :green
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
- say "\nEOL data updated successfully!", :green
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
- desc "version", "Show harbinger version"
99
- def version
100
- say "Harbinger version #{Harbinger::VERSION}", :cyan
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
- private
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
- # Find matching cycle
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Harbinger
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
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.1.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, Rails, PostgreSQL and other technologies
83
- in your stack. Auto-detects versions from your projects and alerts you before support
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