gemview 1.0.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/.rspec +1 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +50 -0
- data/Rakefile +9 -0
- data/exe/gemview +6 -0
- data/lib/gemview/client.rb +23 -0
- data/lib/gemview/commands.rb +120 -0
- data/lib/gemview/gem.rb +215 -0
- data/lib/gemview/git_repo.rb +126 -0
- data/lib/gemview/number.rb +19 -0
- data/lib/gemview/terminal.rb +103 -0
- data/lib/gemview/version.rb +5 -0
- data/lib/gemview/view.rb +38 -0
- data/lib/gemview.rb +30 -0
- metadata +177 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ad8aeeac9963c5eb52cb650708827b9a25a223bfa4597af3466142700e51850b
|
|
4
|
+
data.tar.gz: efdbcb9c871e4462e2e216adf675c9ba2b3373616f6494f60208c9ec39b6f8d3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c997980d1fca42aa4bb49a1f7d43443591907d04f3c20f93a6b72f045a77a8a49a9b85499cd3152b6ccbb349324417dd91be9323b78e57aa1b2825905504b5d8
|
|
7
|
+
data.tar.gz: deec958df2e6515729a091d3f073df221297c01562ae2af1de65b787a566c7b46dde6d3260aa5bbf6063c3978982a225f259a2e3ff27135e4227c59c392a4535
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 apainintheneck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Gemview
|
|
2
|
+
|
|
3
|
+
An unofficial CLI interface for querying information from rubygems.org. It uses the [gems](https://rubygems.org/gems/gems) gem internally.
|
|
4
|
+
|
|
5
|
+
Note: This gem is not directly affiliated with `rubygems.org`. It's just a hobby project.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Commands:
|
|
11
|
+
gemview author USERNAME # Find gems by rubygems.org username
|
|
12
|
+
gemview info NAME # Show gem info
|
|
13
|
+
gemview releases # List the most recent new gem releases
|
|
14
|
+
gemview search TERM # Search for gems
|
|
15
|
+
gemview updates # List the most recent gem updates
|
|
16
|
+
gemview version # Print version
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Demo
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
## Development
|
|
24
|
+
|
|
25
|
+
### Testing & Linting
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
$ rake
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Testing
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
$ rake spec
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Linting
|
|
38
|
+
|
|
39
|
+
```console
|
|
40
|
+
$ rake standard
|
|
41
|
+
$ rake standard:fix
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Contributing
|
|
45
|
+
|
|
46
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/apainintheneck/gemview.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/exe/gemview
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemview
|
|
4
|
+
module Client
|
|
5
|
+
# Create a client manually so that we don't accidentally pick up credentials.
|
|
6
|
+
def self.v1
|
|
7
|
+
@client_v1 ||= Gems::V1::Client.new(
|
|
8
|
+
username: nil,
|
|
9
|
+
password: nil,
|
|
10
|
+
key: nil
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Create a client manually so that we don't accidentally pick up credentials.
|
|
15
|
+
def self.v2
|
|
16
|
+
@client_v2 ||= Gems::V2::Client.new(
|
|
17
|
+
username: nil,
|
|
18
|
+
password: nil,
|
|
19
|
+
key: nil
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
|
|
5
|
+
module Gemview
|
|
6
|
+
module Commands
|
|
7
|
+
extend Dry::CLI::Registry
|
|
8
|
+
|
|
9
|
+
class Info < Dry::CLI::Command
|
|
10
|
+
desc "Show gem info"
|
|
11
|
+
|
|
12
|
+
argument :name, type: :string, required: true, desc: "Gem name"
|
|
13
|
+
|
|
14
|
+
option :version, type: :string, desc: "Gem version"
|
|
15
|
+
|
|
16
|
+
example %w[rubocop bundler]
|
|
17
|
+
|
|
18
|
+
def call(name:, version: nil, **)
|
|
19
|
+
begin
|
|
20
|
+
gem = Gem.find(name: name, version: version)
|
|
21
|
+
rescue Gems::NotFound
|
|
22
|
+
if version
|
|
23
|
+
warn("Error: No gem found with the name '#{name}' and the version '#{version}'")
|
|
24
|
+
exit(1) unless Terminal.confirm(question: "Search for the most recent version?")
|
|
25
|
+
begin
|
|
26
|
+
gem = Gem.find(name: name, version: nil)
|
|
27
|
+
rescue Gems::NotFound
|
|
28
|
+
abort("Error: No gem found with the name: #{name}")
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
abort("Error: No gem found with the name: #{name}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
View.info(gem: gem)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Search < Dry::CLI::Command
|
|
39
|
+
desc "Search for gems"
|
|
40
|
+
|
|
41
|
+
argument :term, type: :string, required: true, desc: "Search term"
|
|
42
|
+
|
|
43
|
+
example %w[cli json]
|
|
44
|
+
|
|
45
|
+
def call(term:, **)
|
|
46
|
+
gems = Gem.search(term: term)
|
|
47
|
+
|
|
48
|
+
if gems.empty?
|
|
49
|
+
abort("Error: No gems found for the search term: #{term}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
View.list(gems: gems)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Author < Dry::CLI::Command
|
|
57
|
+
desc "Find gems by rubygems.org username"
|
|
58
|
+
|
|
59
|
+
argument :username, type: :string, required: true, desc: "rubygems.org username"
|
|
60
|
+
|
|
61
|
+
def call(username:, **)
|
|
62
|
+
gems = Gem.author(username: username)
|
|
63
|
+
|
|
64
|
+
if gems.empty?
|
|
65
|
+
abort("Error: No gems found for the rubygems.org username: #{username}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
View.list(gems: gems)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class Releases < Dry::CLI::Command
|
|
73
|
+
desc "List the most recent new gem releases"
|
|
74
|
+
|
|
75
|
+
def call(**)
|
|
76
|
+
gems = Gem.latest
|
|
77
|
+
|
|
78
|
+
if gems.empty?
|
|
79
|
+
abort("Error: Unable to retrieve latest gem list")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
View.list(gems: gems)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class Updates < Dry::CLI::Command
|
|
87
|
+
desc "List the most recent gem updates"
|
|
88
|
+
|
|
89
|
+
def call(**)
|
|
90
|
+
gems = Gem.just_updated
|
|
91
|
+
|
|
92
|
+
if gems.empty?
|
|
93
|
+
abort("Error: Unable to retrieve latest gem list")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
View.list(gems: gems)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class Version < Dry::CLI::Command
|
|
101
|
+
desc "Print version"
|
|
102
|
+
|
|
103
|
+
def call(*)
|
|
104
|
+
puts Gemview::VERSION
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
register "info", Info
|
|
109
|
+
register "search", Search
|
|
110
|
+
register "author", Author
|
|
111
|
+
register "releases", Releases
|
|
112
|
+
register "updates", Updates
|
|
113
|
+
register "version", Version
|
|
114
|
+
|
|
115
|
+
# @param arguments [Array<String>] defaults to ARGV
|
|
116
|
+
def self.start(arguments: ARGV)
|
|
117
|
+
Dry::CLI.new(self).call(arguments: arguments)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/gemview/gem.rb
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-struct"
|
|
4
|
+
|
|
5
|
+
module Gemview
|
|
6
|
+
class Gem < Dry::Struct
|
|
7
|
+
module Types
|
|
8
|
+
include Dry.Types()
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
transform_keys(&:to_sym)
|
|
12
|
+
|
|
13
|
+
# resolve default types on nil
|
|
14
|
+
transform_types do |type|
|
|
15
|
+
if type.default?
|
|
16
|
+
type.constructor do |value|
|
|
17
|
+
value.nil? ? Dry::Types::Undefined : value
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
type
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attribute :name, Types::Strict::String
|
|
25
|
+
attribute :downloads, Types::Strict::Integer
|
|
26
|
+
attribute :version, Types::Strict::String
|
|
27
|
+
# Note: This is not returned by `Gems.search`.
|
|
28
|
+
attribute? :version_created_at, Types::Params::Time
|
|
29
|
+
attribute :authors, Types::Strict::String
|
|
30
|
+
attribute :info, Types::Strict::String
|
|
31
|
+
# Note: This is occasionally nil so a default value is required.
|
|
32
|
+
attribute :licenses, Types::Array.of(Types::Strict::String).default([].freeze)
|
|
33
|
+
attribute :project_uri, Types::Strict::String
|
|
34
|
+
attribute :homepage_uri, Types::String.optional
|
|
35
|
+
attribute :source_code_uri, Types::String.optional
|
|
36
|
+
attribute :changelog_uri, Types::String.optional
|
|
37
|
+
|
|
38
|
+
class Dependency < Dry::Struct
|
|
39
|
+
transform_keys(&:to_sym)
|
|
40
|
+
|
|
41
|
+
attribute :name, Types::Strict::String
|
|
42
|
+
attribute :requirements, Types::Strict::String
|
|
43
|
+
|
|
44
|
+
def to_str
|
|
45
|
+
%(gem "#{name}", "#{requirements}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Note: This is not returned by `Gems.search`.
|
|
50
|
+
attribute? :dependencies do
|
|
51
|
+
attribute :development, Types::Strict::Array.of(Dependency)
|
|
52
|
+
attribute :runtime, Types::Strict::Array.of(Dependency)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Version < Dry::Struct
|
|
56
|
+
transform_keys(&:to_sym)
|
|
57
|
+
|
|
58
|
+
attribute :number, Types::Strict::String
|
|
59
|
+
alias_method :version, :number
|
|
60
|
+
|
|
61
|
+
attribute :downloads_count, Types::Strict::Integer
|
|
62
|
+
alias_method :downloads, :downloads_count
|
|
63
|
+
|
|
64
|
+
attribute :created_at, Types::Params::Time
|
|
65
|
+
|
|
66
|
+
# @return [Date]
|
|
67
|
+
def release_date = created_at.to_date
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Ex. 1234567890 -> "1,234,567,890"
|
|
71
|
+
# @return [String]
|
|
72
|
+
def humanized_downloads
|
|
73
|
+
Number.humanized_integer(downloads)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [String]
|
|
77
|
+
def selector_str
|
|
78
|
+
<<~SELECT
|
|
79
|
+
#{name} [#{version}]
|
|
80
|
+
-- #{Strings.truncate(info.lines.map(&:strip).join(" "), 75)}
|
|
81
|
+
SELECT
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [String]
|
|
85
|
+
def header_str
|
|
86
|
+
info_lines = Strings.wrap(info, 80).lines.map(&:strip)
|
|
87
|
+
info_lines = info_lines.take(3).append("...") if info_lines.size > 3
|
|
88
|
+
|
|
89
|
+
header = <<~HEADER
|
|
90
|
+
## [#{version}] #{name}
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
#{info_lines.join("\n")}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Updated at | #{version_created_at} |
|
|
97
|
+
| Total Downloads | #{humanized_downloads} |
|
|
98
|
+
| Authors | #{authors} |
|
|
99
|
+
| Licenses | #{licenses} |
|
|
100
|
+
| Project URI | #{project_uri} |
|
|
101
|
+
HEADER
|
|
102
|
+
|
|
103
|
+
Terminal.prettify_markdown(header)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @return [String]
|
|
107
|
+
def dependencies_str
|
|
108
|
+
runtime_deps_str = dependencies.runtime.join("\n").strip
|
|
109
|
+
runtime_deps_str = if runtime_deps_str.empty?
|
|
110
|
+
"(none)"
|
|
111
|
+
else
|
|
112
|
+
"```rb\n#{runtime_deps_str}\n```"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
dev_deps_str = dependencies.development.join("\n").strip
|
|
116
|
+
dev_deps_str = if dev_deps_str.empty?
|
|
117
|
+
"(none)"
|
|
118
|
+
else
|
|
119
|
+
"```rb\n#{dev_deps_str}\n```"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
dependencies = <<~DEPENDENCIES
|
|
123
|
+
## [Dependencies]
|
|
124
|
+
|
|
125
|
+
### Runtime Dependencies:
|
|
126
|
+
#{runtime_deps_str}
|
|
127
|
+
|
|
128
|
+
### Development Dependencies:
|
|
129
|
+
#{dev_deps_str}
|
|
130
|
+
DEPENDENCIES
|
|
131
|
+
|
|
132
|
+
Terminal.prettify_markdown(dependencies)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def versions_str
|
|
136
|
+
rows = self.class.versions(name: name).map do |version|
|
|
137
|
+
pretty_downloads = Number.humanized_integer(version.downloads)
|
|
138
|
+
"| #{version.release_date} | #{version.version} | #{pretty_downloads} |"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
table = <<~TABLE
|
|
142
|
+
## [Versions]
|
|
143
|
+
|
|
144
|
+
| *Release Date* | *Version* | *Downloads* |
|
|
145
|
+
|----------------|-----------|-------------|
|
|
146
|
+
#{rows.join("\n")}
|
|
147
|
+
TABLE
|
|
148
|
+
|
|
149
|
+
Terminal.prettify_markdown(table)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @return [Array<String>]
|
|
153
|
+
def urls
|
|
154
|
+
[
|
|
155
|
+
homepage_uri,
|
|
156
|
+
source_code_uri,
|
|
157
|
+
changelog_uri
|
|
158
|
+
].compact
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @return [String|nil]
|
|
162
|
+
def fetch_readme
|
|
163
|
+
GitRepo.from_urls(urls: urls, version: version)&.readme ||
|
|
164
|
+
"Info: Unable to find a valid readme based on available gem info"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [String|nil]
|
|
168
|
+
def fetch_changelog
|
|
169
|
+
GitRepo.from_urls(urls: urls, version: version)&.changelog ||
|
|
170
|
+
"Info: Unable to find a valid changelog based on available gem info"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @param name [String]
|
|
174
|
+
# @param version [String|nil] will default to latest if not provided
|
|
175
|
+
# @return [Gemview::Gem]
|
|
176
|
+
def self.find(name:, version: nil)
|
|
177
|
+
@find ||= {}
|
|
178
|
+
@find[[name, version]] ||= new case version
|
|
179
|
+
when String
|
|
180
|
+
Client.v2.info(name, version)
|
|
181
|
+
else
|
|
182
|
+
Client.v1.info(name)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @param term [String] search term
|
|
187
|
+
# @return [Array<Gemview::Gem>]
|
|
188
|
+
def self.search(term:)
|
|
189
|
+
Client.v1.search(term).map { |gem_hash| new gem_hash }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @param username [String] rubygems.org username
|
|
193
|
+
# @return [Array<Gemview::Gem>]
|
|
194
|
+
def self.author(username:)
|
|
195
|
+
Client.v1.gems(username).map { |gem_hash| new gem_hash }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @return [Array<Gemview::Gem>]
|
|
199
|
+
def self.latest
|
|
200
|
+
Client.v1.latest.map { |gem_hash| new gem_hash }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @return [Array<Gemview::Gem>]
|
|
204
|
+
def self.just_updated
|
|
205
|
+
Client.v1.just_updated.map { |gem_hash| new gem_hash }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# @param name [String] gem name
|
|
209
|
+
# @return [Array<Gemview::Gem::Version>]
|
|
210
|
+
def self.versions(name:)
|
|
211
|
+
@versions ||= {}
|
|
212
|
+
@versions[name] ||= Client.v1.versions(name).map { |gem_hash| Version.new gem_hash }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemview
|
|
4
|
+
class GitRepo
|
|
5
|
+
HOSTS = [
|
|
6
|
+
GITHUB = :github,
|
|
7
|
+
GITLAB = :gitlab
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
# @param urls [Array<String>]
|
|
11
|
+
# @param version [String]
|
|
12
|
+
# @return [Gemview::GitRepo|nil]
|
|
13
|
+
def self.from_urls(urls:, version:)
|
|
14
|
+
@from_urls ||= {}
|
|
15
|
+
|
|
16
|
+
base_url, git_host = nil
|
|
17
|
+
urls.each do |url|
|
|
18
|
+
base_url, git_host = parse_base_url(url)
|
|
19
|
+
if base_url && git_host
|
|
20
|
+
return @from_urls[base_url] ||= new(
|
|
21
|
+
base_url: base_url,
|
|
22
|
+
git_host: git_host,
|
|
23
|
+
version: version
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param [String]
|
|
31
|
+
# @return [base_url as `String` and git_host as `Symbol`] or nil if unsuccessful
|
|
32
|
+
def self.parse_base_url(url)
|
|
33
|
+
github_base_url = url[%r{^https?://github\.com/[^/]+/[^/]+}, 0]
|
|
34
|
+
return [github_base_url, GITHUB] if github_base_url
|
|
35
|
+
|
|
36
|
+
gitlab_base_url = url[%r{^https?://gitlab\.com/[^/]+/[^/]+}, 0]
|
|
37
|
+
[gitlab_base_url, GITLAB] if gitlab_base_url
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private_class_method :new, :parse_base_url
|
|
41
|
+
|
|
42
|
+
attr_reader :base_url, :git_host, :version
|
|
43
|
+
|
|
44
|
+
# @param base_url [String] base Git repo url for `HOSTS`
|
|
45
|
+
# @param git_host [Symbol] from `HOSTS`
|
|
46
|
+
# @param version [String]
|
|
47
|
+
def initialize(base_url:, git_host:, version:)
|
|
48
|
+
raise ArgumentError, "Invalid host: #{git_host}" unless HOSTS.include?(git_host)
|
|
49
|
+
|
|
50
|
+
@base_url = base_url.dup.freeze
|
|
51
|
+
@git_host = git_host
|
|
52
|
+
@version = version.dup.freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [String|nil]
|
|
56
|
+
def readme
|
|
57
|
+
return @readme if defined?(@readme)
|
|
58
|
+
|
|
59
|
+
@readme = fetch_raw_file("README.md")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [String|nil]
|
|
63
|
+
def changelog
|
|
64
|
+
return @changelog if defined?(@changelog)
|
|
65
|
+
|
|
66
|
+
@changelog = fetch_raw_file("CHANGELOG.md")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# @param filename [String]
|
|
72
|
+
# @return [String|nil]
|
|
73
|
+
def fetch_raw_file(filename)
|
|
74
|
+
case @git_host
|
|
75
|
+
when GITHUB then github_raw_file(filename)
|
|
76
|
+
when GITLAB then gitlab_raw_file(filename)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param filename [String]
|
|
81
|
+
# @return [String|nil]
|
|
82
|
+
def github_raw_file(filename)
|
|
83
|
+
# From: `https://github.com/charmbracelet/bubbles`
|
|
84
|
+
# To: `https://raw.githubusercontent.com/charmbracelet/bubbles/refs/tags/v0.20.0/README.md`
|
|
85
|
+
path = @base_url.sub(%r{^https?://github\.com}, "")
|
|
86
|
+
|
|
87
|
+
[
|
|
88
|
+
"https://raw.githubusercontent.com#{path}/refs/tags/v#{@version}/#{filename}",
|
|
89
|
+
"https://raw.githubusercontent.com#{path}/refs/tags/#{@version}/#{filename}"
|
|
90
|
+
].each do |url|
|
|
91
|
+
content = fetch(url)
|
|
92
|
+
return content if content
|
|
93
|
+
end
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @param filename [String]
|
|
98
|
+
# @return [String|nil]
|
|
99
|
+
def gitlab_raw_file(filename)
|
|
100
|
+
# From: `https://gitlab.com/gitlab-org/gitlab`
|
|
101
|
+
# To: `https://gitlab.com/gitlab-org/gitlab/-/raw/v17.5.1-ee/README.md?ref_type=tags&inline=false`
|
|
102
|
+
path = @base_url.sub(%r{^https?://gitlab\.com}, "")
|
|
103
|
+
|
|
104
|
+
[
|
|
105
|
+
"https://gitlab.com#{path}/-/raw/v#{@version}/#{filename}?ref_type=tags&inline=false",
|
|
106
|
+
"https://gitlab.com#{path}/-/raw/#{@version}/#{filename}?ref_type=tags&inline=false"
|
|
107
|
+
].each do |url|
|
|
108
|
+
content = fetch(url)
|
|
109
|
+
return content if content
|
|
110
|
+
end
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @param url [String]
|
|
115
|
+
# @return [String|nil]
|
|
116
|
+
def fetch(url)
|
|
117
|
+
response = Net::HTTP.get_response(URI(url))
|
|
118
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
119
|
+
body = response.body.force_encoding("UTF-8")
|
|
120
|
+
Terminal.prettify_markdown(body)
|
|
121
|
+
end
|
|
122
|
+
rescue Net::HTTPError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemview
|
|
4
|
+
module Number
|
|
5
|
+
# Ex. 1234567890 -> "1,234,567,890"
|
|
6
|
+
# @param integer [Integer]
|
|
7
|
+
# @return [String]
|
|
8
|
+
def self.humanized_integer(integer)
|
|
9
|
+
integer
|
|
10
|
+
.to_s
|
|
11
|
+
.chars
|
|
12
|
+
.reverse
|
|
13
|
+
.each_slice(3)
|
|
14
|
+
.map(&:join)
|
|
15
|
+
.join(",")
|
|
16
|
+
.reverse
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemview
|
|
4
|
+
module Terminal
|
|
5
|
+
# @param question [String]
|
|
6
|
+
# @return [Boolean]
|
|
7
|
+
def self.confirm(question:)
|
|
8
|
+
TTY::Prompt.new.yes?(question)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param content [String]
|
|
12
|
+
def self.page(content)
|
|
13
|
+
TTY::Pager.page(content)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param prompt [String]
|
|
17
|
+
# @param choices [Array<String>] or [Hash<String, String>] where all choices are unique
|
|
18
|
+
# @yield [String] yields until the user exits the prompt gracefully
|
|
19
|
+
def self.choose(message:, choices:, per_page: 6)
|
|
20
|
+
while (choice = selector.select(message, choices, per_page))
|
|
21
|
+
yield choice
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
TTY_COLOR = ENV["NO_COLOR"] ? :never : :auto
|
|
26
|
+
private_constant :TTY_COLOR
|
|
27
|
+
|
|
28
|
+
# A best effort attempt to format and highlight markdown text.
|
|
29
|
+
# If it's unsuccessful, it will return the original text.
|
|
30
|
+
#
|
|
31
|
+
# @param text [String]
|
|
32
|
+
# @return [String]
|
|
33
|
+
def self.prettify_markdown(text)
|
|
34
|
+
TTY::Markdown.parse(text, color: TTY_COLOR)
|
|
35
|
+
rescue # Return the raw markdown if parsing fails
|
|
36
|
+
text
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Selector]
|
|
40
|
+
def self.selector
|
|
41
|
+
@selector ||= Selector.new
|
|
42
|
+
end
|
|
43
|
+
private_class_method :selector
|
|
44
|
+
|
|
45
|
+
# Wrapper around `TTY::Prompt` that adds Vim keybindings and
|
|
46
|
+
# the ability to gracefully exit the prompt.
|
|
47
|
+
class Selector
|
|
48
|
+
def initialize
|
|
49
|
+
@prompt = TTY::Prompt.new(
|
|
50
|
+
quiet: true,
|
|
51
|
+
track_history: false,
|
|
52
|
+
interrupt: :exit,
|
|
53
|
+
symbols: {marker: ">"},
|
|
54
|
+
enable_color: !ENV["NO_COLOR"]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Indicate user intention to exit
|
|
58
|
+
@exit = false
|
|
59
|
+
|
|
60
|
+
# vim keybindings
|
|
61
|
+
@prompt.on(:keypress) do |event|
|
|
62
|
+
case event.value
|
|
63
|
+
when "j" # Move down
|
|
64
|
+
@prompt.trigger(:keydown)
|
|
65
|
+
when "k" # Move up
|
|
66
|
+
@prompt.trigger(:keyup)
|
|
67
|
+
when "h" # Move left
|
|
68
|
+
@prompt.trigger(:keyleft)
|
|
69
|
+
when "l" # Move right
|
|
70
|
+
@prompt.trigger(:keyright)
|
|
71
|
+
when "q" # Exit
|
|
72
|
+
@exit = true
|
|
73
|
+
@prompt.trigger(:keyenter)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Exit on escape
|
|
78
|
+
@prompt.on(:keyescape) do
|
|
79
|
+
@exit = true
|
|
80
|
+
@prompt.trigger(:keyenter)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param prompt [String]
|
|
85
|
+
# @param choices [Array<String>] where all choices are unique
|
|
86
|
+
# @param per_page [Integer] results per page
|
|
87
|
+
# @return [String|nil]
|
|
88
|
+
def select(message, choices, per_page)
|
|
89
|
+
choice = @prompt.select(
|
|
90
|
+
message,
|
|
91
|
+
choices,
|
|
92
|
+
per_page: per_page,
|
|
93
|
+
help: "(Press Enter to select and Escape to leave)",
|
|
94
|
+
show_help: :always
|
|
95
|
+
)
|
|
96
|
+
choice unless @exit
|
|
97
|
+
ensure
|
|
98
|
+
@exit = false
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
private_constant :Selector
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/gemview/view.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemview
|
|
4
|
+
module View
|
|
5
|
+
def self.info(gem:)
|
|
6
|
+
gem = Gem.find(name: gem.name, version: gem.version) if gem.dependencies.nil?
|
|
7
|
+
prompt = <<~PROMPT.chomp
|
|
8
|
+
#{gem.header_str}
|
|
9
|
+
More info:
|
|
10
|
+
PROMPT
|
|
11
|
+
|
|
12
|
+
Terminal.choose(message: prompt, choices: %w[Readme Changelog Dependencies Versions]) do |choice|
|
|
13
|
+
case choice
|
|
14
|
+
when "Readme"
|
|
15
|
+
Terminal.page([gem.header_str, gem.fetch_readme].join("\n"))
|
|
16
|
+
when "Changelog"
|
|
17
|
+
Terminal.page([gem.header_str, gem.fetch_changelog].join("\n"))
|
|
18
|
+
when "Dependencies"
|
|
19
|
+
Terminal.page([gem.header_str, gem.dependencies_str].join("\n"))
|
|
20
|
+
when "Versions"
|
|
21
|
+
Terminal.page([gem.header_str, gem.versions_str].join("\n"))
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "Unknown choice: #{choice}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.list(gems:)
|
|
29
|
+
gems_by_description = gems.to_h do |gem|
|
|
30
|
+
[gem.selector_str, gem]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Terminal.choose(message: "Choose a gem:", choices: gems_by_description) do |gem|
|
|
34
|
+
info(gem: gem)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/gemview.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "gemview/version"
|
|
4
|
+
|
|
5
|
+
module Gemview
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Internal
|
|
9
|
+
autoload :Client, "gemview/client"
|
|
10
|
+
autoload :Commands, "gemview/commands"
|
|
11
|
+
autoload :Gem, "gemview/gem"
|
|
12
|
+
autoload :GitRepo, "gemview/git_repo"
|
|
13
|
+
autoload :Number, "gemview/number"
|
|
14
|
+
autoload :Terminal, "gemview/terminal"
|
|
15
|
+
autoload :Version, "gemview/version"
|
|
16
|
+
autoload :View, "gemview/view"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# External
|
|
20
|
+
autoload :Gems, "gems"
|
|
21
|
+
autoload :Strings, "strings"
|
|
22
|
+
module Net
|
|
23
|
+
autoload :HTTP, "net/http"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module TTY
|
|
27
|
+
autoload :Markdown, "tty-markdown"
|
|
28
|
+
autoload :Pager, "tty-pager"
|
|
29
|
+
autoload :Prompt, "tty-prompt"
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gemview
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- apainintheneck
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-12-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dry-cli
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.2.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 1.2.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: dry-struct
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 1.6.0
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 1.6.0
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: gems
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 1.3.0
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 1.3.0
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: strings
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 0.2.1
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 0.2.1
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: tty-markdown
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 0.7.2
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 0.7.2
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: tty-pager
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 0.14.0
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 0.14.0
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: tty-prompt
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 0.23.1
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 0.23.1
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: zeitwerk
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "<"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '2.7'
|
|
118
|
+
type: :runtime
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "<"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '2.7'
|
|
125
|
+
description: 'An unofficial CLI interface to browse rubygems.org. Search for gems
|
|
126
|
+
by name, see which ones have been recently updated and look at their dependencies.
|
|
127
|
+
|
|
128
|
+
'
|
|
129
|
+
email:
|
|
130
|
+
- apainintheneck@gmail.com
|
|
131
|
+
executables:
|
|
132
|
+
- gemview
|
|
133
|
+
extensions: []
|
|
134
|
+
extra_rdoc_files: []
|
|
135
|
+
files:
|
|
136
|
+
- ".rspec"
|
|
137
|
+
- CHANGELOG.md
|
|
138
|
+
- LICENSE.txt
|
|
139
|
+
- README.md
|
|
140
|
+
- Rakefile
|
|
141
|
+
- exe/gemview
|
|
142
|
+
- lib/gemview.rb
|
|
143
|
+
- lib/gemview/client.rb
|
|
144
|
+
- lib/gemview/commands.rb
|
|
145
|
+
- lib/gemview/gem.rb
|
|
146
|
+
- lib/gemview/git_repo.rb
|
|
147
|
+
- lib/gemview/number.rb
|
|
148
|
+
- lib/gemview/terminal.rb
|
|
149
|
+
- lib/gemview/version.rb
|
|
150
|
+
- lib/gemview/view.rb
|
|
151
|
+
homepage: https://github.com/apainintheneck/gemview
|
|
152
|
+
licenses:
|
|
153
|
+
- MIT
|
|
154
|
+
metadata:
|
|
155
|
+
homepage_uri: https://github.com/apainintheneck/gemview
|
|
156
|
+
source_code_uri: https://github.com/apainintheneck/gemview
|
|
157
|
+
changelog_uri: https://github.com/apainintheneck/gemview/blob/main/CHANGELOG.md
|
|
158
|
+
post_install_message:
|
|
159
|
+
rdoc_options: []
|
|
160
|
+
require_paths:
|
|
161
|
+
- lib
|
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - ">="
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: 3.0.0
|
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - ">="
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: '0'
|
|
172
|
+
requirements: []
|
|
173
|
+
rubygems_version: 3.5.23
|
|
174
|
+
signing_key:
|
|
175
|
+
specification_version: 4
|
|
176
|
+
summary: An unofficial CLI interface to browse rubygems.org
|
|
177
|
+
test_files: []
|