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** (20 repos/page) → View all repos instantly with smooth scrolling
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
- - **Live Pagination**: Infinite scroll with automatic page prefetching
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
- - Fetches 15 repos per page by default (configurable via `REPOS_PER_FETCH` environment variable, 1-50).
309
- - Updates `totalCount` each time and prefetches the next page when selection nears the end of loaded list.
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
  }
@@ -26,7 +26,7 @@ import {
26
26
  updateCacheAfterRename,
27
27
  updateCacheAfterVisibilityChange,
28
28
  updateCacheWithRepository
29
- } from "./chunk-EZP4YBHA.js";
29
+ } from "./chunk-UOGN2QJU.js";
30
30
  export {
31
31
  archiveRepositoryById,
32
32
  changeRepositoryVisibility,
package/dist/index.js CHANGED
@@ -27,14 +27,14 @@ import {
27
27
  updateCacheAfterRename,
28
28
  updateCacheAfterVisibilityChange,
29
29
  updateCacheWithRepository
30
- } from "./chunk-EZP4YBHA.js";
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.39.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-HIF7MTPO.js").then((m) => m.makeClient(token));
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 <= 50) {
2020
+ if (!isNaN(parsed) && parsed >= 1 && parsed <= 100) {
1971
2021
  return parsed;
1972
2022
  }
1973
2023
  }
1974
- return 15;
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 (!searchActive) {
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, (searchActive ? searchItems.length : items.length) - 1)));
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
- const total = visibleItems.length;
3272
- const LINES_PER_REPO = 3 + spacingLines;
3273
- const visibleRepos = Math.max(1, Math.floor(listHeight / LINES_PER_REPO));
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 && shouldFetch) {
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 && shouldFetch) {
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 && shouldFetch) {
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
- (loading || searchLoading) && /* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginLeft: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "yellow", children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) })
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.39.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": {