gh-manager-cli 1.39.1 → 1.40.1
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.40.1](https://github.com/wiiiimm/gh-manager-cli/compare/v1.40.0...v1.40.1) (2026-06-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* fill compact mode view with actual per-row heights [semantic pr title] ([#47](https://github.com/wiiiimm/gh-manager-cli/issues/47)) ([23de03f](https://github.com/wiiiimm/gh-manager-cli/commit/23de03f3df240bcebc3ac764a1bf76ab8b553c68))
|
|
7
|
+
|
|
8
|
+
# [1.40.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.39.1...v1.40.0) (2026-06-05)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* background fetch-all pagination with light bulk query ([#50](https://github.com/wiiiimm/gh-manager-cli/issues/50)) ([932deb8](https://github.com/wiiiimm/gh-manager-cli/commit/932deb85736bf999bde3c6f4d9bab02aaece8610))
|
|
14
|
+
|
|
1
15
|
## [1.39.1](https://github.com/wiiiimm/gh-manager-cli/compare/v1.39.0...v1.39.1) (2026-06-05)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Interactive terminal app to browse and manage your personal GitHub repositories.
|
|
|
22
22
|
`gh-manager-cli` replaces tedious web clicking with powerful terminal commands:
|
|
23
23
|
|
|
24
24
|
### ❌ GitHub Website Pain Points → ✅ Our Solution
|
|
25
|
-
- **Slow pagination** (
|
|
25
|
+
- **Slow pagination** (page-by-page) → Whole account loaded in the background, browse and search everything instantly
|
|
26
26
|
- **Multiple clicks per action** → Single keypress for any operation
|
|
27
27
|
- **No bulk operations** → Archive, delete, or modify multiple repos at once
|
|
28
28
|
- **Buried settings menus** → Direct keyboard shortcuts for everything
|
|
@@ -73,7 +73,7 @@ On first run, you'll be prompted to authenticate with GitHub (OAuth recommended)
|
|
|
73
73
|
### Core Repository Management
|
|
74
74
|
- **Authentication**: GitHub OAuth (recommended) or Personal Access Token with secure storage
|
|
75
75
|
- **Repository Listing**: Browse all your personal repositories with metadata (stars, forks, language, etc.)
|
|
76
|
-
- **
|
|
76
|
+
- **Background Fetch-All**: Loads your entire account in the background after the first page, so filtering/sorting/search are instant and complete
|
|
77
77
|
- **Interactive Sorting**: Modal-based sort selection (updated, pushed, name, stars) with modal-based direction selection
|
|
78
78
|
- **Smart Search**: Server-side search through repository names and descriptions (3+ characters)
|
|
79
79
|
- **Visibility Filter**: Modal-based visibility filter (All, Public, Private/Internal for enterprise) with smart filtering
|
|
@@ -305,8 +305,9 @@ Status bar shows loaded count vs total. A rate-limit line displays `remaining/li
|
|
|
305
305
|
## Pagination Details
|
|
306
306
|
|
|
307
307
|
- Uses GitHub GraphQL `viewer.repositories` with `ownerAffiliations: OWNER`, ordered by `UPDATED_AT DESC`.
|
|
308
|
-
-
|
|
309
|
-
-
|
|
308
|
+
- **Background fetch-all:** the first page renders immediately, then the remaining repositories load in the background until the whole account is cached locally. Filtering, sorting, and search then operate over the complete set, client-side and instant.
|
|
309
|
+
- Fetches 100 repos per page by default (configurable via `REPOS_PER_FETCH` environment variable, 1-100).
|
|
310
|
+
- Reads `totalCount` from the first page and shows background-load progress (`loaded/total`) while filling. The list stays usable from the first page throughout; very large accounts simply take longer to finish loading.
|
|
310
311
|
|
|
311
312
|
## Development
|
|
312
313
|
|
|
@@ -621,11 +621,8 @@ async function fetchViewerReposPageUnified(token, first, after, orderBy, include
|
|
|
621
621
|
updatedAt
|
|
622
622
|
pushedAt
|
|
623
623
|
diskUsage
|
|
624
|
-
${includeForkTracking ? `
|
|
625
|
-
parent { nameWithOwner defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } } }
|
|
626
|
-
defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } }` : `
|
|
627
624
|
parent { nameWithOwner }
|
|
628
|
-
defaultBranchRef { name }
|
|
625
|
+
defaultBranchRef { name }
|
|
629
626
|
}
|
|
630
627
|
}
|
|
631
628
|
}
|
|
@@ -657,11 +654,8 @@ async function fetchViewerReposPageUnified(token, first, after, orderBy, include
|
|
|
657
654
|
updatedAt
|
|
658
655
|
pushedAt
|
|
659
656
|
diskUsage
|
|
660
|
-
${includeForkTracking ? `
|
|
661
|
-
parent { nameWithOwner defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } } }
|
|
662
|
-
defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } }` : `
|
|
663
657
|
parent { nameWithOwner }
|
|
664
|
-
defaultBranchRef { name }
|
|
658
|
+
defaultBranchRef { name }
|
|
665
659
|
}
|
|
666
660
|
}
|
|
667
661
|
}
|
|
@@ -742,11 +736,8 @@ async function searchRepositoriesUnified(token, viewer, text, first, after, sort
|
|
|
742
736
|
updatedAt
|
|
743
737
|
pushedAt
|
|
744
738
|
diskUsage
|
|
745
|
-
${includeForkTracking ? `
|
|
746
|
-
parent { nameWithOwner defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } } }
|
|
747
|
-
defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } }` : `
|
|
748
739
|
parent { nameWithOwner }
|
|
749
|
-
defaultBranchRef { name }
|
|
740
|
+
defaultBranchRef { name }
|
|
750
741
|
}
|
|
751
742
|
}
|
|
752
743
|
}
|
package/dist/index.js
CHANGED
|
@@ -27,14 +27,14 @@ import {
|
|
|
27
27
|
updateCacheAfterRename,
|
|
28
28
|
updateCacheAfterVisibilityChange,
|
|
29
29
|
updateCacheWithRepository
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-UOGN2QJU.js";
|
|
31
31
|
|
|
32
32
|
// package.json
|
|
33
33
|
var require_package = __commonJS({
|
|
34
34
|
"package.json"(exports, module) {
|
|
35
35
|
module.exports = {
|
|
36
36
|
name: "gh-manager-cli",
|
|
37
|
-
version: "1.
|
|
37
|
+
version: "1.40.1",
|
|
38
38
|
private: false,
|
|
39
39
|
description: "TUI terminal app to manage GitHub repos. Clean up your account in 5 minutes. Archive, delete, rename repos with keyboard shortcuts. Alternative to clicking through github.com",
|
|
40
40
|
license: "MIT",
|
|
@@ -115,6 +115,7 @@ var require_package = __commonJS({
|
|
|
115
115
|
"semantic-release": "^24.2.7",
|
|
116
116
|
tsup: "^8.5.0",
|
|
117
117
|
typescript: "^5.9.2",
|
|
118
|
+
vite: "^6.4.3",
|
|
118
119
|
vitest: "^4.1.0"
|
|
119
120
|
},
|
|
120
121
|
repository: {
|
|
@@ -487,7 +488,7 @@ function OrgSwitcher({ token, currentContext, onSelect, onClose }) {
|
|
|
487
488
|
try {
|
|
488
489
|
setLoading(true);
|
|
489
490
|
setError(null);
|
|
490
|
-
const client = await import("./github-
|
|
491
|
+
const client = await import("./github-ASGTM4ZX.js").then((m) => m.makeClient(token));
|
|
491
492
|
const orgs = await fetchViewerOrganizations(client);
|
|
492
493
|
setOrganizations(orgs);
|
|
493
494
|
const entOrgs = /* @__PURE__ */ new Set();
|
|
@@ -734,6 +735,55 @@ function formatDate(dateStr) {
|
|
|
734
735
|
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
|
|
735
736
|
return `${Math.floor(diffDays / 365)} years ago`;
|
|
736
737
|
}
|
|
738
|
+
function computeWindow(items, cursor, listHeight, spacingLines, buffer = 2) {
|
|
739
|
+
const total = items.length;
|
|
740
|
+
if (total === 0) return { start: 0, end: 0 };
|
|
741
|
+
cursor = Math.max(0, Math.min(cursor, total - 1));
|
|
742
|
+
if (spacingLines > 0) {
|
|
743
|
+
const LINES_PER_REPO = 3 + spacingLines;
|
|
744
|
+
const visibleRepos = Math.max(1, Math.floor(listHeight / LINES_PER_REPO));
|
|
745
|
+
if (visibleRepos >= total) return { start: 0, end: total };
|
|
746
|
+
const half = Math.floor(visibleRepos / 2);
|
|
747
|
+
let start2 = Math.max(0, cursor - half - buffer);
|
|
748
|
+
start2 = Math.min(start2, Math.max(0, total - visibleRepos));
|
|
749
|
+
const end2 = Math.min(total, start2 + visibleRepos + buffer);
|
|
750
|
+
return { start: start2, end: end2 };
|
|
751
|
+
}
|
|
752
|
+
const rowHeight = (idx) => items[idx].description ? 3 : 2;
|
|
753
|
+
if (total * 3 <= listHeight) return { start: 0, end: total };
|
|
754
|
+
if (total * 2 <= listHeight) {
|
|
755
|
+
let totalLines = 0;
|
|
756
|
+
for (let i = 0; i < total; i++) totalLines += rowHeight(i);
|
|
757
|
+
if (totalLines <= listHeight) return { start: 0, end: total };
|
|
758
|
+
}
|
|
759
|
+
const halfHeight = Math.floor(listHeight / 2);
|
|
760
|
+
let start = cursor;
|
|
761
|
+
let accBack = 0;
|
|
762
|
+
while (start > 0) {
|
|
763
|
+
const h = rowHeight(start - 1);
|
|
764
|
+
if (accBack + h > halfHeight) break;
|
|
765
|
+
accBack += h;
|
|
766
|
+
start--;
|
|
767
|
+
}
|
|
768
|
+
let end = start;
|
|
769
|
+
let accFwd = 0;
|
|
770
|
+
while (end < total) {
|
|
771
|
+
const h = rowHeight(end);
|
|
772
|
+
if (accFwd + h > listHeight) break;
|
|
773
|
+
accFwd += h;
|
|
774
|
+
end++;
|
|
775
|
+
}
|
|
776
|
+
if (end >= total) {
|
|
777
|
+
let accFill = accFwd;
|
|
778
|
+
while (start > 0) {
|
|
779
|
+
const h = rowHeight(start - 1);
|
|
780
|
+
if (accFill + h > listHeight) break;
|
|
781
|
+
accFill += h;
|
|
782
|
+
start--;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return { start: Math.max(0, start - buffer), end: Math.min(total, end + buffer) };
|
|
786
|
+
}
|
|
737
787
|
async function copyToClipboard(text) {
|
|
738
788
|
try {
|
|
739
789
|
const clipboardy = await import("clipboardy");
|
|
@@ -1967,11 +2017,11 @@ var getPageSize = () => {
|
|
|
1967
2017
|
const envValue = process.env.REPOS_PER_FETCH;
|
|
1968
2018
|
if (envValue) {
|
|
1969
2019
|
const parsed = parseInt(envValue, 10);
|
|
1970
|
-
if (!isNaN(parsed) && parsed >= 1 && parsed <=
|
|
2020
|
+
if (!isNaN(parsed) && parsed >= 1 && parsed <= 100) {
|
|
1971
2021
|
return parsed;
|
|
1972
2022
|
}
|
|
1973
2023
|
}
|
|
1974
|
-
return
|
|
2024
|
+
return 100;
|
|
1975
2025
|
};
|
|
1976
2026
|
var PAGE_SIZE = getPageSize();
|
|
1977
2027
|
function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextChange, initialOrgSlug: initialOrgSlug2 }) {
|
|
@@ -2678,26 +2728,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
|
|
|
2678
2728
|
fetchPage(null, true, false, void 0, policy);
|
|
2679
2729
|
}, [client, prefsLoaded, ownerContext, ownerAffiliations]);
|
|
2680
2730
|
useEffect12(() => {
|
|
2681
|
-
if (
|
|
2682
|
-
if (items.length > 0) {
|
|
2683
|
-
let policy = "cache-first";
|
|
2684
|
-
const orgLogin = ownerContext !== "personal" ? ownerContext.login : void 0;
|
|
2685
|
-
try {
|
|
2686
|
-
const key = makeApolloKey({
|
|
2687
|
-
viewer: viewerLogin || "unknown",
|
|
2688
|
-
sortKey,
|
|
2689
|
-
sortDir,
|
|
2690
|
-
pageSize: PAGE_SIZE,
|
|
2691
|
-
forkTracking,
|
|
2692
|
-
ownerContext: orgLogin ? `org:${orgLogin}` : "personal",
|
|
2693
|
-
affiliations: ownerAffiliations.join(",")
|
|
2694
|
-
});
|
|
2695
|
-
policy = isFresh(key) ? "cache-first" : "network-only";
|
|
2696
|
-
} catch {
|
|
2697
|
-
}
|
|
2698
|
-
fetchPage(null, true, true, void 0, policy);
|
|
2699
|
-
}
|
|
2700
|
-
} else {
|
|
2731
|
+
if (searchActive) {
|
|
2701
2732
|
if (!searchLoading && filter.trim().length >= 3) {
|
|
2702
2733
|
let policy = "cache-first";
|
|
2703
2734
|
try {
|
|
@@ -3259,45 +3290,33 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
|
|
|
3259
3290
|
}
|
|
3260
3291
|
}, [searchActive, searchItems.length, visibleItems.length, filter]);
|
|
3261
3292
|
useEffect12(() => {
|
|
3262
|
-
setCursor((c) => Math.min(c, Math.max(0,
|
|
3263
|
-
}, [searchActive, searchItems.length, items.length]);
|
|
3293
|
+
setCursor((c) => Math.min(c, Math.max(0, visibleItems.length - 1)));
|
|
3294
|
+
}, [searchActive, searchItems.length, items.length, visibleItems.length]);
|
|
3264
3295
|
const headerHeight = 2;
|
|
3265
3296
|
const footerHeight = 4;
|
|
3266
3297
|
const containerPadding = 2;
|
|
3267
3298
|
const contentHeight = Math.max(1, availableHeight - headerHeight - footerHeight - containerPadding);
|
|
3268
3299
|
const listHeight = Math.max(1, contentHeight - (filterMode ? 2 : 0) - 2);
|
|
3269
3300
|
const spacingLines = density;
|
|
3270
|
-
const windowed = useMemo(
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
if (visibleRepos >= total) return { start: 0, end: total };
|
|
3275
|
-
const buffer = 2;
|
|
3276
|
-
const half = Math.floor(visibleRepos / 2);
|
|
3277
|
-
let start = Math.max(0, cursor - half - buffer);
|
|
3278
|
-
start = Math.min(start, Math.max(0, total - visibleRepos));
|
|
3279
|
-
const end = Math.min(total, start + visibleRepos + buffer);
|
|
3280
|
-
return { start, end };
|
|
3281
|
-
}, [visibleItems.length, cursor, listHeight, spacingLines]);
|
|
3301
|
+
const windowed = useMemo(
|
|
3302
|
+
() => computeWindow(visibleItems, cursor, listHeight, spacingLines),
|
|
3303
|
+
[visibleItems, cursor, listHeight, spacingLines]
|
|
3304
|
+
);
|
|
3282
3305
|
useEffect12(() => {
|
|
3283
3306
|
const prefetchThreshold = Math.floor(visibleItems.length * 0.8);
|
|
3284
3307
|
const nearEnd = visibleItems.length > 0 && cursor >= prefetchThreshold;
|
|
3285
3308
|
const rawItemsLength = starsMode ? starredItems.length : searchActive ? searchItems.length : items.length;
|
|
3286
3309
|
const filterDrainedPage = visibleItems.length === 0 && archiveFilter !== "all" && rawItemsLength > 0;
|
|
3287
|
-
const shouldFetch = nearEnd || filterDrainedPage;
|
|
3288
3310
|
if (starsMode) {
|
|
3289
|
-
if (!starredLoading && starredHasNextPage
|
|
3290
|
-
addDebugMessage(`[Infinite Scroll] Prefetching starred repos at ${cursor}/${visibleItems.length} (80% threshold: ${prefetchThreshold})`);
|
|
3311
|
+
if (!starredLoading && starredHasNextPage) {
|
|
3291
3312
|
fetchStarredRepositories(starredEndCursor);
|
|
3292
3313
|
}
|
|
3293
3314
|
} else if (searchActive) {
|
|
3294
|
-
if (!searchLoading && searchHasNextPage &&
|
|
3295
|
-
addDebugMessage(`[Infinite Scroll] Prefetching search results at ${cursor}/${visibleItems.length} (80% threshold: ${prefetchThreshold})`);
|
|
3315
|
+
if (!searchLoading && searchHasNextPage && (nearEnd || filterDrainedPage)) {
|
|
3296
3316
|
fetchSearchPage(searchEndCursor);
|
|
3297
3317
|
}
|
|
3298
3318
|
} else {
|
|
3299
|
-
if (!loading && !loadingMore && hasNextPage
|
|
3300
|
-
addDebugMessage(`[Infinite Scroll] Prefetching repos at ${cursor}/${visibleItems.length} (80% threshold: ${prefetchThreshold})`);
|
|
3319
|
+
if (!loading && !loadingMore && hasNextPage) {
|
|
3301
3320
|
fetchPage(endCursor);
|
|
3302
3321
|
}
|
|
3303
3322
|
}
|
|
@@ -3324,7 +3343,8 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
|
|
|
3324
3343
|
searchActive ? searchTotalCount : totalCount,
|
|
3325
3344
|
")"
|
|
3326
3345
|
] }),
|
|
3327
|
-
|
|
3346
|
+
loadingMore && hasNextPage && !starsMode && !searchActive && totalCount > 0 && /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: ` \xB7 loading ${items.length}/${totalCount}` }),
|
|
3347
|
+
(loading || searchLoading || loadingMore) && /* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginLeft: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "yellow", children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) })
|
|
3328
3348
|
] }),
|
|
3329
3349
|
(rateLimit || restRateLimit) && /* @__PURE__ */ jsxs19(Text20, { color: lowRate ? "yellow" : "gray", children: [
|
|
3330
3350
|
"GraphQL: ",
|
|
@@ -3856,7 +3876,14 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
|
|
|
3856
3876
|
repo.nameWithOwner
|
|
3857
3877
|
);
|
|
3858
3878
|
}),
|
|
3859
|
-
loadingMore && hasNextPage && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", children: [
|
|
3879
|
+
loadingMore && hasNextPage && !starsMode && !searchActive && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", children: [
|
|
3880
|
+
/* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginRight: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) }),
|
|
3881
|
+
/* @__PURE__ */ jsxs19(Text20, { color: "cyan", children: [
|
|
3882
|
+
"Loading repositories\u2026 ",
|
|
3883
|
+
totalCount > 0 ? `(${items.length}/${totalCount})` : `(${items.length})`
|
|
3884
|
+
] })
|
|
3885
|
+
] }) }),
|
|
3886
|
+
loadingMore && hasNextPage && (starsMode || searchActive) && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", children: [
|
|
3860
3887
|
/* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginRight: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) }),
|
|
3861
3888
|
/* @__PURE__ */ jsx20(Text20, { color: "cyan", children: "Loading more repositories..." })
|
|
3862
3889
|
] }) }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gh-manager-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "TUI terminal app to manage GitHub repos. Clean up your account in 5 minutes. Archive, delete, rename repos with keyboard shortcuts. Alternative to clicking through github.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"semantic-release": "^24.2.7",
|
|
82
82
|
"tsup": "^8.5.0",
|
|
83
83
|
"typescript": "^5.9.2",
|
|
84
|
+
"vite": "^6.4.3",
|
|
84
85
|
"vitest": "^4.1.0"
|
|
85
86
|
},
|
|
86
87
|
"repository": {
|