stackharbinger 0.1.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 +7 -0
- data/CHANGELOG.md +34 -0
- data/README.md +180 -0
- data/Rakefile +12 -0
- data/exe/harbinger +6 -0
- data/lib/harbinger/analyzers/rails_analyzer.rb +40 -0
- data/lib/harbinger/analyzers/ruby_detector.rb +66 -0
- data/lib/harbinger/cli.rb +149 -0
- data/lib/harbinger/eol_fetcher.rb +91 -0
- data/lib/harbinger/version.rb +5 -0
- data/lib/harbinger.rb +11 -0
- data/sig/harbinger.rbs +4 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 78f93c3813cf5f8ec4540a11d5901f4832667fc76ac449c2cbc61899d61844af
|
|
4
|
+
data.tar.gz: a713dcab3bf69bd8ba1bf3aa7ff26d841e2beff17a15ba5a4b33918338b4d093
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '07888386c6149bbef7a607766cd92c59d34c2f38bdddd6dc06a97fb1734f21b9b5775c47fe5f54c81e71a459931b24dac9f3c3dbd30d49d63bdca8bd19fed3cc'
|
|
7
|
+
data.tar.gz: 223b899768d2c1d6e7ad2bbfd5df74679e119e8d0d79b235b401035587e23dba52c624b86f831686f5d93fbd91177b245b60cb582291d22c5b3520ff11733f3e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-01-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Ruby version detection from `.ruby-version`, `Gemfile`, and `Gemfile.lock`
|
|
14
|
+
- Rails version detection from `Gemfile.lock`
|
|
15
|
+
- EOL data fetching from endoflife.date API
|
|
16
|
+
- Smart caching with 24-hour expiry
|
|
17
|
+
- CLI commands:
|
|
18
|
+
- `harbinger scan [PATH]` - Scan project and show EOL status
|
|
19
|
+
- `harbinger update` - Force refresh EOL data
|
|
20
|
+
- `harbinger version` - Show harbinger version
|
|
21
|
+
- Color-coded status display:
|
|
22
|
+
- Red: Already EOL or <30 days remaining
|
|
23
|
+
- Yellow: <6 months remaining
|
|
24
|
+
- Green: >6 months remaining
|
|
25
|
+
- Product detection with helpful guidance when version not specified
|
|
26
|
+
- Comprehensive test suite (36 tests)
|
|
27
|
+
- Automatic EOL data fetch on first scan (zero configuration)
|
|
28
|
+
|
|
29
|
+
### Technical
|
|
30
|
+
- Built with Thor for CLI framework
|
|
31
|
+
- HTTParty for HTTP requests
|
|
32
|
+
- TTY gems for terminal UI
|
|
33
|
+
- RSpec for testing
|
|
34
|
+
- RuboCop for linting
|
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Harbinger
|
|
2
|
+
|
|
3
|
+
**Track End-of-Life dates for your tech stack and stay ahead of deprecations.**
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔍 **Auto-detects versions** from `.ruby-version`, `Gemfile`, and `Gemfile.lock`
|
|
10
|
+
- 📅 **Fetches EOL data** from [endoflife.date](https://endoflife.date)
|
|
11
|
+
- 🎨 **Color-coded warnings** (red: already EOL, yellow: <6 months, green: safe)
|
|
12
|
+
- ⚡ **Smart caching** (24-hour cache, works offline after first fetch)
|
|
13
|
+
- 🚀 **Zero configuration** - just run `harbinger scan`
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install stackharbinger
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or add to your Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'stackharbinger'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The command is still `harbinger` (shorter to type).
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Scan a project
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Scan current directory
|
|
35
|
+
harbinger scan
|
|
36
|
+
|
|
37
|
+
# Scan specific project
|
|
38
|
+
harbinger scan ~/Projects/my-rails-app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Example output:**
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Scanning /Users/you/Projects/my-app...
|
|
45
|
+
|
|
46
|
+
Detected versions:
|
|
47
|
+
Ruby: 3.2.0
|
|
48
|
+
Rails: 7.0.8
|
|
49
|
+
|
|
50
|
+
Fetching EOL data...
|
|
51
|
+
|
|
52
|
+
Ruby 3.2.0:
|
|
53
|
+
EOL Date: 2026-03-31
|
|
54
|
+
Status: 437 days remaining
|
|
55
|
+
|
|
56
|
+
Rails 7.0.8:
|
|
57
|
+
EOL Date: 2025-06-01
|
|
58
|
+
Status: ALREADY EOL (474 days ago)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Update EOL data
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Force refresh EOL data from endoflife.date
|
|
65
|
+
harbinger update
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Show version
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
harbinger version
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How It Works
|
|
75
|
+
|
|
76
|
+
1. **Detection**: Harbinger looks for version info in your project:
|
|
77
|
+
- Ruby: `.ruby-version`, `Gemfile` (`ruby "x.x.x"`), `Gemfile.lock` (RUBY VERSION)
|
|
78
|
+
- Rails: `Gemfile.lock` (rails gem)
|
|
79
|
+
|
|
80
|
+
2. **EOL Data**: Fetches official EOL dates from [endoflife.date](https://endoflife.date) API
|
|
81
|
+
|
|
82
|
+
3. **Caching**: Stores data in `~/.harbinger/data/` for 24 hours (works offline)
|
|
83
|
+
|
|
84
|
+
4. **Analysis**: Compares your versions against EOL dates and color-codes the urgency
|
|
85
|
+
|
|
86
|
+
## Version Detection
|
|
87
|
+
|
|
88
|
+
### Ruby Detection Priority
|
|
89
|
+
|
|
90
|
+
1. `.ruby-version` file (highest priority)
|
|
91
|
+
2. `ruby "x.x.x"` declaration in Gemfile
|
|
92
|
+
3. `RUBY VERSION` section in Gemfile.lock
|
|
93
|
+
|
|
94
|
+
If Harbinger detects a Ruby project but no version:
|
|
95
|
+
```
|
|
96
|
+
Ruby: Present (version not specified - add .ruby-version or ruby declaration in Gemfile)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Rails Detection
|
|
100
|
+
|
|
101
|
+
Parses `Gemfile.lock` for the rails gem version.
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
- Ruby >= 3.1.0
|
|
106
|
+
- Internet connection (for initial EOL data fetch)
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Clone the repo
|
|
112
|
+
git clone https://github.com/RichD/harbinger.git
|
|
113
|
+
cd harbinger
|
|
114
|
+
|
|
115
|
+
# Install dependencies
|
|
116
|
+
bundle install
|
|
117
|
+
|
|
118
|
+
# Run tests
|
|
119
|
+
bundle exec rspec
|
|
120
|
+
|
|
121
|
+
# Run locally
|
|
122
|
+
bundle exec exe/harbinger scan .
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Roadmap
|
|
126
|
+
|
|
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
|
|
132
|
+
|
|
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
|
|
138
|
+
|
|
139
|
+
### V1.0 - Future
|
|
140
|
+
- 🐍 Python support (pyproject.toml, requirements.txt)
|
|
141
|
+
- 📦 Node.js support (package.json, .nvmrc)
|
|
142
|
+
- 🦀 Rust support (Cargo.toml)
|
|
143
|
+
- 🏠 Homebrew distribution: `brew install harbinger`
|
|
144
|
+
|
|
145
|
+
### V2.0 - Vision
|
|
146
|
+
- 🤖 AI-powered upgrade summaries
|
|
147
|
+
- 📧 Email/Slack notifications
|
|
148
|
+
- ☁️ Cloud platform detection (AWS, Heroku, etc.)
|
|
149
|
+
- 👥 Team collaboration features
|
|
150
|
+
|
|
151
|
+
## Contributing
|
|
152
|
+
|
|
153
|
+
Contributions welcome! Please:
|
|
154
|
+
|
|
155
|
+
1. Fork the repo
|
|
156
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
157
|
+
3. Write tests for your changes
|
|
158
|
+
4. Ensure all tests pass (`bundle exec rspec`)
|
|
159
|
+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
160
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
161
|
+
7. Open a Pull Request
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
This gem is available as open source under the terms of the MIT License.
|
|
166
|
+
|
|
167
|
+
## Credits
|
|
168
|
+
|
|
169
|
+
- EOL data provided by [endoflife.date](https://endoflife.date)
|
|
170
|
+
- Built with ❤️ using Ruby and Thor
|
|
171
|
+
|
|
172
|
+
## Links
|
|
173
|
+
|
|
174
|
+
- Website: [stackharbinger.com](https://stackharbinger.com)
|
|
175
|
+
- GitHub: [github.com/RichD/harbinger](https://github.com/RichD/harbinger)
|
|
176
|
+
- RubyGems: [rubygems.org/gems/stackharbinger](https://rubygems.org/gems/stackharbinger)
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
**Like Harbinger?** Give it a ⭐ on GitHub!
|
data/Rakefile
ADDED
data/exe/harbinger
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harbinger
|
|
4
|
+
module Analyzers
|
|
5
|
+
class RailsAnalyzer
|
|
6
|
+
def initialize(project_path)
|
|
7
|
+
@project_path = project_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect
|
|
11
|
+
detect_from_gemfile_lock
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rails_detected?
|
|
15
|
+
gemfile_lock = File.join(project_path, "Gemfile.lock")
|
|
16
|
+
return false unless File.exist?(gemfile_lock)
|
|
17
|
+
|
|
18
|
+
content = File.read(gemfile_lock)
|
|
19
|
+
content.match?(/^\s*rails\s+\(/)
|
|
20
|
+
rescue StandardError
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :project_path
|
|
27
|
+
|
|
28
|
+
def detect_from_gemfile_lock
|
|
29
|
+
file_path = File.join(project_path, "Gemfile.lock")
|
|
30
|
+
return nil unless File.exist?(file_path)
|
|
31
|
+
|
|
32
|
+
content = File.read(file_path)
|
|
33
|
+
match = content.match(/^\s*rails\s+\(([^)]+)\)/)
|
|
34
|
+
match ? match[1] : nil
|
|
35
|
+
rescue StandardError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Harbinger
|
|
4
|
+
module Analyzers
|
|
5
|
+
class RubyDetector
|
|
6
|
+
def initialize(project_path)
|
|
7
|
+
@project_path = project_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect
|
|
11
|
+
detect_from_ruby_version ||
|
|
12
|
+
detect_from_gemfile ||
|
|
13
|
+
detect_from_gemfile_lock
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ruby_detected?
|
|
17
|
+
File.exist?(File.join(project_path, "Gemfile")) ||
|
|
18
|
+
File.exist?(File.join(project_path, "Gemfile.lock")) ||
|
|
19
|
+
File.exist?(File.join(project_path, ".ruby-version"))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :project_path
|
|
25
|
+
|
|
26
|
+
def detect_from_ruby_version
|
|
27
|
+
file_path = File.join(project_path, ".ruby-version")
|
|
28
|
+
return nil unless File.exist?(file_path)
|
|
29
|
+
|
|
30
|
+
content = File.read(file_path).strip
|
|
31
|
+
extract_version(content)
|
|
32
|
+
rescue StandardError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def detect_from_gemfile
|
|
37
|
+
file_path = File.join(project_path, "Gemfile")
|
|
38
|
+
return nil unless File.exist?(file_path)
|
|
39
|
+
|
|
40
|
+
content = File.read(file_path)
|
|
41
|
+
match = content.match(/ruby\s+["']([^"']+)["']/)
|
|
42
|
+
match ? match[1] : nil
|
|
43
|
+
rescue StandardError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def detect_from_gemfile_lock
|
|
48
|
+
file_path = File.join(project_path, "Gemfile.lock")
|
|
49
|
+
return nil unless File.exist?(file_path)
|
|
50
|
+
|
|
51
|
+
content = File.read(file_path)
|
|
52
|
+
match = content.match(/RUBY VERSION\s+ruby\s+([^\s]+)/)
|
|
53
|
+
match ? extract_version(match[1]) : nil
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_version(version_string)
|
|
59
|
+
# Remove "ruby-" prefix if present
|
|
60
|
+
version = version_string.sub(/^ruby-/, "")
|
|
61
|
+
# Remove patch level suffix (e.g., "p223")
|
|
62
|
+
version.sub(/p\d+$/, "")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "date"
|
|
5
|
+
require_relative "version"
|
|
6
|
+
require "harbinger/analyzers/ruby_detector"
|
|
7
|
+
require "harbinger/analyzers/rails_analyzer"
|
|
8
|
+
require "harbinger/eol_fetcher"
|
|
9
|
+
|
|
10
|
+
module Harbinger
|
|
11
|
+
class CLI < Thor
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
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
|
|
20
|
+
|
|
21
|
+
unless File.directory?(project_path)
|
|
22
|
+
say "Error: #{project_path} is not a valid directory", :red
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
say "Scanning #{project_path}...", :cyan
|
|
27
|
+
|
|
28
|
+
# Detect versions
|
|
29
|
+
ruby_detector = Analyzers::RubyDetector.new(project_path)
|
|
30
|
+
rails_analyzer = Analyzers::RailsAnalyzer.new(project_path)
|
|
31
|
+
|
|
32
|
+
ruby_version = ruby_detector.detect
|
|
33
|
+
rails_version = rails_analyzer.detect
|
|
34
|
+
|
|
35
|
+
ruby_present = ruby_detector.ruby_detected?
|
|
36
|
+
rails_present = rails_analyzer.rails_detected?
|
|
37
|
+
|
|
38
|
+
# Display results
|
|
39
|
+
say "\nDetected versions:", :green
|
|
40
|
+
if ruby_version
|
|
41
|
+
say " Ruby: #{ruby_version}", :white
|
|
42
|
+
elsif ruby_present
|
|
43
|
+
say " Ruby: Present (version not specified - add .ruby-version or ruby declaration in Gemfile)", :yellow
|
|
44
|
+
else
|
|
45
|
+
say " Ruby: Not a Ruby project", :red
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if rails_version
|
|
49
|
+
say " Rails: #{rails_version}", :white
|
|
50
|
+
elsif rails_present
|
|
51
|
+
say " Rails: Present (version not found in Gemfile.lock)", :yellow
|
|
52
|
+
else
|
|
53
|
+
say " Rails: Not detected", :yellow
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Fetch and display EOL dates
|
|
57
|
+
if ruby_version || rails_version
|
|
58
|
+
say "\nFetching EOL data...", :cyan
|
|
59
|
+
fetcher = EolFetcher.new
|
|
60
|
+
|
|
61
|
+
if ruby_version
|
|
62
|
+
display_eol_info(fetcher, "Ruby", ruby_version)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if rails_version
|
|
66
|
+
display_eol_info(fetcher, "Rails", rails_version)
|
|
67
|
+
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
|
+
|
|
84
|
+
products.each do |product|
|
|
85
|
+
say "Fetching #{product}...", :white
|
|
86
|
+
data = fetcher.fetch(product)
|
|
87
|
+
|
|
88
|
+
if data
|
|
89
|
+
say " ✓ #{product.capitalize}: #{data.length} versions cached", :green
|
|
90
|
+
else
|
|
91
|
+
say " ✗ #{product.capitalize}: Failed to fetch", :red
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
say "\nEOL data updated successfully!", :green
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "version", "Show harbinger version"
|
|
99
|
+
def version
|
|
100
|
+
say "Harbinger version #{Harbinger::VERSION}", :cyan
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def display_eol_info(fetcher, product, version)
|
|
106
|
+
product_key = product.downcase
|
|
107
|
+
eol_date = fetcher.eol_date_for(product_key, version)
|
|
108
|
+
|
|
109
|
+
if eol_date
|
|
110
|
+
days_until_eol = days_until(eol_date)
|
|
111
|
+
color = eol_color(days_until_eol)
|
|
112
|
+
|
|
113
|
+
say "\n#{product} #{version}:", :white
|
|
114
|
+
say " EOL Date: #{eol_date}", color
|
|
115
|
+
say " Status: #{eol_status(days_until_eol)}", color
|
|
116
|
+
else
|
|
117
|
+
say "\n#{product} #{version}:", :white
|
|
118
|
+
say " EOL Date: Unknown (version not found in database)", :yellow
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def days_until(date_string)
|
|
123
|
+
eol_date = Date.parse(date_string)
|
|
124
|
+
(eol_date - Date.today).to_i
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def eol_color(days)
|
|
128
|
+
if days < 0
|
|
129
|
+
:red
|
|
130
|
+
elsif days < 180 # < 6 months
|
|
131
|
+
:yellow
|
|
132
|
+
else
|
|
133
|
+
:green
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def eol_status(days)
|
|
138
|
+
if days < 0
|
|
139
|
+
"ALREADY EOL (#{days.abs} days ago)"
|
|
140
|
+
elsif days < 30
|
|
141
|
+
"ENDING SOON (#{days} days remaining)"
|
|
142
|
+
elsif days < 180
|
|
143
|
+
"#{days} days remaining"
|
|
144
|
+
else
|
|
145
|
+
"#{days} days remaining"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httparty"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Harbinger
|
|
8
|
+
class EolFetcher
|
|
9
|
+
CACHE_EXPIRY_SECONDS = 24 * 60 * 60 # 24 hours
|
|
10
|
+
API_BASE_URL = "https://endoflife.date/api"
|
|
11
|
+
|
|
12
|
+
def initialize(cache_dir: default_cache_dir)
|
|
13
|
+
@cache_dir = cache_dir
|
|
14
|
+
FileUtils.mkdir_p(@cache_dir)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(product)
|
|
18
|
+
cache_file = cache_file_path(product)
|
|
19
|
+
|
|
20
|
+
# Return fresh cache if available
|
|
21
|
+
if cache_fresh?(cache_file)
|
|
22
|
+
return read_cache(cache_file)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Try to fetch from API
|
|
26
|
+
begin
|
|
27
|
+
data = fetch_from_api(product)
|
|
28
|
+
write_cache(cache_file, data)
|
|
29
|
+
data
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
# Fall back to stale cache if API fails
|
|
32
|
+
read_cache(cache_file) if File.exist?(cache_file)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def eol_date_for(product, version)
|
|
37
|
+
data = fetch(product)
|
|
38
|
+
return nil unless data
|
|
39
|
+
|
|
40
|
+
# Extract major.minor from version (e.g., "3.2.1" -> "3.2")
|
|
41
|
+
version_parts = version.split(".")
|
|
42
|
+
major_minor = "#{version_parts[0]}.#{version_parts[1]}"
|
|
43
|
+
|
|
44
|
+
# Find matching cycle
|
|
45
|
+
entry = data.find { |item| item["cycle"] == major_minor }
|
|
46
|
+
entry ? entry["eol"] : nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :cache_dir
|
|
52
|
+
|
|
53
|
+
def default_cache_dir
|
|
54
|
+
File.join(Dir.home, ".harbinger", "data")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cache_file_path(product)
|
|
58
|
+
File.join(cache_dir, "#{product}.json")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cache_fresh?(cache_file)
|
|
62
|
+
return false unless File.exist?(cache_file)
|
|
63
|
+
|
|
64
|
+
File.mtime(cache_file) > Time.now - CACHE_EXPIRY_SECONDS
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def read_cache(cache_file)
|
|
68
|
+
return nil unless File.exist?(cache_file)
|
|
69
|
+
|
|
70
|
+
JSON.parse(File.read(cache_file))
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write_cache(cache_file, data)
|
|
76
|
+
File.write(cache_file, JSON.pretty_generate(data))
|
|
77
|
+
rescue StandardError
|
|
78
|
+
# Silently fail if we can't write cache
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def fetch_from_api(product)
|
|
83
|
+
url = "#{API_BASE_URL}/#{product}.json"
|
|
84
|
+
response = HTTParty.get(url)
|
|
85
|
+
|
|
86
|
+
raise "API request failed: #{response.code}" unless response.success?
|
|
87
|
+
|
|
88
|
+
response.parsed_response
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/harbinger.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "harbinger/version"
|
|
4
|
+
require_relative "harbinger/cli"
|
|
5
|
+
require_relative "harbinger/analyzers/ruby_detector"
|
|
6
|
+
require_relative "harbinger/analyzers/rails_analyzer"
|
|
7
|
+
require_relative "harbinger/eol_fetcher"
|
|
8
|
+
|
|
9
|
+
module Harbinger
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
end
|
data/sig/harbinger.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stackharbinger
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rich Dabrowski
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: tty-prompt
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.23'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.23'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: tty-table
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.12'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.12'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: colorize
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.1'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: httparty
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.21'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
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.
|
|
85
|
+
email:
|
|
86
|
+
- engineering@richd.net
|
|
87
|
+
executables:
|
|
88
|
+
- harbinger
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- CHANGELOG.md
|
|
93
|
+
- README.md
|
|
94
|
+
- Rakefile
|
|
95
|
+
- exe/harbinger
|
|
96
|
+
- lib/harbinger.rb
|
|
97
|
+
- lib/harbinger/analyzers/rails_analyzer.rb
|
|
98
|
+
- lib/harbinger/analyzers/ruby_detector.rb
|
|
99
|
+
- lib/harbinger/cli.rb
|
|
100
|
+
- lib/harbinger/eol_fetcher.rb
|
|
101
|
+
- lib/harbinger/version.rb
|
|
102
|
+
- sig/harbinger.rbs
|
|
103
|
+
homepage: https://stackharbinger.com
|
|
104
|
+
licenses:
|
|
105
|
+
- MIT
|
|
106
|
+
metadata:
|
|
107
|
+
allowed_push_host: https://rubygems.org
|
|
108
|
+
homepage_uri: https://stackharbinger.com
|
|
109
|
+
source_code_uri: https://github.com/RichD/harbinger
|
|
110
|
+
changelog_uri: https://github.com/RichD/harbinger/blob/main/CHANGELOG.md
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: 3.1.0
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 3.7.2
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: Track End-of-Life dates for your tech stack and stay ahead of deprecations
|
|
128
|
+
test_files: []
|