@d3lm/pr-stats 0.2.12 → 0.2.14

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -41
  3. package/dist/tui-app.mjs +2511 -1943
  4. package/package.json +1 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Elm
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.
package/README.md CHANGED
@@ -1,15 +1,13 @@
1
1
  # pr-stats
2
2
 
3
- An interactive terminal UI, built with [OpenTUI](https://github.com/anomalyco/opentui) and React, that shows statistics for a GitHub user, e.g., time to review, size of authored PRs, or comments received on them. It works with any repository your GitHub login can see. It has a queue tab for the open PRs on your reviewing plate, split into those awaiting your review and those you already reviewed, a Your PRs tab that splits into your open PRs and a merged-and-closed report, chart tabs for the review-time, size, and comments reports, and a live options modal.
3
+ An interactive terminal UI for GitHub PR statistics, built with [OpenTUI](https://github.com/anomalyco/opentui) and React. It shows time to review, PR size, and comment stats for any repository your GitHub login can see, plus a live queue of the PRs waiting on you.
4
4
 
5
5
  ![pr-stats](./assets/screenshot.png)
6
6
 
7
7
  ## Requirements
8
8
 
9
- The TUI renders through native FFI, so it needs either Node 26.4 or newer, or [Bun](https://bun.sh) 1.3 or newer. You also need one of two ways to authenticate:
10
-
11
- - An authenticated gh CLI. Run `gh auth login` if you have not set that up yet.
12
- - A GitHub access token, passed through `--token` or the `GITHUB_TOKEN` or `GH_TOKEN` environment variable. When a token is present, the tool calls the GitHub API directly and does not need the gh CLI at all. The token needs the `repo` scope to see private repositories.
9
+ - Node 26.4+ or Bun 1.3+. The TUI renders through native FFI. Node gates that behind `--experimental-ffi`, and the launcher re-executes itself with the flag, so a plain `pr-stats` works on both runtimes.
10
+ - Either an authenticated gh CLI (`gh auth login`) or a GitHub access token via `--token`, `GITHUB_TOKEN`, or `GH_TOKEN`. With a token, the tool calls the GitHub API directly and does not need the gh CLI. See [Tokens](#tokens) for the access a token needs.
13
11
 
14
12
  ## Install
15
13
 
@@ -17,7 +15,7 @@ The TUI renders through native FFI, so it needs either Node 26.4 or newer, or [B
17
15
  npm install -g @d3lm/pr-stats
18
16
  ```
19
17
 
20
- You can also run it without installing through `npx @d3lm/pr-stats`. The bunx equivalent is `bunx --bun @d3lm/pr-stats`, where `--bun` runs the TUI on Bun directly instead of going through Node.
18
+ You can also run it without installing through `npx @d3lm/pr-stats` or `bunx --bun @d3lm/pr-stats`.
21
19
 
22
20
  ## Usage
23
21
 
@@ -25,19 +23,41 @@ You can also run it without installing through `npx @d3lm/pr-stats`. The bunx eq
25
23
  pr-stats
26
24
  ```
27
25
 
28
- Without flags, it looks at PRs from the last 90 days across all repositories you can access. The tabs hold these views.
29
-
30
- - The queue tab, which it opens on, shows two lists. The open PRs awaiting your review come first with how long each has been waiting, and below them sit the open PRs you already reviewed or commented on with how long ago that was, so a PR stays visible until it merges or closes. A fresh review request moves a PR from the reviewing list back into the awaiting one. The `--review-types` flag narrows what counts as a review to a subset of `approve`, `comment`, and `request-changes`, so with `--review-types approve,request-changes` a comment alone no longer answers a request and the PR stays in the awaiting list.
31
- - The Your PRs tab has two sub-tabs, which the `t` key switches. The first lists your own authored PRs that are still open with their age and size. The second reports how your authored PRs got created, merged, and closed, telling a merge apart from a close without one. It charts time-to-merge percentiles, a histogram and trend, a merge-time heatmap, and a scatter of merge time against PR size. It also measures how long your PRs wait for their first review from someone else, with a histogram and trend of the time from creation to that review and a histogram of how long the open PRs still without one have waited. It also plots a merge-rate trend over the concluded PRs, cumulative created and merged lines whose gap shows the backlog, weekly created and merged volumes, an outcome gauge, and the most recently merged and closed PRs. A reviewer leaderboard ranks who reviews your PRs by distinct PRs reviewed, and a review-coverage gauge counts the merged PRs that never received a review. Your own replies to review threads never count as a review for any of them.
32
- - The time-to-review report pairs its histogram, trend, heatmap, and weekly volume with a scatter of review time against PR size, the completed review cycles per PR, and a verdict gauge splitting approvals from change requests. It also shows the age of the requests still waiting on you, how old PRs already were when the request reached you, and an off-hours gauge that splits weekdays into work hours and after hours once `--work-hours` is set. On the aggregate view it additionally compares median review times by repo.
33
- - The PR size report carries the same histogram, trend, heatmap, and weekly volume for PR sizes and adds a net-lines trend that sums additions minus deletions per week.
34
- - The comments report holds a histogram of comments per PR, a scatter of comments against PR size, and the most commented PRs.
35
-
36
- When the data spans multiple repos, every tab opens on a repo picker that drills into one repo or the aggregate across all of them, and on the two queue lists the `g` key groups the aggregate list by repo. Comparison cards like the reviewer leaderboard cap themselves at eight rows and fold the rest into an overflow line, and the `x` key lifts the cap on the open stats tab and restores it. The footer names the key whenever the tab has a capped card.
37
-
38
- Node gates the FFI that OpenTUI renders through behind the `--experimental-ffi` flag, and the launcher re-executes itself with that flag when it is missing, so a plain `pr-stats` works without extra flags on both runtimes.
39
-
40
- All flags pre-seed the options modal:
26
+ Without flags, it covers PRs from the last 90 days across all repositories you can access.
27
+
28
+ - **Queue** lists the open PRs awaiting your review with their wait time, and below them the open PRs you already reviewed. A fresh review request moves a PR back into the awaiting list.
29
+ - **Your PRs** splits into your open PRs and a merged-and-closed report with merge-time, first-review, backlog, and outcome charts plus a reviewer leaderboard. The `t` key switches the sub-tabs.
30
+ - **Time to review** charts your review times as a histogram, trend, heatmap, and weekly volume, plus review cycles, verdicts, an off-hours gauge, and the requests still waiting on you.
31
+ - **PR size** carries the same charts for PR sizes and adds a weekly net-lines trend.
32
+ - **Comments** holds a histogram of comments per PR, a scatter against PR size, and the most commented PRs.
33
+
34
+ When the data spans multiple repos, every tab opens on a repo picker that drills into one repo or the aggregate. On the queue lists, `g` groups the aggregate by repo, and on stats tabs, `x` lifts the row cap on comparison cards.
35
+
36
+ ## Options
37
+
38
+ All flags pre-seed the options modal. Run `pr-stats --help` for full details.
39
+
40
+ | Flag | Default | Description |
41
+ | ----------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
42
+ | `-s, --since <value>` | `90d` | Only include PRs created after this point. Accepts an ISO date or a relative value like `30d`, `8w`, `6m`, `1y`. |
43
+ | `-r, --repo <name>` | all repos | Restrict the search to a repository, repeatable. Accepts `owner/name` or a bare name. |
44
+ | `-u, --user <login>` | you | Compute stats for this user instead of the authenticated one. |
45
+ | `--token <token>` | | Authenticate with a GitHub access token instead of the gh CLI. |
46
+ | `-t, --target <value>` | | Report how many reviews finished within this time, like `24h`, `2d`, or `90m`. |
47
+ | `--target-percentile <value>` | `90` | Check the target against this percentile of your review times, like `90` or `p90`. |
48
+ | `--size-target <value>` | | Report how many authored PRs fit within this size, like `400`, `400l`, `20f`, or `400l,20f`. |
49
+ | `--tz <zone>` | system | Set the IANA timezone for the weekend and working-hours math. |
50
+ | `--work-days <days>` | `mon-fri` | Count only these days as working days, like `mon-fri`, `sun-thu`, or `mon,wed,fri`. |
51
+ | `-w, --work-hours <value>` | `0-24` | Count only these working hours, like `9-17`, `8:30-16:30`, or `9am-6pm`. |
52
+ | `--wall-clock` | off | Measure raw elapsed time, including weekends. |
53
+ | `--include-drafts` | off | Include PRs that are currently drafts. |
54
+ | `--review-types <list>` | all | Count only these review types. Takes a comma-separated list of `approve`, `comment`, and `request-changes`. |
55
+ | `--no-cache` | off | Refetch every PR instead of reading the disk cache. |
56
+ | `--json` | off | Print every stat as JSON to stdout instead of starting the TUI. |
57
+ | `--debug <path>` | | Serve canned data from a fake `gh` binary in a testdata directory. |
58
+ | `-h, --help` | | Show the help page. |
59
+
60
+ A few examples:
41
61
 
42
62
  ```bash
43
63
  # Limit the search to one repository and a start date
@@ -46,49 +66,74 @@ pr-stats --repo owner/name --since 2026-01-01
46
66
  # Report how many reviews finished within one working day
47
67
  pr-stats --work-hours 9-17 --target 1d
48
68
 
49
- # Report how many authored PRs stayed under 400 changed lines
50
- pr-stats --size-target 400
51
-
52
69
  # Count only approvals and change requests as reviews
53
70
  pr-stats --review-types approve,request-changes
54
-
55
- # Authenticate with an access token instead of the gh CLI
56
- pr-stats --token your-access-token
57
71
  ```
58
72
 
59
- Run `pr-stats --help` for the full list of options.
73
+ ## Tokens
74
+
75
+ The `--token` flag and the `GITHUB_TOKEN` and `GH_TOKEN` environment variables switch the tool from the gh CLI to direct GitHub API calls. The PR list comes from the GitHub search API, which silently leaves out every repository the token cannot see, so a token with too little access shows an empty view instead of an error.
76
+
77
+ A classic token needs the `repo` scope, or `public_repo` when every repository is public.
78
+
79
+ A fine-grained token needs more care, because it only covers one resource owner and only the repositories you select for it.
80
+
81
+ - Pick the organization as the resource owner when the PRs live in an organization's repositories. A token owned by your user account never sees them.
82
+ - Grant access to all repositories or select the ones you care about. The public repositories option hides every private one.
83
+ - Grant read access to `Metadata`, `Pull requests`, and `Contents`. Searching pull requests in private repositories needs `Contents` on top of `Pull requests`, even though GitHub's permission tables do not mention it.
84
+ - Ask an organization owner to approve the token when the organization requires approval for fine-grained tokens. Until then, the token sees nothing in that organization.
85
+
86
+ To check a token, run `pr-stats --token <token> --repo owner/name` against a repository you expect to see. Naming the repository makes the search fail with a `422 Validation Failed` error when the token cannot see it, where a search without `--repo` would just return nothing.
60
87
 
61
88
  ## JSON export
62
89
 
63
- The `--json` flag prints every stat as JSON to stdout instead of starting the TUI, so the output pipes into jq or lands in a file through a redirect. Every other flag applies to the report the same way it seeds the TUI, and load progress renders on stderr, so a piped stdout stays pure JSON.
90
+ The `--json` flag prints the full report to stdout instead of starting the TUI. Every other flag applies the same way, and progress renders on stderr, so a piped stdout stays pure JSON.
64
91
 
65
92
  ```bash
66
93
  pr-stats --json | jq '.review.reviewTimeHours'
67
94
  ```
68
95
 
69
- The report carries the same data the tabs derive. The `review` object holds the counts, the review-time summary, the verdicts, the per-repo medians, the optional target gauge, and one entry per completed, pending, and reviewing cycle. The `authored` object holds the outcome counts, the size, merge-time, and first-review summaries, the optional size-target gauge, the reviewer leaderboard with the review coverage, and one entry per authored PR with its size, comment counts, reviewers, and merge, close, and first-review times. The `comments` object summarizes the comments per PR. Summaries report the count, mean, p50, p90, min, and max, and every duration respects the configured time mode, so a working-hours setup reports counted hours instead of wall-clock hours.
70
-
71
- The settings dialog has an export row that writes the same report to `pr-stats.json` in the directory pr-stats was started from, built from the data currently on screen and the live options.
96
+ The report holds a `review`, `authored`, and `comments` object with one entry per PR. Summaries report the count, mean, p50, p90, min, and max, and every duration respects the configured time mode. The settings dialog has an export row that writes the same report to `pr-stats.json` in the current directory.
72
97
 
73
98
  ## Caching
74
99
 
75
- The expensive part of a run is fetching the per-PR review timelines and the size and comment counters, so those get cached on disk per PR. Only closed and merged PRs are cached, because their timelines and sizes no longer change. The searches and every open PR are fetched fresh on each run, which keeps the results correct while skipping most of the API calls after the first run.
100
+ Closed and merged PRs are cached on disk per PR, because their timelines and sizes no longer change. Searches and open PRs are fetched fresh on each run. The TUI also snapshots the last successful load, so a later start with the same options renders instantly while the real load refreshes in the background.
76
101
 
77
- The login of the authenticated user is cached for a day as well, which skips one round trip per run. A configured `--user` never touches that cache. The TUI additionally snapshots the last successful load, and a later start with the same data options renders the snapshot instantly while the real load refreshes it in the background. A narrower `--since` window also hits the snapshot, because the subset gets cut from it by PR creation date.
102
+ The cache lives in `~/Library/Caches/pr-stats` on macOS and in `$XDG_CACHE_HOME/pr-stats` or `~/.cache/pr-stats` elsewhere. The `PR_STATS_CACHE_DIR` environment variable overrides the location. `--no-cache` skips every cache read for one run, and the settings dialog can disable the cache permanently.
78
103
 
79
- The cache lives in `~/Library/Caches/pr-stats` on macOS and in `$XDG_CACHE_HOME/pr-stats` or `~/.cache/pr-stats` elsewhere. The `PR_STATS_CACHE_DIR` environment variable overrides the location. Pass `--no-cache` to skip every cache read, which refetches everything, including the login, and rewrites the cache with fresh data. Runs with `--debug` never read or write the cache, so canned test data stays out of it.
104
+ The options modal saves the current options to the cache directory with the `s` key. Later runs start from the saved options wherever no flag was given, and flags always take precedence.
80
105
 
81
- The options modal can also save the current options to the cache directory with the `s` key. Later runs start from the saved options wherever no flag was given, and flags on the command line always take precedence. The modal labels whether the current options match the saved ones, and clearing the cache from the settings dialog keeps the saved options.
106
+ ## Auto Reload
82
107
 
83
- The settings dialog can also disable the cache permanently. The toggle persists to `settings.json` in the cache directory, so every later run refetches everything the way `--no-cache` does. An explicit `--no-cache` flag always wins over the saved setting. The same dialog switches the color theme, described under Theming below. The dialog can also reset the settings, which deletes `settings.json` together with a saved theme, so future runs start from the defaults.
108
+ The `r` key reloads the data by hand, and `R` refetches everything past the cache. The TUI can also keep itself fresh while it stays open. The Auto reload row in the settings dialog turns background reloads on, and the Reload interval row below it sets how long the TUI waits after one load finishes before the next one starts, as a value in seconds, minutes, or hours like `30s`, `10m`, or `2h`. The header shows the interval next to the refresh time while it is on. Both settings persist in `settings.json` in the cache directory.
84
109
 
85
- ## Theming
110
+ ```json
111
+ {
112
+ "autoReload": true,
113
+ "reloadInterval": "10m"
114
+ }
115
+ ```
116
+
117
+ ## Notifications
86
118
 
87
- The TUI ships five built-in themes, the warm amber default plus green, blue, purple, and yellow variants. Each variant recolors the accent and chart colors at the same saturation and lightness as the default, so every theme keeps the same contrast, while the neutral text and background grays stay shared. The Theme row in the settings dialog cycles through them with the arrow keys and applies the choice immediately.
119
+ The Desktop notifications row in the settings dialog makes the TUI send a desktop notification whenever a load finds a PR newly awaiting your review or a review re-requested from you after you already reviewed the PR. Every load diffs its review requests against the load before it, so the first load of a session only records what is already waiting and never notifies about it. Manual reloads count as well, but the feature pairs naturally with auto reload, which lets the TUI watch your review queue from a spare terminal. The setting persists in `settings.json` in the cache directory.
88
120
 
89
- The Edit colors row below it opens a list of every theme color with its current hex value. Enter edits the selected color in place, and a committed value applies immediately. The first edit creates a custom theme that starts from the built-in theme on screen, and the Theme row switches to `custom`. Further edits refine the custom theme, and an empty value returns a color to the built-in the custom theme started from. Clearing the last custom color dissolves the custom theme back into that built-in.
121
+ ```json
122
+ {
123
+ "notifications": true,
124
+ "notifyChannel": "auto"
125
+ }
126
+ ```
90
127
 
91
- The custom theme joins the Theme row cycle as a sixth entry and persists across switches, so picking a built-in theme renders it pure while the custom colors stay saved, and cycling back to `custom` restores them. Everything lands in a `theme` object in `settings.json` in the cache directory, which you can also edit by hand:
128
+ Notifications go through the terminal itself when it supports a notification escape sequence, which covers iTerm2, Kitty, Ghostty, WezTerm, and most VTE-based terminals like GNOME Terminal. The terminal posts the notification under its own notification permission and the sequence travels through SSH, so this path needs no setup. Some terminals only show the banner while their window is unfocused. Terminals without such support get the platform's own command instead, `osascript` on macOS and `notify-send` on Linux. A notification that fails to send reports in the footer, for example when `notify-send` is missing on a Linux machine.
129
+
130
+ The "Notification channel" row picks the path, and the choice persists as `notifyChannel` with the values `auto`, `terminal`, and `command`. On `auto` the TUI tries the terminal first and falls back to the platform command, while the other two force one path. The dialog shows the `command` value as the command's name on this platform. Forcing the command pays off inside editor terminals like the one in VS Code, which render the terminal path as a small in-editor toast instead of a system notification. The "Send test notification" row below it names the channel the next send takes and sends a sample notification, so you can check that your desktop displays it before relying on it.
131
+
132
+ The macOS fallback posts through the built-in Script Editor, and since macOS 15 those notifications stay invisible until Script Editor holds notification permission, without any prompt appearing. To grant it once, open the Script Editor app, run the one-line script `display notification "test"`, and allow the prompt that appears. Script Editor then shows up under Notifications in the System Settings like any other app.
133
+
134
+ ## Theming
135
+
136
+ The TUI ships five built-in themes, the warm amber default plus green, blue, purple, and yellow variants. The Theme row in the settings dialog cycles through them, and the Edit colors row below it edits individual colors, which creates a custom theme that joins the cycle as a sixth entry. Everything persists in a `theme` object in `settings.json` in the cache directory, which you can also edit by hand.
92
137
 
93
138
  ```json
94
139
  {
@@ -101,8 +146,12 @@ The custom theme joins the Theme row cycle as a sixth entry and persists across
101
146
  }
102
147
  ```
103
148
 
104
- The `preset` key names the active theme, one of `default`, `green`, `blue`, `purple`, `yellow`, and `custom`. The `base` key names the built-in theme the custom colors start from and defaults to `default`, and every other key sets one custom color on top of that base. The color keys are `bg`, `border`, `text`, `muted`, `dim`, `accent`, `selectedBg`, `inputBg`, `inputFocusedBg`, `warn`, `error`, `success`, `chartBar`, `chartLine`, `chartDim`, and `heat`. The `heat` key colors the weekly heatmap and takes four colors from cool to hot. A file that sets colors without a `preset` activates the custom theme they define, and a `custom` preset without any colors falls back to its `base` theme. A typo in a key or a color fails the start with a message naming the mistake, so a broken theme never renders.
149
+ The `preset` key names the active theme, one of `default`, `green`, `blue`, `purple`, `yellow`, and `custom`. The `base` key names the built-in theme the custom colors start from. The color keys are `bg`, `border`, `text`, `muted`, `dim`, `accent`, `selectedBg`, `inputBg`, `inputFocusedBg`, `warn`, `error`, `success`, `chartBar`, `chartLine`, `chartDim`, and `heat`, where `heat` takes four heatmap colors from cool to hot. An invalid key or color fails the start with a message naming the mistake.
105
150
 
106
151
  ## Development
107
152
 
108
- The TUI runs with `bun tui/main.tsx`. To try it under Node instead, run `pnpm tui:node`, which rebuilds the bundles and starts the built launcher from `dist/`. The entry point accepts `--debug <path>` with a path to a testdata directory, for example `bun tui/main.tsx --debug tui/testdata`, which serves canned data from the fake `gh` binary in that directory instead of fetching from GitHub. The TUI has a test that drives the whole pipeline against the same fake binary, which `bun test tui` runs. `pnpm build` bundles the binary into `dist/`, where `tui.mjs` is the runtime launcher and `tui-app.mjs` is the app bundle it loads.
153
+ The TUI runs with `bun tui/main.tsx`, or under Node with `pnpm tui:node`. The `--debug tui/testdata` flag serves canned data from the fake `gh` binary in that directory instead of fetching from GitHub, and `bun test tui` drives the whole pipeline against it. `pnpm build` bundles everything into `dist/`, where `tui.mjs` is the launcher and `tui-app.mjs` is the app bundle it loads.
154
+
155
+ ## License
156
+
157
+ OpenTUI is licensed under the [MIT License](LICENSE).