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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2024-12-08
4
+
5
+ - Initial release
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
+ ![GIF demoing the info and search subcommands](./assets/gemview-v1.0.0.gif)
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "standard/rake"
5
+ require "rspec/core/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: %i[standard spec]
data/exe/gemview ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ require "gemview"
5
+
6
+ Gemview::Commands.start
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemview
4
+ VERSION = "1.0.0"
5
+ end
@@ -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: []