@d3lm/pr-stats 0.2.22 → 0.2.24

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.
package/README.md CHANGED
@@ -25,8 +25,8 @@ pr-stats
25
25
 
26
26
  Without flags, it covers PRs from the last 90 days across all repositories you can access.
27
27
 
28
- - **Awaiting you** splits into three sub-tabs, which the `t` key cycles. Awaiting review lists the open PRs awaiting your review with their wait time, Reviewed lists the open PRs you already reviewed, and Mentions is an inbox of the PRs that @-mention you. A fresh review request moves a PR back into the awaiting list. The `s` key snoozes a PR or a mention you are not ready for yet, which parks it in a snoozed list until a time you pick, and `d` marks a mention read. See [Mentions](#mentions) and [Snoozing](#snoozing).
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.
28
+ - **Awaiting you** splits into three sub-tabs, which the `t` key cycles forward and `T` (shift+t) cycles backward. Awaiting review lists the open PRs awaiting your review with their wait time, Reviewed lists the open PRs you already reviewed, and Mentions is an inbox of the PRs that @-mention you. A fresh review request moves a PR back into the awaiting list. The `s` key snoozes a PR or a mention you are not ready for yet, which parks it in a snoozed list until a time you pick, and `d` marks a mention read. See [Mentions](#mentions) and [Snoozing](#snoozing).
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` and `T` keys switch the sub-tabs.
30
30
  - **Reviews** counts the PRs you reviewed on request next to the review rounds they took, and 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
31
  - **PR size** carries the same charts for PR sizes and adds a weekly net-lines trend.
32
32
  - **Comments** holds a histogram of comments per PR, a scatter against PR size, and the most commented PRs.
@@ -99,7 +99,7 @@ The report holds a `review`, `authored`, and `comments` object with one entry pe
99
99
 
100
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.
101
101
 
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.
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. The Clear cache row in the settings dialog shows the path next to the size of the directory on disk, and deletes the cached PR data while keeping the saved options and settings.
103
103
 
104
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.
105
105
 
@@ -19,9 +19,9 @@
19
19
  <key>CFBundlePackageType</key>
20
20
  <string>APPL</string>
21
21
  <key>CFBundleShortVersionString</key>
22
- <string>0.2.22</string>
22
+ <string>0.2.24</string>
23
23
  <key>CFBundleVersion</key>
24
- <string>0.2.22</string>
24
+ <string>0.2.24</string>
25
25
  <key>LSMinimumSystemVersion</key>
26
26
  <string>13.0</string>
27
27
  <key>LSUIElement</key>
package/dist/tui-app.mjs CHANGED
@@ -143,7 +143,7 @@ import { readFileSync as readFileSync3 } from "node:fs";
143
143
  import { join as join3 } from "node:path";
144
144
 
145
145
  // src/cache.ts
146
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
146
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
147
147
  import { homedir } from "node:os";
148
148
  import { dirname, join } from "node:path";
149
149
  var VERSION = 5;
@@ -163,6 +163,24 @@ function cacheDir() {
163
163
  }
164
164
  return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "pr-stats");
165
165
  }
166
+ function cacheSize() {
167
+ const dir = cacheDir();
168
+ let total = 0;
169
+ try {
170
+ const entries = readdirSync(dir, { withFileTypes: true });
171
+ for (const entry of entries) {
172
+ if (!entry.isFile()) {
173
+ continue;
174
+ }
175
+ try {
176
+ total += statSync(join(dir, entry.name)).size;
177
+ } catch {
178
+ }
179
+ }
180
+ } catch {
181
+ }
182
+ return total;
183
+ }
166
184
  function prKey(repo, number) {
167
185
  return `${repo}#${number}`;
168
186
  }
@@ -277,6 +295,19 @@ function fail(message) {
277
295
  function formatMinutesOfDay(minutes) {
278
296
  return `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, "0")}`;
279
297
  }
298
+ function formatBytes(bytes) {
299
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
300
+ let value2 = bytes;
301
+ let unit = 0;
302
+ while (value2 >= 1023.5 && unit < units.length - 1) {
303
+ value2 /= 1024;
304
+ unit += 1;
305
+ }
306
+ if (unit === 0) {
307
+ return `${value2} ${units[unit]}`;
308
+ }
309
+ return `${value2 < 10 ? value2.toFixed(1) : Math.round(value2)} ${units[unit]}`;
310
+ }
280
311
  function percentile(sorted, percent) {
281
312
  const index = Math.min(sorted.length - 1, Math.ceil(percent / 100 * sorted.length) - 1);
282
313
  return sorted[Math.max(0, index)];
@@ -757,9 +788,9 @@ var AUTHORED_SUB_TABS = [
757
788
  { key: "open", label: "Open" },
758
789
  { key: "merged", label: "Merged & closed" }
759
790
  ];
760
- function nextPendingSubTab(current2) {
791
+ function cycledPendingSubTab(current2, delta) {
761
792
  const index = PENDING_SUB_TABS.findIndex((entry) => entry.key === current2);
762
- return PENDING_SUB_TABS[(index + 1) % PENDING_SUB_TABS.length];
793
+ return PENDING_SUB_TABS[(index + PENDING_SUB_TABS.length + delta) % PENDING_SUB_TABS.length];
763
794
  }
764
795
  var initialBrowseState = {
765
796
  tab: 0,
@@ -806,9 +837,9 @@ function browseReducer(state, action) {
806
837
  case "tabCycled": {
807
838
  return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
808
839
  }
809
- case "subTabToggled": {
840
+ case "subTabCycled": {
810
841
  if (state.tab === 0) {
811
- return { ...state, pendingTab: nextPendingSubTab(state.pendingTab).key };
842
+ return { ...state, pendingTab: cycledPendingSubTab(state.pendingTab, action.delta).key };
812
843
  }
813
844
  if (state.tab === 1) {
814
845
  return { ...state, authoredTab: state.authoredTab === "open" ? "merged" : "open" };
@@ -1677,7 +1708,7 @@ function queueViewsOf(key, views) {
1677
1708
  }
1678
1709
  function subTabHint(browse) {
1679
1710
  if (browse.tab === 0) {
1680
- return `t ${nextPendingSubTab(browse.pendingTab).label.toLowerCase()} \xB7 `;
1711
+ return `t ${cycledPendingSubTab(browse.pendingTab, 1).label.toLowerCase()} \xB7 `;
1681
1712
  }
1682
1713
  if (browse.tab === 1) {
1683
1714
  return browse.authoredTab === "open" ? "t merged stats \xB7 " : "t open PRs \xB7 ";
@@ -3117,7 +3148,7 @@ function SubTabBar({
3117
3148
  /* @__PURE__ */ jsx8("span", { fg: isActive ? theme.accent : theme.muted, bg, children: `${label} ` })
3118
3149
  ] }, key);
3119
3150
  }),
3120
- /* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t switches" })
3151
+ /* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t/T switches" })
3121
3152
  ] });
3122
3153
  }
3123
3154
 
@@ -3794,7 +3825,7 @@ import { join as join6 } from "node:path";
3794
3825
  // src/github.ts
3795
3826
  import { execFile } from "node:child_process";
3796
3827
  import { createHash } from "node:crypto";
3797
- import { statSync } from "node:fs";
3828
+ import { statSync as statSync2 } from "node:fs";
3798
3829
  import { join as join5, resolve } from "node:path";
3799
3830
  import { promisify } from "node:util";
3800
3831
  var execFileAsync = promisify(execFile);
@@ -3804,10 +3835,10 @@ var token;
3804
3835
  var ghBinary = "gh";
3805
3836
  function resolveDebugBinary(input) {
3806
3837
  const resolved = resolve(input);
3807
- const stats = statSync(resolved, { throwIfNoEntry: false });
3838
+ const stats = statSync2(resolved, { throwIfNoEntry: false });
3808
3839
  if (stats?.isDirectory()) {
3809
3840
  const binary = join5(resolved, "gh");
3810
- if (!statSync(binary, { throwIfNoEntry: false })?.isFile()) {
3841
+ if (!statSync2(binary, { throwIfNoEntry: false })?.isFile()) {
3811
3842
  throw new CliError(`--debug directory "${input}" does not contain a gh executable`);
3812
3843
  }
3813
3844
  return binary;
@@ -5350,7 +5381,15 @@ function SettingValue({
5350
5381
  return /* @__PURE__ */ jsx12(ToggleValue, { value: noCache2 ? "yes" : "no", isSelected });
5351
5382
  }
5352
5383
  case "clearCache": {
5353
- return /* @__PURE__ */ jsx12(PathValue, { path: cacheDir(), confirming: cacheAction === "confirm", isSelected });
5384
+ return /* @__PURE__ */ jsx12(
5385
+ PathValue,
5386
+ {
5387
+ path: cacheDir(),
5388
+ detail: formatBytes(cacheSize()),
5389
+ confirming: cacheAction === "confirm",
5390
+ isSelected
5391
+ }
5392
+ );
5354
5393
  }
5355
5394
  case "autoReload": {
5356
5395
  return /* @__PURE__ */ jsx12(ToggleValue, { value: autoReload2 ? "yes" : "no", isSelected });
@@ -5428,11 +5467,22 @@ function IntervalValue({ value: value2, active, isSelected }) {
5428
5467
  }
5429
5468
  return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.muted, children: value2 });
5430
5469
  }
5431
- function PathValue({ path, confirming, isSelected }) {
5470
+ function PathValue({
5471
+ path,
5472
+ detail,
5473
+ confirming,
5474
+ isSelected
5475
+ }) {
5432
5476
  if (confirming) {
5433
5477
  return /* @__PURE__ */ jsx12("text", { wrapMode: "none", children: /* @__PURE__ */ jsx12("b", { fg: theme.warn, children: "enter to confirm" }) });
5434
5478
  }
5435
- return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
5479
+ return /* @__PURE__ */ jsxs11("text", { wrapMode: "none", children: [
5480
+ /* @__PURE__ */ jsx12("span", { fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") }),
5481
+ detail === void 0 ? null : /* @__PURE__ */ jsxs11("span", { fg: isSelected ? theme.muted : theme.dim, children: [
5482
+ " \xB7 ",
5483
+ detail
5484
+ ] })
5485
+ ] });
5436
5486
  }
5437
5487
 
5438
5488
  // src/tui/components/SnoozeModal.tsx
@@ -8109,7 +8159,7 @@ function handleAppKey(key, context) {
8109
8159
  } else if (key.name === "right" || key.name === "tab") {
8110
8160
  context.dispatchBrowse({ type: "tabCycled", delta: 1 });
8111
8161
  } else if ((context.browse.tab === 0 || context.browse.tab === 1) && key.name === "t") {
8112
- context.dispatchBrowse({ type: "subTabToggled" });
8162
+ context.dispatchBrowse({ type: "subTabCycled", delta: key.shift ? -1 : 1 });
8113
8163
  } else {
8114
8164
  const queue = activeQueueTab(context.browse);
8115
8165
  if (queue === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3lm/pr-stats",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "description": "GitHub PR stats in an interactive terminal UI, via the gh CLI or an access token",
5
5
  "type": "module",
6
6
  "author": "Dominic Elm",