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.
- checksums.yaml +4 -4
- data/.envrc +6 -2
- data/.github/workflows/ruby.yml +8 -19
- data/.gitignore +5 -0
- data/.rubocop.yml +5 -2
- data/.rubocop_todo.yml +62 -84
- data/.secrets.baseline +127 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +13 -1
- data/Gemfile.lock +266 -124
- data/{LICENSE → LICENSE.txt} +1 -1
- data/README.adoc +18 -2
- data/Rakefile +13 -6
- data/config/label-mapping.yml +12 -0
- data/docs/img/badge.svg +21 -0
- data/docs/img/coverage.png +0 -0
- data/docs/img/coverage.svg +3 -3
- data/githuh.gemspec +8 -12
- data/justfile +57 -0
- data/lefthook.yml +35 -0
- data/lib/githuh/cli/commands/base.rb +36 -24
- data/lib/githuh/cli/commands/issue/export.rb +60 -58
- data/lib/githuh/cli/commands/repo/list.rb +138 -27
- data/lib/githuh/cli/commands/user/info.rb +3 -3
- data/lib/githuh/cli/launcher.rb +13 -14
- data/lib/githuh/env_loader.rb +34 -0
- data/lib/githuh/llm/anthropic.rb +29 -0
- data/lib/githuh/llm/base.rb +69 -0
- data/lib/githuh/llm/openai.rb +26 -0
- data/lib/githuh/llm.rb +31 -0
- data/lib/githuh/version.rb +1 -1
- data/lib/githuh.rb +10 -8
- metadata +45 -106
- data/issues.json +0 -3168
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 :
|
|
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 ||
|
|
42
|
-
self.per_page = per_page.to_i
|
|
43
|
-
|
|
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
|
-
|
|
52
|
+
def client
|
|
53
|
+
@client ||= Octokit::Client.new(access_token: token)
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
protected
|
|
49
57
|
|
|
50
|
-
def puts(*
|
|
51
|
-
stdout.puts(*
|
|
58
|
+
def puts(*)
|
|
59
|
+
stdout.puts(*)
|
|
52
60
|
end
|
|
53
61
|
|
|
54
|
-
def warn(*
|
|
55
|
-
stderr.puts(*
|
|
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
|
|
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(
|
|
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
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
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
|
|
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:
|
|
21
|
-
csv:
|
|
21
|
+
json: "json",
|
|
22
|
+
csv: "csv",
|
|
22
23
|
}.freeze
|
|
23
24
|
|
|
24
|
-
DEFAULT_FORMAT
|
|
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
|
-
"
|
|
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:
|
|
34
|
-
option :format, values: FORMATS.keys.map(&:to_s), default: DEFAULT_FORMAT.to_s, required: false, desc:
|
|
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, **
|
|
37
|
-
super(**
|
|
38
|
+
def call(repo: nil, file: nil, format: nil, mapping: nil, **)
|
|
39
|
+
super(**)
|
|
38
40
|
|
|
39
41
|
self.record_count = 0
|
|
40
|
-
self.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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
el = issue_labels(issue).find { |l|
|
|
99
|
-
el ?
|
|
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
|
-
|
|
102
|
-
|
|
113
|
+
"Current State" => ->(*) { "unstarted" },
|
|
114
|
+
"Requested By" => ->(client, issue) do
|
|
103
115
|
find_user(client, issue.user.login)
|
|
104
116
|
end,
|
|
105
|
-
|
|
117
|
+
"Owned By" => ->(client, issue) do
|
|
106
118
|
find_user(client, issue.user.login)
|
|
107
119
|
end,
|
|
108
|
-
|
|
120
|
+
"Description" => ->(_client, issue) {
|
|
109
121
|
issue.body
|
|
110
122
|
},
|
|
111
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
value
|
|
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:
|
|
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
|
|
188
|
-
prefix.register
|
|
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,
|
|
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
|
-
"
|
|
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,
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
#{
|
|
122
|
-
#{
|
|
178
|
+
#{"**#{repo.language}**. " if repo.language}
|
|
179
|
+
#{"Distributed under the **#{repo.license.name}** license." if repo.license}
|
|
123
180
|
|
|
124
|
-
#{
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
data/lib/githuh/cli/launcher.rb
CHANGED
|
@@ -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 =
|
|
15
|
-
if ::Githuh.launcher
|
|
16
|
-
|
|
17
|
-
|
|
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? ||
|
|
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(
|
|
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.
|
|
47
|
-
|
|
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}
|