githuh 0.2.1 → 0.4.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.
data/lefthook.yml ADDED
@@ -0,0 +1,35 @@
1
+ output:
2
+ - summary
3
+ - failure
4
+
5
+ pre-commit:
6
+ parallel: true
7
+ jobs:
8
+ - name: lint
9
+ run: bundle exec rubocop -c .rubocop.yml {staged_files}
10
+ glob: "*.{rb,Gemfile}"
11
+ stage_fixed: true
12
+
13
+ - name: check for conflict markers and whitespace issues
14
+ run: git --no-pager diff --check
15
+
16
+ # If tests take >1 second, move this (or just the long-running tests) to pre-push.
17
+ - name: run tests
18
+ run: just test
19
+
20
+ - name: fix rubocop formatting issues
21
+ run: bundle exec rubocop -a {staged_files}
22
+ glob: "*.{rb,Gemfile,gemspec}"
23
+ stage_fixed: true
24
+
25
+ - name: spell check
26
+ run: codespell {staged_files}
27
+ glob: "*.{rb,md,gemspec}"
28
+
29
+ - name: format markdown
30
+ run: mdformat {staged_files}
31
+ glob: "*.md"
32
+ stage_fixed: true
33
+
34
+ - name: scan for secrets
35
+ run: detect-secrets-hook --baseline .secrets.baseline
@@ -28,31 +28,39 @@ module Githuh
28
28
  end
29
29
  end
30
30
 
31
- attr_accessor :client, :token, :per_page, :verbose, :info, :box, :context
31
+ attr_accessor :token, :per_page, :verbose, :info, :box, :context
32
32
 
33
33
  def call(api_token: nil,
34
34
  per_page: DEFAULT_PAGE_SIZE,
35
35
  verbose: false,
36
36
  info: true)
37
-
38
37
  self.context = Githuh
39
38
  self.verbose = verbose
40
39
  self.info = info
41
- self.token = api_token || token_from_gitconfig
42
- self.per_page = per_page.to_i || DEFAULT_PAGE_SIZE
43
- self.client = Octokit::Client.new(access_token: token)
40
+ self.token = api_token || determine_github_token
41
+ self.per_page = per_page.to_i
42
+
43
+ return unless info
44
+
45
+ begin
46
+ print_userinfo
47
+ rescue StandardError
48
+ nil
49
+ end
50
+ end
44
51
 
45
- print_userinfo if info
52
+ def client
53
+ @client ||= Octokit::Client.new(access_token: token)
46
54
  end
47
55
 
48
56
  protected
49
57
 
50
- def puts(*args)
51
- stdout.puts(*args)
58
+ def puts(*)
59
+ stdout.puts(*)
52
60
  end
53
61
 
54
- def warn(*args)
55
- stderr.puts(*args)
62
+ def warn(*)
63
+ stderr.puts(*)
56
64
  end
57
65
 
58
66
  def user_info
@@ -83,20 +91,34 @@ module Githuh
83
91
  complete: '▉'.magenta)
84
92
  end
85
93
 
94
+ def determine_github_token
95
+ @github_token ||= ENV['GITHUB_TOKEN'] || `git config --global --get user.token`.chomp
96
+
97
+ return @github_token unless @github_token.empty?
98
+
99
+ raise "No token was found in your ~/.gitconfig.\n" \
100
+ "To add, run the following command: \n" \
101
+ "git config --global --set user.token YOUR_GITHUB_TOKEN\n" \
102
+ "or set environment variable GITHUB_TOKEN"
103
+ end
104
+
86
105
  private
87
106
 
88
107
  def print_userinfo
89
108
  duration = DateTime.now - DateTime.parse(user_info[:created_at].to_s)
90
109
  years = (duration / 365).to_i
91
- months = ((duration - years * 365) / 30).to_i
92
- days = (duration - years * 365 - months * 30).to_i
110
+ months = ((duration - (years * 365)) / 30).to_i
111
+ days = (duration - (years * 365) - (months * 30)).to_i
93
112
 
94
113
  lines = []
95
- lines << sprintf(" Github API Token: %s", h("#{token[0..9]}#{'.' * 20}#{token[-11..-1]}"))
114
+ lines << sprintf(" Github API Token: %s", h("#{token[0..9]}#{'.' * 20}#{token[-11..]}"))
96
115
  lines << sprintf(" Current User: %s", h(user_info.login))
97
116
  lines << sprintf(" Public Repos: %s", h(user_info.public_repos.to_s))
98
117
  lines << sprintf(" Followers: %s", h(user_info.followers.to_s))
99
- lines << sprintf(" Member For: %s", h(sprintf("%d years, %d months, %d days", years, months, days)))
118
+ lines << sprintf(
119
+ " Member For: %s",
120
+ h("%<years>d years, %<months>d months, %<days>d days" % { years:, months:, days: })
121
+ )
100
122
 
101
123
  self.box = TTY::Box.frame(*lines,
102
124
  padding: 0,
@@ -116,16 +138,6 @@ module Githuh
116
138
  def h(arg)
117
139
  arg.to_s
118
140
  end
119
-
120
- def token_from_gitconfig
121
- @token_from_gitconfig ||= `git config --global --get user.token`.chomp
122
-
123
- return @token_from_gitconfig unless @token_from_gitconfig.empty?
124
-
125
- raise "No token was found in your ~/.gitconfig.\n" \
126
- "To add, run the following command: \n" \
127
- "git config --global --set user.token YOUR_GITHUB_TOKEN"
128
- end
129
141
  end
130
142
  end
131
143
  end
@@ -2,14 +2,15 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # vim: ft=ruby
5
- require 'bundler/setup'
6
- require 'dry/cli'
7
- require 'json'
8
- require 'tty/progressbar'
9
- require 'csv'
10
- require 'active_support/inflector'
5
+ require "bundler/setup"
6
+ require "dry/cli"
7
+ require "json"
8
+ require "tty/progressbar"
9
+ require "active_support/inflector"
10
+ require "yaml"
11
+ require "csv"
11
12
 
12
- require_relative '../base'
13
+ require_relative "../base"
13
14
 
14
15
  module Githuh
15
16
  module CLI
@@ -17,40 +18,51 @@ module Githuh
17
18
  module Issue
18
19
  class Export < Base
19
20
  FORMATS = {
20
- json: 'json',
21
- csv: 'csv'
21
+ json: "json",
22
+ csv: "csv",
22
23
  }.freeze
23
24
 
24
- DEFAULT_FORMAT = :csv
25
+ DEFAULT_FORMAT = :csv
25
26
  DEFAULT_OUTPUT_FORMAT = "<username>.<repo>.issues.<format>"
26
27
 
27
- attr_accessor :filename, :file, :output, :repo, :issues, :format, :record_count
28
+ attr_accessor :filename, :file, :output, :repo, :issues, :format, :record_count, :mapping
28
29
 
29
- desc "Export Repo issues into a CSV or JSON format\n" \
30
- " Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
30
+ desc "Export Repo issues into a CSV or JSON format\n " \
31
+ "Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
31
32
 
32
33
  argument :repo, type: :string, required: true, desc: 'Name of the repo, eg "rails/rails"'
33
- option :file, required: false, desc: 'Output file, overrides ' + DEFAULT_OUTPUT_FORMAT
34
- option :format, values: FORMATS.keys.map(&:to_s), default: DEFAULT_FORMAT.to_s, required: false, desc: 'Output format'
34
+ option :file, required: false, desc: "Output file, overrides #{DEFAULT_OUTPUT_FORMAT}"
35
+ option :format, values: FORMATS.keys.map(&:to_s), default: DEFAULT_FORMAT.to_s, required: false, desc: "Output format"
36
+ option :mapping, type: :string, require: false, desc: "YAML file with label to estimates mapping"
35
37
 
36
- def call(repo: nil, file: nil, format: nil, **opts)
37
- super(**opts)
38
+ def call(repo: nil, file: nil, format: nil, mapping: nil, **)
39
+ super(**)
38
40
 
39
41
  self.record_count = 0
40
- self.repo = repo
42
+ self.repo = repo
41
43
 
42
44
  raise ArgumentError, "argument <repo> is required" unless repo
43
45
  raise ArgumentError, "argument <repo> is not a repository, expected eg 'rails/rails'" unless repo =~ %r{/}
44
46
 
47
+ self.mapping = {}
48
+ if mapping && ::File.exist?(mapping)
49
+ self.mapping = ::YAML.safe_load_file(mapping)['label-to-estimates'] || {}
50
+ end
51
+
52
+ Export.send(:remove_const, :LabelEstimates) if Export.const_defined?(:LabelEstimates)
53
+ Export.const_set(:LabelEstimates, self.mapping)
54
+
45
55
  self.issues = []
46
56
  self.output = StringIO.new
47
57
  self.format = (format || DEFAULT_FORMAT).to_sym
48
58
 
49
59
  self.filename = file || file_name(repo)
50
- self.file = File.open(filename, 'w')
60
+ self.file = File.open(filename, "w")
51
61
 
52
62
  print_summary
53
63
 
64
+ raise ArgumentError, "Format is not provided" unless FORMATS.key?(format&.to_sym)
65
+
54
66
  # —————————— actually get all issues ———————————————
55
67
  self.file.write send("render_as_#{format}", fetch_issues)
56
68
  # ————————————————————————————————————————————————————————
@@ -62,9 +74,9 @@ module Githuh
62
74
 
63
75
  def fetch_issues
64
76
  client.auto_paginate = true
65
- self.issues = filter_issues(client.issues(repo, query: default_options)).tap do |issue_list|
77
+ self.issues = filter_issues(client.issues(repo, query: default_options)).tap do |issue_list|
66
78
  self.record_count = issue_list.size
67
- bar('Issues')&.advance
79
+ bar("Issues")&.advance
68
80
  end
69
81
  end
70
82
 
@@ -79,7 +91,7 @@ module Githuh
79
91
  end
80
92
 
81
93
  def default_options
82
- { state: 'open' }
94
+ { state: "open" }
83
95
  end
84
96
 
85
97
  def self.issue_labels(issue)
@@ -87,42 +99,33 @@ module Githuh
87
99
  end
88
100
 
89
101
  def self.find_user(client, username)
90
- @user_cache ||= {}
102
+ @user_cache ||= {}
91
103
  @user_cache[username] ||= client.user(username).name
92
104
  end
93
105
 
94
106
  CSV_MAP = {
95
- 'Labels' => ->(_client, issue) { issue_labels(issue).reject { |l| LABEL_ESTIMATES.key?(l) }.join(',').downcase },
96
- 'Type' => ->(*) { 'feature' },
97
- 'Estimate' => ->(_client, issue) do
98
- el = issue_labels(issue).find { |l| LABEL_ESTIMATES.key?(l) }
99
- el ? LABEL_ESTIMATES[el] : nil
107
+ "Labels" => ->(_client, issue) { issue_labels(issue).reject { |l| LabelEstimates.key?(l) }.join(",").downcase },
108
+ "Type" => ->(*) { "feature" },
109
+ "Estimate" => ->(_client, issue) do
110
+ el = issue_labels(issue).find { |l| LabelEstimates.key?(l) }
111
+ el ? LabelEstimates[el] : nil
100
112
  end,
101
- 'Current State' => ->(*) { 'unstarted' },
102
- 'Requested By' => ->(client, issue) do
113
+ "Current State" => ->(*) { "unstarted" },
114
+ "Requested By" => ->(client, issue) do
103
115
  find_user(client, issue.user.login)
104
116
  end,
105
- 'Owned By' => ->(client, issue) do
117
+ "Owned By" => ->(client, issue) do
106
118
  find_user(client, issue.user.login)
107
119
  end,
108
- 'Description' => ->(_client, issue) {
120
+ "Description" => ->(_client, issue) {
109
121
  issue.body
110
122
  },
111
- 'Created at' => ->(_client, issue) { issue.created_at },
112
- }.freeze
113
-
114
- LABEL_ESTIMATES = {
115
- 'XXL(13 eng day)' => 15,
116
- 'XL(8 eng day)' => 15,
117
- 'xs(<=1 eng day)' => 3,
118
- 'L(5 eng day)' => 15,
119
- 'm(3 eng day)' => 9,
120
- 's(2 eng day)' => 6
123
+ "Created at" => ->(_client, issue) { issue.created_at },
121
124
  }.freeze
122
125
 
123
126
  CSV_HEADER = %w(Id Title Labels Type Estimate) +
124
- ['Current State', 'Created at', 'Accepted at', 'Deadline', 'Requested By',
125
- 'Owned By', 'Description', 'Comment', 'Comment', 'Comment', 'Comment'].freeze
127
+ ["Current State", "Created at", "Accepted at", "Deadline", "Requested By",
128
+ "Owned By", "Description", "Comment", "Comment", "Comment", "Comment"].freeze
126
129
 
127
130
  # Id,Title,Labels,Type,Estimate,Current State,Created at,Accepted at,Deadline,Requested By,Owned By,Description,Comment,Comment
128
131
  # 100, existing started story,"label one,label two",feature,1,started,"Nov 22, 2007",,,user1,user2,this will update story 100,,
@@ -136,16 +139,16 @@ module Githuh
136
139
  row = []
137
140
  CSV_HEADER.each do |column|
138
141
  method = column.downcase.underscore.to_sym
139
- value = if CSV_MAP[column]
140
- CSV_MAP[column][client, issue]
141
- else
142
- begin
143
- issue.to_h[method]
144
- rescue StandardError
145
- nil
146
- end
147
- end
148
- value = value.strip if value.is_a?(String)
142
+ value = if CSV_MAP[column]
143
+ CSV_MAP[column][client, issue]
144
+ else
145
+ begin
146
+ issue.to_h[method]
147
+ rescue StandardError
148
+ nil
149
+ end
150
+ end
151
+ value = value.strip if value.is_a?(String)
149
152
  row << value
150
153
  end
151
154
  csv << row
@@ -173,8 +176,7 @@ module Githuh
173
176
  puts TTY::Box.info("Format : #{self.format}\n" \
174
177
  "File : #{filename}\n" \
175
178
  "Repo : #{repo}\n",
176
- width: ui_width,
177
- padding: 1)
179
+ width: ui_width, padding: 1)
178
180
  puts
179
181
  end
180
182
 
@@ -184,8 +186,8 @@ module Githuh
184
186
  end
185
187
  end
186
188
 
187
- register 'issue', aliases: ['r'] do |prefix|
188
- prefix.register 'export', Issue::Export
189
+ register "issue", aliases: ["r"] do |prefix|
190
+ prefix.register "export", Issue::Export
189
191
  end
190
192
  end
191
193
  end
@@ -4,10 +4,12 @@
4
4
  # vim: ft=ruby
5
5
  require 'bundler/setup'
6
6
  require 'dry/cli'
7
+ require 'base64'
7
8
  require 'json'
8
9
  require 'tty/progressbar'
9
10
 
10
11
  require_relative '../base'
12
+ require_relative '../../../llm'
11
13
 
12
14
  module Githuh
13
15
  module CLI
@@ -23,18 +25,42 @@ module Githuh
23
25
  DEFAULT_OUTPUT_FORMAT = "<username>.repositories.<format>"
24
26
  FORK_OPTIONS = %w(exclude include only).freeze
25
27
 
26
- attr_accessor :filename, :file, :output, :repos, :format, :forks, :private, :record_count
28
+ attr_accessor :filename, :file, :output, :repos, :format,
29
+ :forks, :private, :record_count, :llm_adapter
27
30
 
28
- desc "List owned repositories and render the output in markdown or JSON\n" \
29
- " Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
31
+ desc "List owned repositories and render the output in markdown or JSON\n " \
32
+ "Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
30
33
 
31
- option :file, required: false, desc: 'Output file, overrides ' + DEFAULT_OUTPUT_FORMAT
32
- option :format, values: FORMATS.keys, default: DEFAULT_FORMAT.to_s, required: false, desc: 'Output format'
33
- option :forks, type: :string, values: FORK_OPTIONS, default: FORK_OPTIONS.first, required: false, desc: 'Include or exclude forks'
34
- option :private, type: :boolean, default: nil, required: false, desc: 'If specified, returns only private repos for true, public for false'
34
+ option :file,
35
+ required: false,
36
+ desc: "Output file, overrides #{DEFAULT_OUTPUT_FORMAT}"
35
37
 
36
- def call(file: nil, format: nil, forks: nil, private: nil, **opts)
37
- super(**opts)
38
+ option :format,
39
+ values: FORMATS.keys,
40
+ default: DEFAULT_FORMAT.to_s,
41
+ required: false,
42
+ desc: 'Output format'
43
+
44
+ option :forks, type: :string,
45
+ values: FORK_OPTIONS,
46
+ default: FORK_OPTIONS.first,
47
+ required: false,
48
+ desc: 'Include or exclude forks'
49
+
50
+ option :private,
51
+ type: :boolean,
52
+ default: nil,
53
+ required: false,
54
+ desc: 'If specified, returns only private repos for true, public for false'
55
+
56
+ option :llm,
57
+ type: :boolean,
58
+ default: false,
59
+ required: false,
60
+ desc: 'Use LLM (ANTHROPIC_API_KEY or OPENAI_API_KEY) to summarize README'
61
+
62
+ def call(file: nil, format: nil, forks: nil, private: nil, llm: false, **)
63
+ super(**)
38
64
 
39
65
  self.record_count = 0
40
66
  self.forks = forks
@@ -42,6 +68,7 @@ module Githuh
42
68
  self.repos = []
43
69
  self.output = StringIO.new
44
70
  self.format = (format || DEFAULT_FORMAT).to_sym
71
+ self.llm_adapter = build_llm_adapter if llm
45
72
 
46
73
  self.filename = file || "#{user_info.login}.repositories.#{FORMATS[self.format]}"
47
74
  self.file = File.open(filename, 'w')
@@ -98,55 +125,139 @@ module Githuh
98
125
  end
99
126
 
100
127
  def bar_size
101
- client.last_response.rels[:last].href.match(/page=(\d+).*$/)[1].to_i
128
+ return 1 if client&.last_response.nil?
129
+
130
+ client&.last_response&.rels&.[](:last)&.href&.match(/page=(\d+).*$/)&.[](1)&.to_i # rubocop:disable Style/SafeNavigationChainLength
102
131
  end
103
132
 
104
133
  def render_as_markdown(repositories)
105
134
  output.puts "### #{client.user.name}'s Repos\n"
135
+
136
+ llm_bar = build_llm_progress_bar(repositories.size) if llm_adapter
137
+
106
138
  repositories.each_with_index do |repo, index|
107
139
  output.puts repo_as_markdown(index, repo)
140
+ llm_bar&.advance
141
+ end
142
+
143
+ if llm_bar
144
+ llm_bar.finish
145
+ puts
108
146
  end
147
+
109
148
  output.string
110
149
  end
111
150
 
151
+ def build_llm_progress_bar(total)
152
+ return unless info || verbose
153
+
154
+ color = llm_adapter.class.const_defined?(:BAR_COLOR) ? llm_adapter.class::BAR_COLOR : :cyan
155
+ provider = llm_adapter.class.name.split('::').last
156
+
157
+ puts
158
+ puts " • Summarizing #{total} READMEs with #{provider}…".send(color)
159
+ TTY::ProgressBar.new("[:bar]",
160
+ title: 'LLM Summaries',
161
+ total: total,
162
+ width: ui_width - 2,
163
+ head: '',
164
+ complete: '▉'.send(color))
165
+ end
166
+
112
167
  def render_as_json(repositories)
113
168
  JSON.pretty_generate(repositories.map(&:to_hash))
114
169
  end
115
170
 
116
171
  def repo_as_markdown(index, repo)
172
+ description = describe(repo)
173
+
117
174
  <<~REPO
118
175
 
119
176
  ### #{index + 1}. [#{repo.name}](#{repo.url}) (#{repo.stargazers_count} ★)
120
177
 
121
- #{repo.language ? "**#{repo.language}**. " : ''}
122
- #{repo.license ? "Distributed under the **#{repo.license.name}** license." : ''}
178
+ #{"**#{repo.language}**. " if repo.language}
179
+ #{"Distributed under the **#{repo.license.name}** license." if repo.license}
123
180
 
124
- #{repo.description}
181
+ #{description}
125
182
 
126
183
  REPO
127
184
  end
128
185
 
186
+ def describe(repo)
187
+ return repo.description unless llm_adapter
188
+
189
+ readme = fetch_readme(repo)
190
+ return repo.description if readme.nil? || readme.empty?
191
+
192
+ llm_adapter.summarize(readme)
193
+ rescue StandardError => e
194
+ warn "LLM summary failed for #{repo_full_name(repo)}: #{e.message}" if verbose
195
+ repo.description
196
+ end
197
+
198
+ def fetch_readme(repo)
199
+ readme = client.readme(repo_full_name(repo))
200
+ return nil unless readme
201
+
202
+ encoded = readme.respond_to?(:content) ? readme.content : readme[:content]
203
+ return nil if encoded.nil? || encoded.to_s.empty?
204
+
205
+ Base64.decode64(encoded).force_encoding('UTF-8')
206
+ rescue StandardError => e
207
+ warn "README fetch failed for #{repo_full_name(repo)}: #{e.message}" if verbose
208
+ nil
209
+ end
210
+
211
+ def repo_full_name(repo)
212
+ repo.respond_to?(:full_name) && repo.full_name ? repo.full_name : repo.name
213
+ end
214
+
215
+ def build_llm_adapter
216
+ adapter = Githuh::LLM.build
217
+ raise Githuh::LLM::Error, '--llm was specified but neither ANTHROPIC_API_KEY nor OPENAI_API_KEY is set' unless adapter
218
+
219
+ announce_llm(adapter) if info
220
+ adapter
221
+ end
222
+
223
+ def announce_llm(adapter)
224
+ provider = adapter.class.name.split('::').last
225
+ model = adapter.class.const_defined?(:MODEL) ? adapter.class::MODEL : 'n/a'
226
+
227
+ puts
228
+ puts TTY::Box.info(
229
+ "LLM summaries: ENABLED\n" \
230
+ "Provider : #{provider}\n" \
231
+ "Model : #{model}\n" \
232
+ "\n" \
233
+ "For each repository, the README will be fetched and summarized\n" \
234
+ "into a 5-6 sentence description before writing to the output file.",
235
+ width: ui_width, padding: 1
236
+ )
237
+ puts
238
+ end
239
+
129
240
  private
130
241
 
131
242
  def filter_result!(result)
132
243
  result.reject! do |r|
133
244
  fork_reject = case forks
134
- when 'exclude'
135
- r.fork
136
- when 'only'
137
- !r.fork
138
- when 'include'
139
- false
140
- end
245
+ when 'exclude'
246
+ r.fork
247
+ when 'only'
248
+ !r.fork
249
+ when 'include'
250
+ false
251
+ end
141
252
 
142
253
  private_reject = case private
143
- when true
144
- !r.private
145
- when false
146
- r.private
147
- when nil
148
- false
149
- end
254
+ when true
255
+ !r.private
256
+ when false
257
+ r.private
258
+ when nil
259
+ false
260
+ end
150
261
 
151
262
  fork_reject || private_reject
152
263
  end
@@ -15,9 +15,9 @@ module Githuh
15
15
  class Info < Base
16
16
  desc "Print user information"
17
17
 
18
- def call(**opts)
19
- super(**opts)
20
- puts JSON.pretty_generate(client.user.to_hash)
18
+ def call(**)
19
+ super
20
+ ap client.user.to_hash
21
21
  end
22
22
  end
23
23
  end
@@ -6,17 +6,17 @@ require 'tty/box'
6
6
  require 'tty/screen'
7
7
 
8
8
  require 'githuh'
9
+ require 'githuh/cli/commands/base'
10
+
9
11
  module Githuh
10
12
  module CLI
11
13
  class Launcher
12
14
  attr_accessor :argv, :stdin, :stdout, :stderr, :kernel, :command
13
15
 
14
- def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = nil)
15
- if ::Githuh.launcher
16
- raise(ArgumentError, "Another instance of CLI Launcher was detected, aborting.")
17
- else
18
- Githuh.launcher = self
19
- end
16
+ def initialize(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr, kernel = nil)
17
+ raise(ArgumentError, "Another instance of CLI Launcher was detected, aborting.") if ::Githuh.launcher
18
+
19
+ Githuh.launcher = self
20
20
 
21
21
  self.argv = argv
22
22
  self.stdin = stdin
@@ -26,27 +26,26 @@ module Githuh
26
26
  end
27
27
 
28
28
  def execute!
29
- if argv.empty? || !(%w(--help -h) & argv).empty?
29
+ if argv.empty? || !!%w(--help -h).intersect?(argv)
30
30
  stdout.puts BANNER
31
31
  Githuh.configure_kernel_behavior! help: true
32
32
  else
33
33
  Githuh.configure_kernel_behavior!
34
34
  end
35
35
 
36
+ # noinspection RubyYardParamTypeMatch
36
37
  self.command = ::Dry::CLI.new(::Githuh::CLI::Commands)
37
38
  command.call(arguments: argv, out: stdout, err: stderr)
38
39
  rescue StandardError => e
39
- lines = [e.message.gsub(/\n/, ', ')]
40
- if e.backtrace
40
+ lines = [e.message.gsub("\n", ', ')]
41
+ if e.backtrace && !!ARGV.intersect?(%w[-v --verbose])
41
42
  lines << ''
42
43
  lines.concat(e.backtrace)
43
44
  end
44
45
 
45
46
  box = TTY::Box.frame(*lines,
46
- **BOX_OPTIONS.merge(
47
- width: TTY::Screen.width,
48
- title: { top_center: "┤ #{e.class.name} ├" },
49
- ))
47
+ **BOX_OPTIONS, width: TTY::Screen.width,
48
+ title: { top_center: "┤ #{e.class.name} ├" })
50
49
  stderr.puts
51
50
  stderr.print box
52
51
  ensure
@@ -59,7 +58,7 @@ module Githuh
59
58
  end
60
59
  end
61
60
 
62
- BANNER = <<~BANNER
61
+ BANNER = <<~BANNER.freeze
63
62
 
64
63
  #{'Githuh CLI'.bold.yellow} #{::Githuh::VERSION.bold.green} — API client for Github.com.
65
64
  #{'© 2020 Konstantin Gredeskoul, All rights reserved. MIT License.'.cyan}