gh-summary 0.1.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: 228ab8d8992d696d9e505a82f4ea178824652fffd40ec56c860f34d3ef2e5853
4
+ data.tar.gz: 5f8fac59f24a7a5e725d83fdb894684ed658bb454ddf02c874bc86705a06d57c
5
+ SHA512:
6
+ metadata.gz: 6a4208d706a7626fc6e74149bab22b0e0b277f49bce7df6e6b9b1ecd68179769d3761254635bd239c5c1a465088682a44f9bdbc24eaaca6940a6d88b0ae6ef64
7
+ data.tar.gz: 133ba481345bedb37913f1b27c0ef0e95baa66b42fa3bb147cdc55de9904ef29a405362ca595ff364b5196819ec5dcce248c843903f821032f919aa839236803
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikita Miloserdov
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # gh-summary
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/gh-summary.svg)](https://rubygems.org/gems/gh-summary)
4
+
5
+ Your GitHub day at a glance — PRs, reviews, issues, notifications — all in one terminal command.
6
+
7
+ Zero dependencies. Ruby + `gh` CLI.
8
+
9
+ ## Install
10
+
11
+ ```
12
+ gem install gh-summary
13
+ ```
14
+
15
+ ## Requirements
16
+
17
+ - Ruby >= 3.0
18
+ - [gh](https://cli.github.com/), authenticated
19
+
20
+ ## Usage
21
+
22
+ ```
23
+ gh-summary
24
+ gh-summary --short
25
+ ```
26
+
27
+ `--short` skips the recent activity section.
28
+
29
+ ## Output
30
+
31
+ ```
32
+ GitHub Summary 2026-04-13 14:54
33
+ ────────────────────────────────────────────────────────────
34
+ PRs awaiting your review
35
+ torvalds/linux — Fix memory leak in page allocator
36
+ https://github.com/torvalds/linux/pull/842
37
+ rails/rails — ActiveRecord: add async batch find
38
+ https://github.com/rails/rails/pull/51203
39
+ (2 pending)
40
+ ────────────────────────────────────────────────────────────
41
+ Your open PRs
42
+ matz/ruby — Optimize frozen string dedup in parser
43
+ https://github.com/matz/ruby/pull/9104
44
+ sinatra/sinatra — Add streaming response helpers
45
+ https://github.com/sinatra/sinatra/pull/1877
46
+ rack/rack — Fix multipart boundary parsing edge case
47
+ https://github.com/rack/rack/pull/2190
48
+ (3 open)
49
+ ────────────────────────────────────────────────────────────
50
+ Issues assigned to you
51
+ homebrew/brew — Formula audit fails on Apple Silicon
52
+ https://github.com/homebrew/brew/issues/17450
53
+ (1 open)
54
+ ────────────────────────────────────────────────────────────
55
+ Unread notifications
56
+ rails/rails (3):
57
+ [Issue] ActiveStorage mirror sync regression (mention)
58
+ [PullRequest] Fix connection pool exhaustion (review_requested)
59
+ [Release] v8.1.0 (subscribed)
60
+ ruby/ruby (1):
61
+ [Issue] YJIT segfault on ARM64 (assign)
62
+ (4 total unread)
63
+ ────────────────────────────────────────────────────────────
64
+ Recent activity (your repos)
65
+ Repo Type Action Date
66
+ matz/ruby PullRequest opened 2026-04-12
67
+ sinatra/sinatra Issues closed 2026-04-11
68
+ rack/rack PullRequest merged 2026-04-10
69
+ ────────────────────────────────────────────────────────────
70
+ ```
71
+
72
+ ## Tests
73
+
74
+ ```
75
+ make test
76
+ ```
77
+
78
+ ## Structure
79
+
80
+ ```
81
+ .
82
+ ├── bin
83
+ │ └── gh-summary
84
+ ├── lib
85
+ │ ├── gh_summary
86
+ │ │ ├── auth_error.rb
87
+ │ │ ├── cli.rb
88
+ │ │ ├── colors.rb
89
+ │ │ ├── github.rb
90
+ │ │ └── runner.rb
91
+ │ └── gh_summary.rb
92
+ ├── Makefile
93
+ ├── README.md
94
+ └── test
95
+ ├── test_auth_error.rb
96
+ ├── test_cli.rb
97
+ ├── test_colors.rb
98
+ ├── test_github.rb
99
+ ├── test_helper.rb
100
+ └── test_runner.rb
101
+ ```
data/bin/gh-summary ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+
5
+ require "gh_summary"
6
+
7
+ begin
8
+ GhSummary::Runner.new(short: ARGV.include?("--short")).run
9
+ rescue GhSummary::AuthError => error
10
+ GhSummary::CLI.new.error("Error: #{error.message}")
11
+ exit 1
12
+ end
@@ -0,0 +1,3 @@
1
+ module GhSummary
2
+ class AuthError < StandardError; end
3
+ end
@@ -0,0 +1,79 @@
1
+ module GhSummary
2
+ class CLI
3
+ include Colors
4
+
5
+ INDENT_SECTION = 2
6
+ INDENT_ITEM = 4
7
+ INDENT_SUB = 6
8
+
9
+ def initialize
10
+ end
11
+
12
+ def divider
13
+ output "#{DIM}#{"─" * 60}#{RESET}"
14
+ end
15
+
16
+ def header(color, text)
17
+ divider
18
+ output "#{BOLD}#{color}#{left_padding(text, INDENT_SECTION)}#{RESET}"
19
+ end
20
+
21
+ def item(repository, title, url)
22
+ output left_padding("#{repository} — #{title}", INDENT_ITEM)
23
+ output left_padding(url, INDENT_SUB)
24
+ end
25
+
26
+ def sub_item(text)
27
+ output left_padding(text, INDENT_SUB)
28
+ end
29
+
30
+ def group(label)
31
+ output left_padding(label, INDENT_ITEM)
32
+ end
33
+
34
+ def count(number, label)
35
+ output "#{DIM}#{left_padding("(#{number} #{label})", INDENT_SECTION)}#{RESET}"
36
+ end
37
+
38
+ def empty(text)
39
+ output "#{DIM}#{left_padding(text, INDENT_SECTION)}#{RESET}"
40
+ end
41
+
42
+ def success(text)
43
+ output "#{GREEN}#{left_padding(text, INDENT_SECTION)}#{RESET}"
44
+ end
45
+
46
+ def error(text)
47
+ $stderr.puts "#{RED}#{left_padding(text, INDENT_SECTION)}#{RESET}"
48
+ end
49
+
50
+ def banner
51
+ output "\n#{BOLD}#{CYAN}#{left_padding("GitHub Summary", INDENT_SECTION)}#{RESET} #{DIM}#{Time.now.strftime("%Y-%m-%d %H:%M")}#{RESET}"
52
+ end
53
+
54
+ def table(columns, rows)
55
+ column_widths = columns.map(&:length)
56
+ rows.each do |row|
57
+ row.each_with_index { |cell, index| column_widths[index] = [column_widths[index] || 0, cell.length].max }
58
+ end
59
+
60
+ format_string = column_widths.map { |width| "%-#{width}s" }.join(" ")
61
+ output "#{DIM}#{left_padding(sprintf(format_string, *columns), INDENT_SECTION)}#{RESET}"
62
+ rows.each { |row| output left_padding(sprintf(format_string, *row), INDENT_SECTION) }
63
+ end
64
+
65
+ def newline
66
+ output
67
+ end
68
+
69
+ private
70
+
71
+ def output(text = "")
72
+ puts text
73
+ end
74
+
75
+ def left_padding(text, spacing = INDENT_SECTION)
76
+ "#{" " * spacing}#{text}"
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,12 @@
1
+ module GhSummary
2
+ module Colors
3
+ BOLD = "\033[1m"
4
+ DIM = "\033[2m"
5
+ CYAN = "\033[36m"
6
+ YELLOW = "\033[33m"
7
+ GREEN = "\033[32m"
8
+ RED = "\033[31m"
9
+ MAGENTA = "\033[35m"
10
+ RESET = "\033[0m"
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ require "json"
2
+ require "open3"
3
+
4
+ module GhSummary
5
+ class GitHub
6
+ def initialize
7
+ unless system("gh", "auth", "status", out: File::NULL, err: File::NULL)
8
+ raise AuthError, "gh is not authenticated. Run 'gh auth login' first."
9
+ end
10
+ end
11
+
12
+ def search_prs(**options)
13
+ flags = options.map { |key, value| "--#{key}=#{value}" }
14
+ fetch_json("search", "prs", *flags, "--json", "title,repository,url,updatedAt", "--limit", "15")
15
+ end
16
+
17
+ def search_issues(**options)
18
+ flags = options.map { |key, value| "--#{key}=#{value}" }
19
+ fetch_json("search", "issues", *flags, "--json", "title,repository,url", "--limit", "15")
20
+ end
21
+
22
+ def notifications
23
+ fetch_json("api", "notifications")
24
+ end
25
+
26
+ def events(per_page: 20)
27
+ fetch_json("api", "events", "-f", "per_page=#{per_page}")
28
+ end
29
+
30
+ private
31
+
32
+ def execute(*arguments)
33
+ output, _stderr, status = Open3.capture3("gh", *arguments)
34
+ status.success? ? output : nil
35
+ end
36
+
37
+ def fetch_json(*arguments)
38
+ output = execute(*arguments)
39
+ return [] unless output
40
+ parsed = JSON.parse(output)
41
+ parsed.is_a?(Array) ? parsed : [parsed]
42
+ rescue JSON::ParserError
43
+ []
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,89 @@
1
+ module GhSummary
2
+ class Runner
3
+ include Colors
4
+
5
+ RELEVANT_EVENTS = %w[IssuesEvent PullRequestEvent ReleaseEvent ForkEvent WatchEvent].freeze
6
+
7
+ def initialize(short: false)
8
+ @short_mode = short
9
+ @cli = CLI.new
10
+ @github = GitHub.new
11
+ end
12
+
13
+ def run
14
+ @cli.banner
15
+ show_section(
16
+ YELLOW, "PRs awaiting your review",
17
+ @github.search_prs("review-requested": "@me", state: "open"),
18
+ empty: "None — you're clear!", empty_style: :success, count_label: "pending"
19
+ )
20
+ show_section(
21
+ GREEN, "Your open PRs",
22
+ @github.search_prs(author: "@me", state: "open"),
23
+ empty: "No open PRs", count_label: "open"
24
+ )
25
+ show_section(
26
+ RED, "Issues assigned to you",
27
+ @github.search_issues(assignee: "@me", state: "open"),
28
+ empty: "No assigned issues", count_label: "open"
29
+ )
30
+ show_notifications
31
+ show_recent_activity unless @short_mode
32
+ @cli.divider
33
+ @cli.newline
34
+ end
35
+
36
+ private
37
+
38
+ def repository_name(item)
39
+ item.dig("repository", "nameWithOwner") || item.dig("repository", "name") || "?"
40
+ end
41
+
42
+ def show_section(color, title, items, empty:, count_label:, empty_style: :empty)
43
+ @cli.header(color, title)
44
+ if items.any?
45
+ items.each { |item| @cli.item(repository_name(item), item["title"], item["url"]) }
46
+ @cli.count(items.size, count_label)
47
+ else
48
+ @cli.send(empty_style, empty)
49
+ end
50
+ end
51
+
52
+ def show_notifications
53
+ @cli.header(MAGENTA, "Unread notifications")
54
+ notifications = @github.notifications
55
+ if notifications.any?
56
+ grouped_notifications = notifications.group_by { |notification| notification.dig("repository", "full_name") || "unknown" }
57
+ grouped_notifications.each do |repository, notification_items|
58
+ @cli.group("#{repository} (#{notification_items.size}):")
59
+ notification_items.first(5).each do |notification|
60
+ subject_type = notification.dig("subject", "type") || "?"
61
+ subject_title = notification.dig("subject", "title") || "?"
62
+ reason = notification["reason"] || "?"
63
+ @cli.sub_item("[#{subject_type}] #{subject_title} (#{reason})")
64
+ end
65
+ end
66
+ @cli.count(notifications.size, "total unread")
67
+ else
68
+ @cli.success("All caught up!")
69
+ end
70
+ end
71
+
72
+ def show_recent_activity
73
+ @cli.header(CYAN, "Recent activity (your repos)")
74
+ all_events = @github.events(per_page: 20)
75
+ relevant_events = all_events.select { |event| RELEVANT_EVENTS.include?(event["type"]) }.first(10)
76
+ if relevant_events.any?
77
+ rows = relevant_events.map do |event|
78
+ [event.dig("repo", "name") || "?",
79
+ (event["type"] || "").sub("Event", ""),
80
+ event.dig("payload", "action") || "—",
81
+ (event["created_at"] || "").split("T").first]
82
+ end
83
+ @cli.table(%w[Repo Type Action Date], rows)
84
+ else
85
+ @cli.empty("No recent events")
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module GhSummary
2
+ VERSION = "0.1.0"
3
+ end
data/lib/gh_summary.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative "gh_summary/version"
2
+ require_relative "gh_summary/auth_error"
3
+ require_relative "gh_summary/colors"
4
+ require_relative "gh_summary/cli"
5
+ require_relative "gh_summary/github"
6
+ require_relative "gh_summary/runner"
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gh-summary
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nikita Miloserdov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Shows open PRs, review requests, assigned issues, unread notifications,
13
+ and recent activity. Wraps the gh CLI. No dependencies.
14
+ email:
15
+ - nmiloserdov@proton.me
16
+ executables:
17
+ - gh-summary
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - bin/gh-summary
24
+ - lib/gh_summary.rb
25
+ - lib/gh_summary/auth_error.rb
26
+ - lib/gh_summary/cli.rb
27
+ - lib/gh_summary/colors.rb
28
+ - lib/gh_summary/github.rb
29
+ - lib/gh_summary/runner.rb
30
+ - lib/gh_summary/version.rb
31
+ homepage: https://github.com/wh1le/gh-summary
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ homepage_uri: https://github.com/wh1le/gh-summary
36
+ source_code_uri: https://github.com/wh1le/gh-summary
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.7.2
52
+ specification_version: 4
53
+ summary: GitHub activity digest in your terminal
54
+ test_files: []