stackharbinger 0.2.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: e9932e2c3197ac2e3a514ac213380f97c336f85c8c302745db6754f70f7c1a79
4
- data.tar.gz: 11f4b85563611e4921eba0666f29181bb26560d19ddb65dd0673e331276dd7bd
3
+ metadata.gz: 0625ffb650537306abc4297407d0373c204c69412c7610254488c1006461bbcc
4
+ data.tar.gz: 65106dadaa487141d44e7772c57d9b0c59e18ab478517f0e94cad1440b8ab8b4
5
5
  SHA512:
6
- metadata.gz: e0271ed33509bcf4fa3bce6a9f7b214753107131d3dfddbc248d68d5aea7cdb0b6a11eee4f4c2367f3060f1f1f7a7b81bca27b07e0f67a427c75eb0b0cd0efd5
7
- data.tar.gz: 71178783539fdf85565e517bad7ebc983f188ff13bfa179d77d4fc6dcea953308c8c93ccb7fe22441ea8af51d00e4a437708a2769a345f04667e4de6768f16fb
6
+ metadata.gz: 3922e4f871e0782ebf821ef3e01f91aebba249c908f81a3b8f94e550d0d7d68a8525118d61f78b326b801aeecb801fbd89f975b48c2c95bbafd12c7703ebcf7a
7
+ data.tar.gz: c50866bca2f2b69d995d01735d62da1cb44f436aa546e5a069d7f8b50b8d25e63d95969f4df85ed008cd7d893a7e8d92959efe12e29e7834199f7322455a9cb9
data/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ 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
+
10
50
  ## [0.2.0] - 2026-01-18
11
51
 
12
52
  ### Added
data/README.md CHANGED
@@ -2,20 +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)
13
14
  - šŸ“Š **Track multiple projects** with `--save` and view dashboard with `harbinger show`
14
- - šŸ”„ **Bulk scanning** with `--recursive` flag to scan entire directories
15
+ - šŸ”„ **Bulk operations** with `--recursive` scan and `rescan` command
15
16
  - šŸš€ **Zero configuration** - just run `harbinger scan`
16
17
 
17
18
  ## Installation
18
19
 
20
+ ### Homebrew (macOS)
21
+
22
+ ```bash
23
+ brew tap RichD/harbinger
24
+ brew install stackharbinger
25
+ ```
26
+
27
+ ### RubyGems
28
+
19
29
  ```bash
20
30
  gem install stackharbinger
21
31
  ```
@@ -26,7 +36,7 @@ Or add to your Gemfile:
26
36
  gem 'stackharbinger'
27
37
  ```
28
38
 
29
- The command is still `harbinger` (shorter to type).
39
+ The command is `harbinger` (shorter to type).
30
40
 
31
41
  ## Usage
32
42
 
@@ -52,8 +62,9 @@ harbinger scan --path ~/Projects --recursive --save
52
62
  Scanning /Users/you/Projects/my-app...
53
63
 
54
64
  Detected versions:
55
- Ruby: 3.2.0
56
- Rails: 7.0.8
65
+ Ruby: 3.2.0
66
+ Rails: 7.0.8
67
+ PostgreSQL: 16.11
57
68
 
58
69
  Fetching EOL data...
59
70
 
@@ -64,6 +75,10 @@ Ruby 3.2.0:
64
75
  Rails 7.0.8:
65
76
  EOL Date: 2025-06-01
66
77
  Status: ALREADY EOL (474 days ago)
78
+
79
+ PostgreSQL 16.11:
80
+ EOL Date: 2028-11-09
81
+ Status: 1026 days remaining
67
82
  ```
68
83
 
69
84
  ### View tracked projects
@@ -78,14 +93,24 @@ harbinger show
78
93
  ```
79
94
  Tracked Projects (10)
80
95
  ================================================================================
81
- ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
82
- │ Project │ Ruby │ Rails │ Status │
83
- ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
84
- │ ledger │ 3.3.0 │ 6.1.7.10 │ āœ— Rails EOL │
85
- │ option_tracker │ 3.3.0 │ 7.0.8.7 │ āœ— Rails EOL │
86
- │ CarCal │ - │ 8.0.2 │ āœ“ Current │
87
- │ job_tracker │ 3.3.0 │ 8.0.4 │ āœ“ Current │
88
- ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
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
89
114
  ```
90
115
 
91
116
  ### Update EOL data
@@ -106,6 +131,8 @@ harbinger version
106
131
  1. **Detection**: Harbinger looks for version info in your project:
107
132
  - Ruby: `.ruby-version`, `Gemfile` (`ruby "x.x.x"`), `Gemfile.lock` (RUBY VERSION)
108
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
109
136
 
110
137
  2. **EOL Data**: Fetches official EOL dates from [endoflife.date](https://endoflife.date) API
111
138
 
@@ -130,6 +157,22 @@ Ruby: Present (version not specified - add .ruby-version or ruby declaration in
130
157
 
131
158
  Parses `Gemfile.lock` for the rails gem version.
132
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
+
133
176
  ## Requirements
134
177
 
135
178
  - Ruby >= 3.1.0
@@ -154,23 +197,24 @@ bundle exec exe/harbinger scan .
154
197
 
155
198
  ## Roadmap
156
199
 
157
- ### V0.2.0 - Current
158
- - āœ… Dashboard: `harbinger show` to see all tracked projects
159
- - āœ… Config management: Save and track multiple projects with `--save`
160
- - āœ… Recursive scanning: `--recursive` flag to scan multiple projects at once
161
- - āœ… Enhanced project tracking with YAML config
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
162
206
 
163
- ### V0.3.0 - Planned
164
- - 🐘 PostgreSQL version detection
165
- - šŸ—„ļø MySQL version detection
166
- - šŸ”„ Rescan command to update all tracked projects
207
+ ### V0.4.0 - Planned
167
208
  - šŸ“‹ Export reports to JSON/CSV
209
+ - 🐳 Docker Compose database version detection
210
+ - šŸ”“ Redis version detection
211
+ - šŸƒ MongoDB version detection
168
212
 
169
213
  ### V1.0 - Future
170
214
  - šŸ Python support (pyproject.toml, requirements.txt)
171
215
  - šŸ“¦ Node.js support (package.json, .nvmrc)
172
216
  - šŸ¦€ Rust support (Cargo.toml)
173
- - šŸ  Homebrew distribution: `brew install harbinger`
217
+ - 🐘 Go support (go.mod)
174
218
 
175
219
  ### V2.0 - Vision
176
220
  - šŸ¤– AI-powered upgrade summaries
data/docs/index.html CHANGED
@@ -21,7 +21,8 @@
21
21
 
22
22
  <div class="flex flex-col sm:flex-row gap-4">
23
23
  <div class="bg-gray-900 rounded-lg p-4 font-mono text-sm">
24
- <span class="text-gray-400">$</span> <span class="text-green-400">gem install stackharbinger</span>
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>
25
26
  </div>
26
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">
27
28
  View on GitHub →
@@ -59,10 +60,20 @@
59
60
  <h3 class="text-xl font-semibold mb-2">Works Offline</h3>
60
61
  <p class="text-gray-600">Smart caching means it works offline after initial data fetch</p>
61
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>
62
73
  <div class="bg-white p-6 rounded-lg shadow-sm">
63
74
  <div class="text-4xl mb-4">🧪</div>
64
75
  <h3 class="text-xl font-semibold mb-2">Well Tested</h3>
65
- <p class="text-gray-600">36 RSpec tests with 100% pass rate, built with TDD</p>
76
+ <p class="text-gray-600">52 RSpec tests with 100% pass rate, built with TDD</p>
66
77
  </div>
67
78
  </div>
68
79
  </div>
@@ -73,7 +84,11 @@
73
84
  <h2 class="text-3xl font-bold mb-8">Quick Start</h2>
74
85
 
75
86
  <div class="bg-gray-900 rounded-lg p-6 mb-8 font-mono text-sm overflow-x-auto">
76
- <div class="text-gray-400"># Install</div>
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>
77
92
  <div class="mb-4"><span class="text-gray-400">$</span> <span class="text-green-400">gem install stackharbinger</span></div>
78
93
 
79
94
  <div class="text-gray-400"># Scan your project</div>
@@ -96,7 +111,10 @@
96
111
  <div class="prose max-w-none">
97
112
  <h3 class="text-2xl font-semibold mb-4">Commands</h3>
98
113
  <ul class="space-y-3 text-gray-700">
99
- <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan [PATH]</code> - Scan a project and show EOL status</li>
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>
100
118
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger update</code> - Force refresh EOL data from API</li>
101
119
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger version</code> - Show harbinger version</li>
102
120
  </ul>
@@ -109,21 +127,21 @@
109
127
  <h2 class="text-3xl font-bold mb-8 text-center">Roadmap</h2>
110
128
  <div class="grid md:grid-cols-3 gap-8">
111
129
  <div>
112
- <h3 class="text-xl font-semibold mb-4 text-green-600">āœ… V0.1.0 (Current)</h3>
130
+ <h3 class="text-xl font-semibold mb-4 text-green-600">āœ… V0.2.0 (Current)</h3>
113
131
  <ul class="space-y-2 text-gray-600">
114
- <li>• Ruby & Rails detection</li>
115
- <li>• EOL data fetching</li>
116
- <li>• CLI with color output</li>
117
- <li>• Smart caching</li>
132
+ <li>• Dashboard view</li>
133
+ <li>• Project tracking</li>
134
+ <li>• Recursive scanning</li>
135
+ <li>• Homebrew distribution</li>
118
136
  </ul>
119
137
  </div>
120
138
  <div>
121
- <h3 class="text-xl font-semibold mb-4 text-blue-600">šŸ“‹ V0.2.0 (Planned)</h3>
139
+ <h3 class="text-xl font-semibold mb-4 text-blue-600">šŸ“‹ V0.3.0 (Planned)</h3>
122
140
  <ul class="space-y-2 text-gray-600">
123
- <li>• Dashboard view</li>
124
- <li>• Config management</li>
125
141
  <li>• PostgreSQL detection</li>
126
142
  <li>• MySQL detection</li>
143
+ <li>• Rescan command</li>
144
+ <li>• Export to JSON/CSV</li>
127
145
  </ul>
128
146
  </div>
129
147
  <div>
@@ -131,8 +149,8 @@
131
149
  <ul class="space-y-2 text-gray-600">
132
150
  <li>• Python support</li>
133
151
  <li>• Node.js support</li>
134
- <li>• Go & Rust support</li>
135
- <li>• Homebrew distribution</li>
152
+ <li>• Go support</li>
153
+ <li>• Rust support</li>
136
154
  </ul>
137
155
  </div>
138
156
  </div>
@@ -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
@@ -6,6 +6,9 @@ require "tty-table"
6
6
  require_relative "version"
7
7
  require "harbinger/analyzers/ruby_detector"
8
8
  require "harbinger/analyzers/rails_analyzer"
9
+ require "harbinger/analyzers/database_detector"
10
+ require "harbinger/analyzers/postgres_detector"
11
+ require "harbinger/analyzers/mysql_detector"
9
12
  require "harbinger/eol_fetcher"
10
13
  require "harbinger/config_manager"
11
14
 
@@ -54,6 +57,8 @@ module Harbinger
54
57
  projects.each do |name, data|
55
58
  ruby_version = data["ruby"]
56
59
  rails_version = data["rails"]
60
+ postgres_version = data["postgres"]
61
+ mysql_version = data["mysql"]
57
62
 
58
63
  # Determine worst EOL status
59
64
  worst_status = :green
@@ -87,15 +92,45 @@ module Harbinger
87
92
  end
88
93
  end
89
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
+
90
123
  ruby_display = ruby_version && !ruby_version.empty? ? ruby_version : "-"
91
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 : "-"
92
127
 
93
- rows << [name, ruby_display, rails_display, colorize_status(status_text, worst_status)]
128
+ rows << [name, ruby_display, rails_display, postgres_display, mysql_display, colorize_status(status_text, worst_status)]
94
129
  end
95
130
 
96
131
  # Sort by status priority (worst first), then by name
97
132
  rows.sort_by! do |row|
98
- status = row[3]
133
+ status = row[5] # Status is now in column 5 (0-indexed)
99
134
  priority = if status.include?("āœ—")
100
135
  0
101
136
  elsif status.include?("⚠")
@@ -107,7 +142,7 @@ module Harbinger
107
142
  end
108
143
 
109
144
  table = TTY::Table.new(
110
- header: ["Project", "Ruby", "Rails", "Status"],
145
+ header: ["Project", "Ruby", "Rails", "PostgreSQL", "MySQL", "Status"],
111
146
  rows: rows
112
147
  )
113
148
 
@@ -121,7 +156,7 @@ module Harbinger
121
156
  say "Updating EOL data...", :cyan
122
157
 
123
158
  fetcher = EolFetcher.new
124
- products = %w[ruby rails]
159
+ products = %w[ruby rails postgresql mysql]
125
160
 
126
161
  products.each do |product|
127
162
  say "Fetching #{product}...", :white
@@ -137,6 +172,68 @@ module Harbinger
137
172
  say "\nEOL data updated successfully!", :green
138
173
  end
139
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
+
140
237
  desc "version", "Show harbinger version"
141
238
  def version
142
239
  say "Harbinger version #{Harbinger::VERSION}", :cyan
@@ -179,33 +276,51 @@ module Harbinger
179
276
  # Detect versions
180
277
  ruby_detector = Analyzers::RubyDetector.new(project_path)
181
278
  rails_analyzer = Analyzers::RailsAnalyzer.new(project_path)
279
+ postgres_detector = Analyzers::PostgresDetector.new(project_path)
280
+ mysql_detector = Analyzers::MysqlDetector.new(project_path)
182
281
 
183
282
  ruby_version = ruby_detector.detect
184
283
  rails_version = rails_analyzer.detect
284
+ postgres_version = postgres_detector.detect
285
+ mysql_version = mysql_detector.detect
185
286
 
186
287
  ruby_present = ruby_detector.ruby_detected?
187
288
  rails_present = rails_analyzer.rails_detected?
289
+ postgres_present = postgres_detector.database_detected?
290
+ mysql_present = mysql_detector.database_detected?
188
291
 
189
292
  # Display results
190
293
  say "\nDetected versions:", :green
191
294
  if ruby_version
192
- say " Ruby: #{ruby_version}", :white
295
+ say " Ruby: #{ruby_version}", :white
193
296
  elsif ruby_present
194
- 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
195
298
  else
196
- say " Ruby: Not a Ruby project", :red
299
+ say " Ruby: Not a Ruby project", :red
197
300
  end
198
301
 
199
302
  if rails_version
200
- say " Rails: #{rails_version}", :white
303
+ say " Rails: #{rails_version}", :white
201
304
  elsif rails_present
202
- say " Rails: Present (version not found in Gemfile.lock)", :yellow
305
+ say " Rails: Present (version not found in Gemfile.lock)", :yellow
203
306
  else
204
- 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
205
320
  end
206
321
 
207
322
  # Fetch and display EOL dates
208
- if ruby_version || rails_version
323
+ if ruby_version || rails_version || postgres_version || mysql_version
209
324
  say "\nFetching EOL data...", :cyan
210
325
  fetcher = EolFetcher.new
211
326
 
@@ -216,11 +331,19 @@ module Harbinger
216
331
  if rails_version
217
332
  display_eol_info(fetcher, "Rails", rails_version)
218
333
  end
334
+
335
+ if postgres_version && !postgres_version.include?("gem")
336
+ display_eol_info(fetcher, "PostgreSQL", postgres_version)
337
+ end
338
+
339
+ if mysql_version && !mysql_version.include?("gem")
340
+ display_eol_info(fetcher, "MySQL", mysql_version)
341
+ end
219
342
  end
220
343
 
221
344
  # Save to config if --save flag is used
222
345
  if options[:save] && !options[:recursive]
223
- save_to_config(project_path, ruby_version, rails_version)
346
+ save_to_config(project_path, ruby_version, rails_version, postgres_version, mysql_version)
224
347
  elsif options[:save] && options[:recursive]
225
348
  # In recursive mode, save without the confirmation message for each project
226
349
  config_manager = ConfigManager.new
@@ -228,19 +351,19 @@ module Harbinger
228
351
  config_manager.save_project(
229
352
  name: project_name,
230
353
  path: project_path,
231
- versions: { ruby: ruby_version, rails: rails_version }.compact
354
+ versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version, mysql: mysql_version }.compact
232
355
  )
233
356
  end
234
357
  end
235
358
 
236
- def save_to_config(project_path, ruby_version, rails_version)
359
+ def save_to_config(project_path, ruby_version, rails_version, postgres_version = nil, mysql_version = nil)
237
360
  config_manager = ConfigManager.new
238
361
  project_name = File.basename(project_path)
239
362
 
240
363
  config_manager.save_project(
241
364
  name: project_name,
242
365
  path: project_path,
243
- versions: { ruby: ruby_version, rails: rails_version }.compact
366
+ versions: { ruby: ruby_version, rails: rails_version, postgres: postgres_version, mysql: mysql_version }.compact
244
367
  )
245
368
 
246
369
  say "\nāœ“ Saved to config as '#{project_name}'", :green
@@ -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.2.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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rich Dabrowski
@@ -95,6 +95,9 @@ files:
95
95
  - docs/index.html
96
96
  - exe/harbinger
97
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
98
101
  - lib/harbinger/analyzers/rails_analyzer.rb
99
102
  - lib/harbinger/analyzers/ruby_detector.rb
100
103
  - lib/harbinger/cli.rb