end_of_life 0.5.1 → 1.0.0.alpha

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: 19209199d1886b9a553b2ce566a075a162fc0f877073aad2c63739f99d32f0ee
4
- data.tar.gz: 9ef0fbd8ef557d73c0a6739b29fa4f40edb3dbba4e6b306f4e132ae160ed815e
3
+ metadata.gz: 31c4b9042f127a4857ab815c550b47c84a749d079a7661778180527094f3ebb9
4
+ data.tar.gz: 7c5ec81d9f45348408fb73bb0c218080ef0f5b6da60eb7b1541300ec0124c373
5
5
  SHA512:
6
- metadata.gz: 66fe59b2626bf7656eed917b4fa8e196b15e81dd327f19636af914359956b318648e334f8655d249a979f8d50c94ece93fde880ac241fc497985ba06f18ab55f
7
- data.tar.gz: 924eed863de214be0a8ac4a91fccfa964f1349efa13631acec507a6b59a481811885f9dbdc6ff3cd4473e6d460f42a1cffb4400bf6f1297707d83b23b422ffda
6
+ metadata.gz: cc4cdf54b0ac364cbcc8560b43697c14e340c9ff6cbb912a69260559339a5dd064e762844ab41f0430a212cc309960f78f84467f8fefe47f110edf299a591909
7
+ data.tar.gz: 65815bb7004eee6f7579c39cdc4453a4aa85da248f4e5efa1656ff8f118af6b40246f9c3f3b85656587abb14f4efeaa313d4798ed1d1a46652b228b7e234d7c4
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ ignore:
2
+ - '**/*':
3
+ - Style/ItAssignment
data/CHANGELOG.md CHANGED
@@ -1,4 +1,22 @@
1
- ## [Unreleased]
1
+ <!-- ## [Unreleased] -->
2
+
3
+ ## [1.0.0.alpha] - 2025-10-24
4
+
5
+ - Revamp CLI
6
+ - Now the main command is `end_of_life scan <product>` instead of `end_of_life scan --product=<product>`
7
+ - 🎉 Add support for scanning EOL Node.js versions!
8
+ - Add support for detecting versions from `mise.toml`.
9
+ - Add `check` command to check if specific product releases are EOL:
10
+
11
+ ```sh
12
+ $ end_of_life check ruby@3 nodejs@18
13
+ ┌─────────────────┬───────────┬───────────────────────────┐
14
+ │ Product Release │ Status │ EOL Date │
15
+ ├─────────────────┼───────────┼───────────────────────────┤
16
+ │ ruby@3.4.7 │ Supported │ 2028-03-31 (in 2 years) │
17
+ │ nodejs@18.20.8 │ EOL │ 2025-04-30 (5 months ago) │
18
+ └─────────────────┴───────────┴───────────────────────────┘
19
+ ```
2
20
 
3
21
  ## [0.5.1] - 2025-09-10
4
22
 
data/Gemfile CHANGED
@@ -6,10 +6,11 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "climate_control", "~> 1.0"
9
+ gem "ostruct"
9
10
  gem "rake", "~> 13.0"
10
11
  gem "rspec", "~> 3.10"
11
12
  gem "rspec-mocks", "~> 3.10"
12
13
  gem "simplecov", "~> 0.22.0"
13
14
  gem "standard", github: "testdouble/standard"
14
- gem "vcr", "~> 6.0"
15
+ gem "vcr", github: "vcr/vcr"
15
16
  gem "webmock", "~> 3.13"
data/Gemfile.lock CHANGED
@@ -9,15 +9,25 @@ GIT
9
9
  standard-custom (~> 1.0.0)
10
10
  standard-performance (~> 1.8)
11
11
 
12
+ GIT
13
+ remote: https://github.com/vcr/vcr.git
14
+ revision: ce35c236fe48899f02ddf780973b44cdb756c0ee
15
+ specs:
16
+ vcr (6.3.1)
17
+
12
18
  PATH
13
19
  remote: .
14
20
  specs:
15
- end_of_life (0.5.1)
21
+ end_of_life (1.0.0.alpha)
22
+ argument_parser (~> 0.1.0)
16
23
  async
17
- bundler (>= 2.3.0, < 3)
24
+ base64
25
+ bundler (>= 2.7.2, < 3)
18
26
  dry-monads (~> 1.3)
27
+ faraday-retry (~> 2.0)
19
28
  octokit (~> 9.0)
20
29
  pastel (~> 0.8.0)
30
+ perfect_toml (~> 0.9.0)
21
31
  tty-spinner (~> 0.9.0)
22
32
  tty-table (~> 0.12.0)
23
33
  zeitwerk (~> 2.7)
@@ -27,14 +37,15 @@ GEM
27
37
  specs:
28
38
  addressable (2.8.7)
29
39
  public_suffix (>= 2.0.2, < 7.0)
40
+ argument_parser (0.1.3)
30
41
  ast (2.4.3)
31
- async (2.31.0)
42
+ async (2.32.0)
32
43
  console (~> 1.29)
33
44
  fiber-annotation
34
45
  io-event (~> 1.11)
35
46
  metrics (~> 0.12)
36
47
  traces (~> 0.18)
37
- base64 (0.2.0)
48
+ base64 (0.3.0)
38
49
  bigdecimal (3.1.9)
39
50
  climate_control (1.2.0)
40
51
  concurrent-ruby (1.3.5)
@@ -61,6 +72,8 @@ GEM
61
72
  logger
62
73
  faraday-net_http (3.4.1)
63
74
  net-http (>= 0.5.0)
75
+ faraday-retry (2.3.2)
76
+ faraday (~> 2.0)
64
77
  fiber-annotation (0.2.0)
65
78
  fiber-local (1.1.0)
66
79
  fiber-storage
@@ -77,19 +90,21 @@ GEM
77
90
  octokit (9.2.0)
78
91
  faraday (>= 1, < 3)
79
92
  sawyer (~> 0.9)
93
+ ostruct (0.6.3)
80
94
  parallel (1.27.0)
81
95
  parser (3.3.9.0)
82
96
  ast (~> 2.4.1)
83
97
  racc
84
98
  pastel (0.8.0)
85
99
  tty-color (~> 0.5)
100
+ perfect_toml (0.9.0)
86
101
  prism (1.4.0)
87
102
  public_suffix (6.0.1)
88
103
  racc (1.8.1)
89
104
  rainbow (3.1.1)
90
105
  rake (13.2.1)
91
106
  regexp_parser (2.11.2)
92
- rexml (3.4.0)
107
+ rexml (3.4.4)
93
108
  rspec (3.13.0)
94
109
  rspec-core (~> 3.13.0)
95
110
  rspec-expectations (~> 3.13.0)
@@ -155,8 +170,6 @@ GEM
155
170
  unicode-display_width (2.6.0)
156
171
  unicode_utils (1.4.0)
157
172
  uri (1.0.3)
158
- vcr (6.3.1)
159
- base64
160
173
  webmock (3.24.0)
161
174
  addressable (>= 2.8.0)
162
175
  crack (>= 0.3.2)
@@ -169,13 +182,14 @@ PLATFORMS
169
182
  DEPENDENCIES
170
183
  climate_control (~> 1.0)
171
184
  end_of_life!
185
+ ostruct
172
186
  rake (~> 13.0)
173
187
  rspec (~> 3.10)
174
188
  rspec-mocks (~> 3.10)
175
189
  simplecov (~> 0.22.0)
176
190
  standard!
177
- vcr (~> 6.0)
191
+ vcr!
178
192
  webmock (~> 3.13)
179
193
 
180
194
  BUNDLED WITH
181
- 2.3.9
195
+ 2.7.2
data/README.md CHANGED
@@ -1,18 +1,24 @@
1
1
  # End of Life
2
2
 
3
- This gem lists GitHub repositories using end-of-life versions of various
4
- products.
3
+ This tool lists GitHub repositories using end-of-life software.
4
+
5
+ We currently support Ruby, Rails, and Node.js. If you want to add support for
6
+ more products, please check out the [Contributing](#contributing) section.
5
7
 
6
8
  ![End of Life Demo](demo.gif)
7
9
 
8
10
  ## Installation
9
11
 
12
+ If you have Ruby installed, you can install End of Life as a gem with:
13
+
10
14
  ```sh
11
15
  gem install end_of_life
12
16
  ```
13
17
 
14
18
  ## Usage
15
19
 
20
+ ### Scanning your repositories
21
+
16
22
  1. Set up a [GitHub access token][] (we recommend using a read-only token);
17
23
 
18
24
  [github access token]:
@@ -21,12 +27,12 @@ gem install end_of_life
21
27
  2. Export the `GITHUB_TOKEN` environment variable or set it when calling
22
28
  `end_of_life`;
23
29
 
24
- 3. Use the `end_of_life` command to list the repositories:
30
+ 3. Use the `end_of_life scan` command to list the repositories:
25
31
 
26
32
  ```sh
27
- $ GITHUB_TOKEN=something end_of_life # if your platform supports symlinks, you can use the `eol` command instead
28
- [✔] Searching repositories with Ruby...
29
- [✔] Searching for EOL Ruby in repositories...
33
+ $ GITHUB_TOKEN=something end_of_life scan ruby
34
+ [✔] Searching repositories that might use Ruby...
35
+ [✔] Scanning 27 repositories for EOL Ruby...
30
36
 
31
37
  Found 2 repositories using EOL Ruby (<= 3.1.7):
32
38
  ┌───┬──────────────────────────────────────────────┬──────────────┐
@@ -37,44 +43,94 @@ Found 2 repositories using EOL Ruby (<= 3.1.7):
37
43
  └───┴──────────────────────────────────────────────┴──────────────┘
38
44
  ```
39
45
 
40
- ### Options
46
+ > [!TIP]
47
+ > You can use the shorthand `eol` instead of `end_of_life` if your platform
48
+ > supports symlinks.
49
+
50
+ #### Options for `scan`
41
51
 
42
52
  There are some options to help you filter down the results:
43
53
 
54
+ ```sh
55
+ Usage: end_of_life scan PRODUCT [OPTIONS]
56
+ --exclude=NAME,NAME2 Exclude repositories containing a certain word in their name. You can specify up to five words.
57
+ --public-only Searches only public repositories
58
+ --private-only Searches only private repositories
59
+ --repo, --repository=USER/REPO Searches a specific repository
60
+ --org, --organization=ORG,ORG2 Searches within specific organizations
61
+ -u, --user=NAME Sets the user used on the repository search
62
+ --max-eol-days-away NUMBER Sets the maximum number of days away a version can be from EOL.
63
+ --include-archived Includes archived repositories on the search
64
+ -h, --help Show this help message
65
+ ```
66
+
67
+ ### Checking if a specific product version is EOL
68
+
69
+ > [!IMPORTANT]
70
+ > You don't need a GitHub token to use this command.
71
+
72
+ You can also check if a specific product version is end-of-life with the
73
+ `end_of_life check` command:
74
+
75
+ ```sh
76
+ $ end_of_life check ruby@2.5.8 # exits with status code 1 on EOL
77
+ ┌─────────────────┬────────┬──────────────────────────┐
78
+ │ Product Release │ Status │ EOL Date │
79
+ ├─────────────────┼────────┼──────────────────────────┤
80
+ │ ruby@2.5.9 │ EOL │ 2021-03-31 (4 years ago) │
81
+ └─────────────────┴────────┴──────────────────────────┘
44
82
  ```
45
- Usage: end_of_life [options]
46
- -p, --product NAME Sets the product to scan for (default: ruby). Supported products are: ruby, rails.
47
- --exclude=NAME,NAME2 Exclude repositories containing a certain word in its name. You can specify up to five words.
48
- --public-only Searches only public repositories
49
- --private-only Searches only private repositories
50
- --repo, --repository=USER/REPO Searches a specific repository
51
- --org, --organization=ORG,ORG2 Searches within specific organizations
52
- -u, --user=NAME Sets the user used on the repository search
53
- --max-eol-days-away NUMBER Sets the maximum number of days away a version can be from EOL. It defaults to 0.
54
- --include-archived Includes archived repositories on the search
55
- -v, --version Displays end_of_life version
56
- -h, --help Displays this help
83
+
84
+ You can pass multiple products to check at once:
85
+
86
+ ```sh
87
+ $ end_of_life check ruby@2.5.8 nodejs@18
88
+ ┌─────────────────┬────────┬───────────────────────────┐
89
+ Product Release Status │ EOL Date │
90
+ ├─────────────────┼────────┼───────────────────────────┤
91
+ ruby@2.5.9 │ EOL 2021-03-31 (4 years ago) │
92
+ │ nodejs@18.20.8 │ EOL │ 2025-04-30 (4 months ago)
93
+ └─────────────────┴────────┴───────────────────────────┘
94
+ ```
95
+
96
+ #### Options for `check`
97
+
98
+ ```sh
99
+ Usage: end_of_life check PRODUCT@VERSION PRODUCT2@VERSION... [OPTIONS]
100
+ --max-eol-days-away NUMBER Sets the maximum number of days away a version can be from EOL.
101
+ -h, --help Show this help message
102
+ ```
103
+
104
+ > [!TIP]
105
+ > You can use check with the `--max-eol-days-away` option on your CI to be
106
+ > alerted when your current version is close to its end-of-life date:
107
+
108
+ ```sh
109
+ $ end_of_life check ruby@$(ruby -v | awk '{print $2}') --max-eol-days-away=365
110
+ ┌─────────────────┬──────────┬──────────────────────────┐
111
+ │ Product Release │ Status │ EOL Date │
112
+ ├─────────────────┼──────────┼──────────────────────────┤
113
+ │ ruby@3.2.9 │ Near EOL │ 2026-03-31 (in 6 months) │
114
+ └─────────────────┴──────────┴──────────────────────────┘
57
115
  ```
58
116
 
59
117
  ## How it works
60
118
 
61
119
  This gem fetches all your GitHub repositories that contain code for the
62
120
  specified product, then searches for files that may contain version information.
63
- For Ruby, those files are: `.ruby-version`, `Gemfile`, `Gemfile.lock`, and
64
- `.tool-version`. End of Life parses these files and extracts the minimum version
65
- used in the repository.
121
+ For Ruby, those files include `.ruby-version`, `Gemfile`, `Gemfile.lock`,
122
+ `mise.toml`, and `.tool-version`. End of Life parses these files and extracts
123
+ the minimum version used in each repository.
66
124
 
67
- The EOL version information is provided by https://endoflife.date/, with a file
68
- [fallback].
125
+ The EOL version information is provided by https://endoflife.date/.
69
126
 
70
- > [!ATTENTION]
127
+ > [!CAUTION]
71
128
  > To parse Gemfiles, we need to execute the code inside them.
72
129
  > **Be careful** because this may be a security risk. We plan to add secure
73
130
  > parsers for these files in the future.
74
131
 
75
132
  Some other limitations are listed on the [issues page].
76
133
 
77
- [fallback]: ./lib/end_of_life.json
78
134
  [issues page]: https://github.com/MatheusRich/end_of_life/issues
79
135
 
80
136
  ## Development
@@ -95,7 +151,19 @@ Bug reports and pull requests are welcome on GitHub at
95
151
  https://github.com/MatheusRich/end_of_life. If you want to add a new product,
96
152
  [check out this commit for reference].
97
153
 
98
- [check out this commit for reference]: ba9a92a690e0d61ea09e508c1cd76b8309fb89df
154
+ [check out this commit for reference]: https://github.com/MatheusRich/end_of_life/commit/bfb6f9ceb5afb338fa5553a1266aa2c063e61200
155
+
156
+ ## About thoughtbot
157
+
158
+ ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg)
159
+
160
+ The development of this project is funded by thoughtbot, inc.
161
+
162
+ We love open source software! See [our other projects][community]. We are
163
+ [available for hire][hire].
164
+
165
+ [community]: https://thoughtbot.com/community?utm_source=github
166
+ [hire]: https://thoughtbot.com/hire-us?utm_source=github
99
167
 
100
168
  ## License
101
169
 
data/bin/end_of_life ADDED
@@ -0,0 +1 @@
1
+ exe/end_of_life
data/end_of_life.gemspec CHANGED
@@ -34,10 +34,14 @@ Gem::Specification.new do |spec|
34
34
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
35
35
  spec.require_paths = ["lib"]
36
36
 
37
+ spec.add_dependency "argument_parser", "~> 0.1.0"
37
38
  spec.add_dependency "async"
38
- spec.add_dependency "bundler", ">= 2.3.0", "< 3"
39
+ spec.add_dependency "base64"
40
+ spec.add_dependency "bundler", ">= 2.7.2", "< 3"
39
41
  spec.add_dependency "dry-monads", "~> 1.3"
42
+ spec.add_dependency "perfect_toml", "~> 0.9.0"
40
43
  spec.add_dependency "octokit", "~> 9.0"
44
+ spec.add_dependency "faraday-retry", "~> 2.0"
41
45
  spec.add_dependency "pastel", "~> 0.8.0"
42
46
  spec.add_dependency "tty-spinner", "~> 0.9.0"
43
47
  spec.add_dependency "tty-table", "~> 0.12.0"
@@ -1,3 +1,6 @@
1
+ require "json"
2
+ require "net/http"
3
+
1
4
  module EndOfLife
2
5
  module API
3
6
  extend self
@@ -0,0 +1,46 @@
1
+ require "argument_parser"
2
+
3
+ module EndOfLife
4
+ module Check
5
+ include Helpers::Terminal
6
+ include Helpers::Time
7
+ extend self
8
+
9
+ def run(releases, options)
10
+ report_for releases.map { |release_string| build_row(release_string, options) }
11
+ end
12
+
13
+ private
14
+
15
+ def build_row(release_string, options)
16
+ product_release = Product::Release.parse!(release_string)
17
+ cycle_release = product_release.latest_release_in_cycle or raise(
18
+ ArgumentError,
19
+ "Unknown product release: #{release_string}"
20
+ )
21
+
22
+ status = if cycle_release.supported?(at: options[:max_eol_date])
23
+ "Supported"
24
+ elsif cycle_release.supported?(at: Date.today)
25
+ "Near EOL"
26
+ else
27
+ "EOL"
28
+ end
29
+
30
+ eol_date = if cycle_release.eol_date
31
+ eol_days_away = relative_time_in_words(cycle_release.eol_date)
32
+ "#{cycle_release.eol_date} (#{eol_days_away})"
33
+ else
34
+ "N/A"
35
+ end
36
+
37
+ [cycle_release.to_s, status, eol_date]
38
+ end
39
+
40
+ HEADERS = ["Product Release", "Status", "EOL Date"].freeze
41
+ def report_for(rows)
42
+ puts table(HEADERS, rows)
43
+ exit 1 if rows.any? { |_, status, _| status != "Supported" }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ require "optparse"
2
+
3
+ module EndOfLife
4
+ class CLI
5
+ module Command::Registry
6
+ Command = Data.define(:name, :summary, :parser, :action) do
7
+ include Helpers::Terminal
8
+
9
+ def run(argv)
10
+ action.call(argv, parser)
11
+ rescue OptionParser::ParseError, ArgumentParser::ParseError => e
12
+ abort "#{error_msg(e.message.capitalize)}\n\n#{parser}"
13
+ end
14
+ end
15
+
16
+ def self.included(base)
17
+ base.extend ClassMethods
18
+ end
19
+
20
+ module ClassMethods
21
+ def command_registry
22
+ @command_registry ||= {}
23
+ end
24
+
25
+ def command(name, summary, &action)
26
+ option_parser = OptionParser.new("", 35, " " * 2) do |opt_parser|
27
+ opt_parser.on_tail("-h", "--help", "Show this help message") do
28
+ puts "#{summary}\n\n#{opt_parser}"
29
+ exit
30
+ end
31
+ end
32
+ command_registry[name.to_s] = Command.new(name: name.to_s, summary:, parser: option_parser, action:)
33
+ end
34
+
35
+ def commands = command_registry.values
36
+
37
+ def summarize_commands
38
+ max_length = commands.map { |cmd| cmd.name.length }.max
39
+ commands.map { |it| " #{it.name.ljust(max_length)} #{it.summary}" }.join("\n")
40
+ end
41
+ end
42
+
43
+ def command(name) = self.class.command_registry.fetch(name.to_s)
44
+ end
45
+ end
46
+ end
@@ -1,29 +1,124 @@
1
+ require "argument_parser"
2
+ require "date"
3
+
1
4
  module EndOfLife
2
5
  class CLI
6
+ include Command::Registry
7
+ extend Helpers::Terminal
3
8
  include Helpers::Terminal
4
9
 
10
+ command :scan, "Find projects using end-of-life software" do |argv, opt_parser|
11
+ options = {max_eol_date: Date.today, skip_archived: true}
12
+
13
+ opt_parser.banner = "Usage: end_of_life scan PRODUCT [OPTIONS]"
14
+ opt_parser.on("--exclude=NAME,NAME2", Array, "Exclude repositories containing a certain word in their name. You can specify up to five words.") do |excludes|
15
+ options[:excludes] = excludes.first(5)
16
+ end
17
+
18
+ opt_parser.on("--public-only", "Searches only public repositories") do
19
+ options[:visibility] = :public
20
+ end
21
+
22
+ opt_parser.on("--private-only", "Searches only private repositories") do
23
+ options[:visibility] = :private
24
+ end
25
+
26
+ opt_parser.on("--repo=USER/REPO", "--repository=USER/REPO", "Searches a specific repository") do |repository|
27
+ options[:repository] = repository
28
+ end
29
+
30
+ opt_parser.on("--org=ORG,ORG2...", "--organization=ORG,ORG2", Array, "Searches within specific organizations") do |organizations|
31
+ options[:organizations] = organizations
32
+ end
33
+
34
+ opt_parser.on("-u NAME", "--user=NAME", "Sets the user used on the repository search") do |user|
35
+ options[:user] = user
36
+ end
37
+
38
+ opt_parser.on("--max-eol-days-away NUMBER", "Sets the maximum number of days away a version can be from EOL.") do |days|
39
+ options[:max_eol_date] = Date.today + days.to_i.abs
40
+ end
41
+
42
+ opt_parser.on("--include-archived", "Includes archived repositories on the search") do
43
+ options[:skip_archived] = false
44
+ end
45
+ opt_parser.parse!(argv)
46
+
47
+ argument_parser = ArgumentParser.build do
48
+ required :product, pattern: EndOfLife.products_pattern
49
+ end
50
+ name = argument_parser.parse!(argv).fetch(:product)
51
+
52
+ Scanner.scan(Product.find(name), options)
53
+ end
54
+
55
+ command :check, "Check if specific product releases are end-of-life" do |argv, opt_parser|
56
+ options = {max_eol_date: Date.today}
57
+ opt_parser.banner = "Usage: end_of_life check PRODUCT@VERSION PRODUCT2@VERSION... [OPTIONS]"
58
+ opt_parser.on("--max-eol-days-away NUMBER", "Sets the maximum number of days away a version can be from EOL.") do |days|
59
+ options[:max_eol_date] = Date.today + days.to_i.abs
60
+ end
61
+ opt_parser.parse!(argv)
62
+
63
+ argument_parser = ArgumentParser.build do
64
+ rest :releases, pattern: EndOfLife.products_pattern(suffix: "@"), min: 1
65
+ end
66
+ args = argument_parser.parse!(argv)
67
+
68
+ Check.run(args[:releases], options)
69
+ end
70
+
71
+ command :help, "Show this help message" do |args, _|
72
+ io = args.include?("--error") ? $stderr : $stdout
73
+
74
+ io.puts <<~HELP
75
+ Usage: end_of_life COMMAND [OPTIONS]
76
+
77
+ Commands:
78
+ #{summarize_commands}
79
+
80
+ Options:
81
+ -h, --help Show this help message
82
+ -v, --version Show end_of_life version
83
+
84
+ Pass -h/--help to commands to see their specific options.
85
+ HELP
86
+ end
87
+
88
+ command :version, "Show end_of_life version" do
89
+ puts "end_of_life v#{EndOfLife::VERSION}"
90
+ end
91
+
5
92
  def call(argv)
6
- parse_options(argv)
7
- .then { |options| execute_command(options) }
93
+ find_command(argv).run(argv)
94
+ rescue ArgumentError, ArgumentParser::ParseError => e
95
+ abort_with(e.message.capitalize)
8
96
  end
9
97
 
10
98
  private
11
99
 
12
- def parse_options(argv)
13
- Options.from(argv)
100
+ def find_command(argv)
101
+ parse_args(argv).then { |args| command(args[:command]) }
14
102
  end
15
103
 
16
- def execute_command(options)
17
- case options[:command]
18
- when :help
19
- puts options[:parser]
20
- when :version
21
- puts "end_of_life v#{EndOfLife::VERSION}"
22
- when :print_error
23
- abort error_msg(options[:error])
24
- else
25
- Scanner.scan(options)
104
+ def parse_args(argv)
105
+ argument_parser = ArgumentParser.build do
106
+ required :command, pattern: {
107
+ "-h" => :help,
108
+ "-v" => :version,
109
+ "--help" => :help,
110
+ "--version" => :version,
111
+ **EndOfLife::CLI.commands.to_h { |cmd| [cmd.name, cmd.name] }
112
+ }
26
113
  end
114
+
115
+ argument_parser.parse!(argv)
116
+ end
117
+
118
+ def abort_with(message)
119
+ warn error_msg(message)
120
+ command(:help).run(["--error"])
121
+ exit 1
27
122
  end
28
123
  end
29
124
  end
@@ -26,11 +26,11 @@ module EndOfLife
26
26
  end
27
27
 
28
28
  def paint
29
- @paint ||= Pastel.new
29
+ Pastel.new(enabled: TTY::Color.support?)
30
30
  end
31
31
 
32
- def new_spinner(message, options = {success_mark: paint.green("✔"), error_mark: paint.red("✖")})
33
- TTY::Spinner.new("[:spinner] #{message}", options)
32
+ def new_spinner(message)
33
+ TTY::Spinner.new("[:spinner] #{message}", success_mark: paint.green("✔"), error_mark: paint.red("✖"))
34
34
  end
35
35
  end
36
36
  end
@@ -0,0 +1,11 @@
1
+ module EndOfLife
2
+ module Helpers::Text
3
+ def pluralize(count, singular, plural = nil)
4
+ if count == 1
5
+ "#{count} #{singular}"
6
+ else
7
+ "#{count} #{plural || "#{singular}s"}"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ module EndOfLife
2
+ module Helpers::Time
3
+ include Helpers::Text
4
+
5
+ def relative_time_in_words(date)
6
+ days_away = (date - Date.today).to_i
7
+ return "today" if days_away.zero?
8
+
9
+ duration = duration_in_words(days_away.abs)
10
+
11
+ if days_away.positive?
12
+ "in #{duration}"
13
+ else
14
+ "#{duration} ago"
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def duration_in_words(number_of_days)
21
+ if number_of_days >= 365
22
+ years = (number_of_days / 365.0).floor
23
+ pluralize(years, "year")
24
+ elsif number_of_days >= 30
25
+ months = (number_of_days / 30.0).floor
26
+ pluralize(months, "month")
27
+ elsif number_of_days >= 14
28
+ weeks = (number_of_days / 7.0).floor
29
+ pluralize(weeks, "week")
30
+ else
31
+ pluralize(number_of_days, "day")
32
+ end
33
+ end
34
+ end
35
+ end