@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`
|
|
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>0.2.24</string>
|
|
23
23
|
<key>CFBundleVersion</key>
|
|
24
|
-
<string>0.2.
|
|
24
|
+
<string>0.2.24</string>
|
|
25
25
|
<key>LSMinimumSystemVersion</key>
|
|
26
26
|
<string>13.0</string>
|
|
27
27
|
<key>LSUIElement</key>
|
|
Binary file
|
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
|
|
791
|
+
function cycledPendingSubTab(current2, delta) {
|
|
761
792
|
const index = PENDING_SUB_TABS.findIndex((entry) => entry.key === current2);
|
|
762
|
-
return PENDING_SUB_TABS[(index +
|
|
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 "
|
|
840
|
+
case "subTabCycled": {
|
|
810
841
|
if (state.tab === 0) {
|
|
811
|
-
return { ...state, pendingTab:
|
|
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 ${
|
|
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 =
|
|
3838
|
+
const stats = statSync2(resolved, { throwIfNoEntry: false });
|
|
3808
3839
|
if (stats?.isDirectory()) {
|
|
3809
3840
|
const binary = join5(resolved, "gh");
|
|
3810
|
-
if (!
|
|
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(
|
|
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({
|
|
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__ */
|
|
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: "
|
|
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) {
|