gh-manager-cli 1.41.0 → 1.42.0

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,10 @@
1
+ # [1.42.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.41.0...v1.42.0) (2026-06-05)
2
+
3
+
4
+ ### Features
5
+
6
+ * fuzzy repository search over full cached set (SWR-361) [semantic pr title] ([#55](https://github.com/wiiiimm/gh-manager-cli/issues/55)) ([042999d](https://github.com/wiiiimm/gh-manager-cli/commit/042999da6de2ce6b2688da84a3a4d698e0f7d16d))
7
+
1
8
  # [1.41.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.40.2...v1.41.0) (2026-06-05)
2
9
 
3
10
 
package/README.md CHANGED
@@ -75,7 +75,7 @@ On first run, you'll be prompted to authenticate with GitHub (OAuth recommended)
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
- - **Smart Search**: Server-side search through repository names and descriptions (3+ characters)
78
+ - **Fuzzy Search**: Instant typo-tolerant search over the full cached repository set no network calls in the search path (powered by [fuse.js](https://www.fusejs.io/))
79
79
  - **Visibility Filter**: Modal-based visibility filter (All, Public, Private/Internal for enterprise) with smart filtering
80
80
  - **Archive Filter**: Toggle-based archive filter (`A` key cycles All → Unarchived → Archived) for quick filtering by archive status
81
81
  - **Fork Status Tracking**: Always shows commits behind upstream for forked repositories
package/dist/index.js CHANGED
@@ -17,7 +17,6 @@ import {
17
17
  makeClient,
18
18
  purgeApolloCacheFiles,
19
19
  renameRepositoryById,
20
- searchRepositoriesUnified,
21
20
  starRepository,
22
21
  syncForkWithUpstream,
23
22
  unarchiveRepositoryById,
@@ -34,7 +33,7 @@ var require_package = __commonJS({
34
33
  "package.json"(exports, module) {
35
34
  module.exports = {
36
35
  name: "gh-manager-cli",
37
- version: "1.41.0",
36
+ version: "1.42.0",
38
37
  private: false,
39
38
  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
39
  license: "MIT",
@@ -97,6 +96,7 @@ var require_package = __commonJS({
97
96
  chalk: "^5.6.0",
98
97
  dotenv: "^17.2.1",
99
98
  "env-paths": "^3.0.0",
99
+ "fuse.js": "^7.4.1",
100
100
  graphql: "^16.11.0",
101
101
  ink: "^6.2.3",
102
102
  "ink-spinner": "^5.0.0",
@@ -564,11 +564,6 @@ function makeApolloKey(opts) {
564
564
  const affiliations = opts.affiliations || "OWNER";
565
565
  return `viewer:${v}|context:${context}|affiliations:${affiliations}|sort:${opts.sortKey}:${opts.sortDir}|ps:${opts.pageSize}|forks:${opts.forkTracking ? "1" : "0"}`;
566
566
  }
567
- function makeSearchKey(opts) {
568
- const v = opts.viewer || "unknown";
569
- const query = (opts.q || "").trim().toLowerCase();
570
- return `search:${query}|viewer:${v}|sort:${opts.sortKey}:${opts.sortDir}|ps:${opts.pageSize}|forks:${opts.forkTracking ? "1" : "0"}`;
571
- }
572
567
  function isFresh(key, ttlMs = Number(process.env.APOLLO_TTL_MS || 30 * 60 * 1e3)) {
573
568
  const meta = readMeta();
574
569
  const iso = meta.fetched[key];
@@ -583,6 +578,26 @@ function markFetched(key) {
583
578
  writeMeta(meta);
584
579
  }
585
580
 
581
+ // src/lib/fuzzySearch.ts
582
+ import Fuse from "fuse.js";
583
+ var FUSE_OPTIONS = {
584
+ keys: [
585
+ { name: "name", weight: 0.4 },
586
+ { name: "nameWithOwner", weight: 0.3 },
587
+ { name: "description", weight: 0.2 },
588
+ { name: "primaryLanguage.name", weight: 0.1 }
589
+ ],
590
+ threshold: 0.4,
591
+ ignoreLocation: true,
592
+ includeScore: true
593
+ };
594
+ function fuzzySearch(repos, query) {
595
+ const q = query.trim();
596
+ if (!q) return [];
597
+ const fuse = new Fuse(repos, FUSE_OPTIONS);
598
+ return fuse.search(q).map((r) => r.item);
599
+ }
600
+
586
601
  // src/ui/views/RepoList.tsx
587
602
  import { exec } from "child_process";
588
603
 
@@ -2028,15 +2043,14 @@ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
2028
2043
 
2029
2044
  // src/ui/components/repo/RepoListHeader.tsx
2030
2045
  import { Box as Box18, Text as Text19 } from "ink";
2031
- import { Fragment as Fragment9, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
2046
+ import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
2032
2047
  function RepoListHeader({
2033
2048
  ownerContext,
2034
2049
  sortKey,
2035
2050
  sortDir,
2036
2051
  forkTracking,
2037
2052
  filter,
2038
- searchActive,
2039
- searchLoading,
2053
+ filterActive,
2040
2054
  visibilityFilter = "all",
2041
2055
  archiveFilter = "all",
2042
2056
  isEnterprise = false,
@@ -2051,9 +2065,7 @@ function RepoListHeader({
2051
2065
  starsMode && /* @__PURE__ */ jsx19(Text19, { color: theme.warning, bold: true, children: "\u2B50 Stars Mode" }),
2052
2066
  /* @__PURE__ */ jsxs18(Text19, { color: theme.muted, dimColor: true, children: [
2053
2067
  "Sort: ",
2054
- sortKey,
2055
- " ",
2056
- sortDir === "asc" ? "\u2191" : "\u2193"
2068
+ filterActive ? "relevance" : `${sortKey} ${sortDir === "asc" ? "\u2191" : "\u2193"}`
2057
2069
  ] }),
2058
2070
  /* @__PURE__ */ jsxs18(Text19, { color: theme.muted, dimColor: true, children: [
2059
2071
  "Fork Status - Commits Behind: ",
@@ -2067,27 +2079,17 @@ function RepoListHeader({
2067
2079
  "Archive: ",
2068
2080
  archiveFilter === "archived" ? "Archived" : "Unarchived"
2069
2081
  ] }),
2070
- filter && !searchActive && /* @__PURE__ */ jsxs18(Text19, { color: theme.primary, children: [
2071
- 'Filter: "',
2072
- filter,
2082
+ (filterActive || starsMode && filter.trim().length > 0) && /* @__PURE__ */ jsxs18(Text19, { color: theme.primary, children: [
2083
+ starsMode ? "Filter" : "Search",
2084
+ ': "',
2085
+ filter.trim(),
2073
2086
  '"'
2074
- ] }),
2075
- searchActive && /* @__PURE__ */ jsxs18(Fragment9, { children: [
2076
- /* @__PURE__ */ jsxs18(Text19, { color: theme.primary, children: [
2077
- 'Search: "',
2078
- filter.trim(),
2079
- '"'
2080
- ] }),
2081
- searchLoading && /* @__PURE__ */ jsx19(Box18, { marginLeft: 1, children: /* @__PURE__ */ jsxs18(Text19, { color: theme.primary, children: [
2082
- /* @__PURE__ */ jsx19(SlowSpinner, {}),
2083
- " Searching\u2026"
2084
- ] }) })
2085
2087
  ] })
2086
2088
  ] });
2087
2089
  }
2088
2090
 
2089
2091
  // src/ui/views/RepoList.tsx
2090
- import { Fragment as Fragment10, jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
2092
+ import { Fragment as Fragment9, jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
2091
2093
  var getPageSize = () => {
2092
2094
  const envValue = process.env.REPOS_PER_FETCH;
2093
2095
  if (envValue) {
@@ -2150,11 +2152,6 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2150
2152
  const [orgSwitcherOpen, setOrgSwitcherOpen] = useState16(false);
2151
2153
  const [operationCount, setOperationCount] = useState16(0);
2152
2154
  const [showSponsorReminder, setShowSponsorReminder] = useState16(false);
2153
- const [searchItems, setSearchItems] = useState16([]);
2154
- const [searchEndCursor, setSearchEndCursor] = useState16(null);
2155
- const [searchHasNextPage, setSearchHasNextPage] = useState16(false);
2156
- const [searchTotalCount, setSearchTotalCount] = useState16(0);
2157
- const [searchLoading, setSearchLoading] = useState16(false);
2158
2155
  const [deleteMode, setDeleteMode] = useState16(false);
2159
2156
  const [deleteTarget, setDeleteTarget] = useState16(null);
2160
2157
  const [deleteCode, setDeleteCode] = useState16("");
@@ -2348,7 +2345,6 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2348
2345
  return r;
2349
2346
  };
2350
2347
  setItems((prev) => prev.map(updateRepo));
2351
- setSearchItems((prev) => prev.map(updateRepo));
2352
2348
  trackSuccessfulOperation();
2353
2349
  setStarMode(false);
2354
2350
  setStarTarget(null);
@@ -2406,7 +2402,6 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2406
2402
  return r;
2407
2403
  };
2408
2404
  setItems((prev) => prev.map(updateSyncedRepo));
2409
- setSearchItems((prev) => prev.map(updateSyncedRepo));
2410
2405
  closeSyncModal();
2411
2406
  } catch (e) {
2412
2407
  setSyncing(false);
@@ -2427,7 +2422,6 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2427
2422
  await updateCacheAfterArchive(token, id, !isArchived);
2428
2423
  const updateRepo = (r) => r.id === id ? { ...r, isArchived: !isArchived } : r;
2429
2424
  setItems((prev) => prev.map(updateRepo));
2430
- setSearchItems((prev) => prev.map(updateRepo));
2431
2425
  trackSuccessfulOperation();
2432
2426
  closeArchiveModal();
2433
2427
  } catch (e) {
@@ -2445,7 +2439,6 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2445
2439
  await updateCacheAfterRename(token, id, newName, newNameWithOwner);
2446
2440
  const updateRepo = (r) => r.id === id ? { ...r, name: newName, nameWithOwner: newNameWithOwner } : r;
2447
2441
  setItems((prev) => prev.map(updateRepo));
2448
- setSearchItems((prev) => prev.map(updateRepo));
2449
2442
  closeRenameModal();
2450
2443
  } catch (error2) {
2451
2444
  throw error2;
@@ -2494,18 +2487,12 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2494
2487
  const shouldRemove = visibilityFilter === "public" && newVisibility !== "PUBLIC" || visibilityFilter === "private" && newVisibility !== "PRIVATE" && newVisibility !== "INTERNAL";
2495
2488
  if (shouldRemove) {
2496
2489
  setItems((prev) => prev.filter((r) => r.id !== id));
2497
- setSearchItems((prev) => prev.filter((r) => r.id !== id));
2498
2490
  setTotalCount((c) => Math.max(0, c - 1));
2499
- if (searchActive) {
2500
- setSearchTotalCount((c) => Math.max(0, c - 1));
2501
- }
2502
- const currentItemsLength = searchActive ? searchItems.length : items.length;
2503
- setCursor((c) => Math.max(0, Math.min(c, currentItemsLength - 2)));
2491
+ setCursor((c) => Math.max(0, Math.min(c, items.length - 2)));
2504
2492
  } else {
2505
2493
  const isPrivate = newVisibility === "PRIVATE";
2506
2494
  const updateRepo = (r) => r.id === id ? { ...r, visibility: newVisibility, isPrivate } : r;
2507
2495
  setItems((prev) => prev.map(updateRepo));
2508
- setSearchItems((prev) => prev.map(updateRepo));
2509
2496
  }
2510
2497
  closeChangeVisibilityModal();
2511
2498
  } catch (e) {
@@ -2518,9 +2505,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2518
2505
  setCursor(0);
2519
2506
  setOrgSwitcherOpen(false);
2520
2507
  setItems([]);
2521
- setSearchItems([]);
2522
2508
  setTotalCount(0);
2523
- setSearchTotalCount(0);
2524
2509
  setFilter("");
2525
2510
  setFilterMode(false);
2526
2511
  setVisibilityFilter("all");
@@ -2567,11 +2552,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2567
2552
  const targetId = deleteTarget.id;
2568
2553
  await updateCacheAfterDelete(token, targetId);
2569
2554
  setItems((prev) => prev.filter((r) => r.id !== targetId));
2570
- setSearchItems((prev) => prev.filter((r) => r.id !== targetId));
2571
2555
  setTotalCount((c) => Math.max(0, c - 1));
2572
- if (searchActive) {
2573
- setSearchTotalCount((c) => Math.max(0, c - 1));
2574
- }
2575
2556
  trackSuccessfulOperation();
2576
2557
  setDeleteMode(false);
2577
2558
  setDeleteTarget(null);
@@ -2694,64 +2675,6 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2694
2675
  setLoadingMore(false);
2695
2676
  }
2696
2677
  };
2697
- const fetchSearchPage = async (after, reset = false, policy, searchQuery) => {
2698
- const query = searchQuery ?? filter;
2699
- addDebugMessage(`[fetchSearchPage] query="${query}", searchQuery="${searchQuery}", filter="${filter}"`);
2700
- if (!viewerLogin) {
2701
- addDebugMessage("\u274C No viewerLogin for search");
2702
- return;
2703
- }
2704
- setSearchLoading(true);
2705
- try {
2706
- const orderBy = { field: sortFieldMap[sortKey], direction: sortDir.toUpperCase() };
2707
- const orgLogin = ownerContext !== "personal" ? ownerContext.login : void 0;
2708
- addDebugMessage(`[fetchSearchPage] Calling API with viewer="${viewerLogin}", orgLogin="${orgLogin || "none"}", query="${query.trim()}"`);
2709
- const page = await searchRepositoriesUnified(
2710
- token,
2711
- viewerLogin,
2712
- query.trim(),
2713
- PAGE_SIZE,
2714
- after ?? null,
2715
- orderBy.field,
2716
- orderBy.direction,
2717
- forkTracking,
2718
- policy ?? (after ? "network-only" : "cache-first"),
2719
- orgLogin
2720
- );
2721
- addDebugMessage(`[fetchSearchPage] API returned ${page.nodes.length} results, totalCount=${page.totalCount}`);
2722
- if (page.nodes.length > 0) {
2723
- addDebugMessage(`[fetchSearchPage] First result: ${page.nodes[0].name}`);
2724
- }
2725
- setSearchItems((prev) => reset || !after ? page.nodes : [...prev, ...page.nodes]);
2726
- setSearchEndCursor(page.endCursor);
2727
- setSearchHasNextPage(page.hasNextPage);
2728
- setSearchTotalCount(page.totalCount);
2729
- if (!after) {
2730
- try {
2731
- const key = makeSearchKey({
2732
- viewer: viewerLogin || "unknown",
2733
- q: query.trim(),
2734
- sortKey,
2735
- sortDir,
2736
- pageSize: PAGE_SIZE,
2737
- forkTracking
2738
- });
2739
- markFetched(key);
2740
- } catch {
2741
- }
2742
- }
2743
- setError(null);
2744
- } catch (e) {
2745
- const errorMsg = `Failed to search: ${e.message || e}`;
2746
- addDebugMessage(`\u274C Search error: ${e.message || e}`);
2747
- if (e.stack) {
2748
- addDebugMessage(`Stack: ${e.stack.split("\n")[0]}`);
2749
- }
2750
- setError(errorMsg);
2751
- } finally {
2752
- setSearchLoading(false);
2753
- }
2754
- };
2755
2678
  useEffect12(() => {
2756
2679
  const ui = getUIPrefs();
2757
2680
  if (ui.density !== void 0) setDensity(ui.density);
@@ -2808,64 +2731,14 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2808
2731
  setCursor(0);
2809
2732
  fetchPage(null, true, false, void 0, policy);
2810
2733
  }, [client, prefsLoaded, ownerContext, ownerAffiliations]);
2811
- useEffect12(() => {
2812
- if (searchActive) {
2813
- if (!searchLoading && filter.trim().length >= 3) {
2814
- let policy = "cache-first";
2815
- try {
2816
- const key = makeSearchKey({
2817
- viewer: viewerLogin || "unknown",
2818
- q: filter.trim(),
2819
- sortKey,
2820
- sortDir,
2821
- pageSize: PAGE_SIZE,
2822
- forkTracking
2823
- });
2824
- policy = isFresh(key, 90 * 1e3) ? "cache-first" : "network-only";
2825
- } catch {
2826
- }
2827
- fetchSearchPage(null, true, policy);
2828
- }
2829
- }
2830
- }, [sortKey, sortDir]);
2831
2734
  useEffect12(() => {
2832
2735
  if (visibilityFilter !== "all" || previousVisibilityFilter.current && previousVisibilityFilter.current !== visibilityFilter) {
2833
- if (!searchActive) {
2834
- if (items.length > 0) {
2835
- let policy = "network-only";
2836
- const orgLogin = ownerContext !== "personal" ? ownerContext.login : void 0;
2837
- fetchPage(null, true, true, void 0, policy);
2838
- }
2839
- } else {
2840
- if (!searchLoading && filter.trim().length >= 3) {
2841
- let policy = "network-only";
2842
- fetchSearchPage(null, true, policy);
2843
- }
2736
+ if (items.length > 0) {
2737
+ fetchPage(null, true, true, void 0, "network-only");
2844
2738
  }
2845
2739
  }
2846
2740
  previousVisibilityFilter.current = visibilityFilter;
2847
2741
  }, [visibilityFilter]);
2848
- useEffect12(() => {
2849
- if (viewerLogin && searchActive && !searchLoading && searchItems.length === 0) {
2850
- let policy = "cache-first";
2851
- try {
2852
- const orgLogin = ownerContext !== "personal" ? ownerContext.login : void 0;
2853
- const key = makeSearchKey({
2854
- viewer: viewerLogin || "unknown",
2855
- q: filter.trim(),
2856
- sortKey,
2857
- sortDir,
2858
- pageSize: PAGE_SIZE,
2859
- forkTracking,
2860
- ownerContext: orgLogin ? `org:${orgLogin}` : "personal",
2861
- affiliations: ownerAffiliations.join(",")
2862
- });
2863
- policy = isFresh(key, 90 * 1e3) ? "cache-first" : "network-only";
2864
- } catch {
2865
- }
2866
- fetchSearchPage(null, true, policy);
2867
- }
2868
- }, [viewerLogin]);
2869
2742
  useInput16((input, key) => {
2870
2743
  if (error) {
2871
2744
  if (input && input.toUpperCase() === "Q") {
@@ -3050,15 +2923,11 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3050
2923
  if (key.escape) {
3051
2924
  setFilterMode(false);
3052
2925
  setFilter("");
3053
- setSearchItems([]);
3054
- setSearchEndCursor(null);
3055
- setSearchHasNextPage(false);
3056
- setSearchTotalCount(0);
3057
2926
  setCursor(0);
3058
2927
  addDebugMessage("[ESC] Cleared search and returned to normal listing");
3059
2928
  return;
3060
2929
  }
3061
- if (key.downArrow && (searchActive || starsMode && filter.trim().length > 0) && visibleItems.length > 0) {
2930
+ if (key.downArrow && (filterActive || starsMode && filter.trim().length > 0) && visibleItems.length > 0) {
3062
2931
  setFilterMode(false);
3063
2932
  setCursor(0);
3064
2933
  addDebugMessage("[DOWN] Exited filter mode and selected first result");
@@ -3066,14 +2935,8 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3066
2935
  }
3067
2936
  return;
3068
2937
  }
3069
- if (key.escape && (searchActive || starsMode && filter.trim().length > 0)) {
2938
+ if (key.escape && (filterActive || starsMode && filter.trim().length > 0)) {
3070
2939
  setFilter("");
3071
- if (!starsMode) {
3072
- setSearchItems([]);
3073
- setSearchEndCursor(null);
3074
- setSearchHasNextPage(false);
3075
- setSearchTotalCount(0);
3076
- }
3077
2940
  setCursor(0);
3078
2941
  addDebugMessage("[ESC] Cleared filter and returned to normal listing");
3079
2942
  return;
@@ -3223,11 +3086,11 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3223
3086
  setOrgSwitcherOpen(true);
3224
3087
  return;
3225
3088
  }
3226
- if (input && input.toUpperCase() === "S" && !key.shift && !key.ctrl) {
3089
+ if (input && input.toUpperCase() === "S" && !key.shift && !key.ctrl && !filterActive) {
3227
3090
  setSortMode(true);
3228
3091
  return;
3229
3092
  }
3230
- if (input && input.toUpperCase() === "D") {
3093
+ if (input && input.toUpperCase() === "D" && !filterActive) {
3231
3094
  setSortDirectionMode(true);
3232
3095
  return;
3233
3096
  }
@@ -3239,16 +3102,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3239
3102
  setFilterMode(false);
3240
3103
  if (newStarsMode) {
3241
3104
  setVisibilityFilter("all");
3242
- setSearchItems([]);
3243
- setSearchEndCursor(null);
3244
- setSearchHasNextPage(false);
3245
- setSearchTotalCount(0);
3246
3105
  fetchStarredRepositories(null, true);
3247
- } else {
3248
- setSearchItems([]);
3249
- setSearchEndCursor(null);
3250
- setSearchHasNextPage(false);
3251
- setSearchTotalCount(0);
3252
3106
  }
3253
3107
  return;
3254
3108
  }
@@ -3315,14 +3169,8 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3315
3169
  } else if (archiveFilter === "unarchived") {
3316
3170
  result = result.filter((r) => !r.isArchived);
3317
3171
  }
3318
- const q = filter.trim().toLowerCase();
3319
- if (q) {
3320
- result = result.filter(
3321
- (r) => r.nameWithOwner.toLowerCase().includes(q) || (r.description ? r.description.toLowerCase().includes(q) : false)
3322
- );
3323
- }
3324
3172
  return result;
3325
- }, [items, filter, visibilityFilter, archiveFilter]);
3173
+ }, [items, visibilityFilter, archiveFilter]);
3326
3174
  const filteredAndSorted = useMemo2(() => {
3327
3175
  const arr = [...filtered];
3328
3176
  const dir = sortDir === "asc" ? 1 : -1;
@@ -3343,21 +3191,19 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3343
3191
  });
3344
3192
  return arr;
3345
3193
  }, [filtered, sortKey, sortDir]);
3346
- const searchActive = !starsMode && filter.trim().length >= 3;
3347
- const filteredSearchItems = useMemo2(() => {
3348
- let result = searchItems;
3194
+ const filterActive = !starsMode && filter.trim().length > 0;
3195
+ const fuzzyItems = useMemo2(() => {
3196
+ if (!filterActive) return [];
3197
+ let results = fuzzySearch(items, filter);
3349
3198
  if (visibilityFilter === "private") {
3350
- result = result.filter((r) => r.visibility === "PRIVATE" || r.visibility === "INTERNAL");
3199
+ results = results.filter((r) => r.visibility === "PRIVATE" || r.visibility === "INTERNAL");
3351
3200
  } else if (visibilityFilter === "public") {
3352
- result = result.filter((r) => r.visibility === "PUBLIC");
3201
+ results = results.filter((r) => r.visibility === "PUBLIC");
3353
3202
  }
3354
- if (archiveFilter === "archived") {
3355
- result = result.filter((r) => r.isArchived);
3356
- } else if (archiveFilter === "unarchived") {
3357
- result = result.filter((r) => !r.isArchived);
3358
- }
3359
- return result;
3360
- }, [searchItems, visibilityFilter, archiveFilter]);
3203
+ if (archiveFilter === "archived") results = results.filter((r) => r.isArchived);
3204
+ else if (archiveFilter === "unarchived") results = results.filter((r) => !r.isArchived);
3205
+ return results;
3206
+ }, [filterActive, items, filter, visibilityFilter, archiveFilter]);
3361
3207
  const filteredStarredItems = useMemo2(() => {
3362
3208
  let result = starredItems;
3363
3209
  if (filter && filter.trim().length > 0) {
@@ -3373,15 +3219,10 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3373
3219
  }
3374
3220
  return result;
3375
3221
  }, [starredItems, filter, archiveFilter]);
3376
- const visibleItems = starsMode ? filteredStarredItems : searchActive ? filteredSearchItems : filteredAndSorted;
3377
- useEffect12(() => {
3378
- if (searchActive) {
3379
- addDebugMessage(`[State] searchActive=${searchActive}, searchItems=${searchItems.length}, visibleItems=${visibleItems.length}, filter="${filter}"`);
3380
- }
3381
- }, [searchActive, searchItems.length, visibleItems.length, filter]);
3222
+ const visibleItems = starsMode ? filteredStarredItems : filterActive ? fuzzyItems : filteredAndSorted;
3382
3223
  useEffect12(() => {
3383
3224
  setCursor((c) => Math.min(c, Math.max(0, visibleItems.length - 1)));
3384
- }, [searchActive, searchItems.length, items.length, visibleItems.length]);
3225
+ }, [filterActive, items.length, visibleItems.length]);
3385
3226
  const headerHeight = 2;
3386
3227
  const footerHeight = 4;
3387
3228
  const containerPadding = 2;
@@ -3393,24 +3234,18 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3393
3234
  [visibleItems, cursor, listHeight, spacingLines]
3394
3235
  );
3395
3236
  useEffect12(() => {
3396
- const prefetchThreshold = Math.floor(visibleItems.length * 0.8);
3397
- const nearEnd = visibleItems.length > 0 && cursor >= prefetchThreshold;
3398
- const rawItemsLength = starsMode ? starredItems.length : searchActive ? searchItems.length : items.length;
3237
+ const rawItemsLength = starsMode ? starredItems.length : items.length;
3399
3238
  const filterDrainedPage = visibleItems.length === 0 && archiveFilter !== "all" && rawItemsLength > 0;
3400
3239
  if (starsMode) {
3401
3240
  if (!starredLoading && starredHasNextPage) {
3402
3241
  fetchStarredRepositories(starredEndCursor);
3403
3242
  }
3404
- } else if (searchActive) {
3405
- if (!searchLoading && searchHasNextPage && (nearEnd || filterDrainedPage)) {
3406
- fetchSearchPage(searchEndCursor);
3407
- }
3408
3243
  } else {
3409
3244
  if (!loading && !loadingMore && hasNextPage) {
3410
3245
  fetchPage(endCursor);
3411
3246
  }
3412
3247
  }
3413
- }, [cursor, visibleItems.length, archiveFilter, items.length, starredItems.length, searchItems.length, starsMode, starredLoading, starredHasNextPage, starredEndCursor, searchActive, searchLoading, searchHasNextPage, searchEndCursor, loading, loadingMore, hasNextPage, endCursor]);
3248
+ }, [visibleItems.length, archiveFilter, items.length, starredItems.length, starsMode, starredLoading, starredHasNextPage, starredEndCursor, loading, loadingMore, hasNextPage, endCursor]);
3414
3249
  function openInBrowser(url) {
3415
3250
  const platform = process.platform;
3416
3251
  const cmd = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
@@ -3430,11 +3265,11 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3430
3265
  "(",
3431
3266
  visibleItems.length,
3432
3267
  "/",
3433
- searchActive ? searchTotalCount : totalCount,
3268
+ totalCount,
3434
3269
  ")"
3435
3270
  ] }),
3436
- loadingMore && hasNextPage && !starsMode && !searchActive && totalCount > 0 && /* @__PURE__ */ jsx20(Text20, { color: theme.primary, children: ` \xB7 loading ${items.length}/${totalCount}` }),
3437
- (loading || searchLoading || loadingMore) && /* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginLeft: 1, children: /* @__PURE__ */ jsx20(Text20, { color: theme.warning, children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) })
3271
+ loadingMore && hasNextPage && !starsMode && totalCount > 0 && /* @__PURE__ */ jsx20(Text20, { color: theme.primary, children: ` \xB7 loading ${items.length}/${totalCount}` }),
3272
+ (loading || loadingMore) && /* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginLeft: 1, children: /* @__PURE__ */ jsx20(Text20, { color: theme.warning, children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) })
3438
3273
  ] }),
3439
3274
  (rateLimit || restRateLimit) && /* @__PURE__ */ jsxs19(Text20, { color: lowRate ? theme.warning : theme.muted, children: [
3440
3275
  "GraphQL: ",
@@ -3446,7 +3281,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3446
3281
  prevRestRateLimit !== void 0 && restRateLimit && prevRestRateLimit !== restRateLimit.core.remaining && /* @__PURE__ */ jsx20(Text20, { color: restRateLimit.core.remaining < prevRestRateLimit ? theme.error : theme.success, children: ` (${restRateLimit.core.remaining - prevRestRateLimit > 0 ? "+" : ""}${restRateLimit.core.remaining - prevRestRateLimit})` }),
3447
3282
  " "
3448
3283
  ] })
3449
- ] }), [visibleItems.length, searchActive, searchTotalCount, totalCount, loading, searchLoading, rateLimit, lowRate, modalOpen, prevRateLimit, ownerContext, isEnterpriseOrg, restRateLimit, prevRestRateLimit, theme]);
3284
+ ] }), [visibleItems.length, totalCount, loading, loadingMore, rateLimit, lowRate, modalOpen, prevRateLimit, ownerContext, isEnterpriseOrg, restRateLimit, prevRestRateLimit, starsMode, hasNextPage, items.length, theme]);
3450
3285
  if (error) {
3451
3286
  return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", height: availableHeight, children: [
3452
3287
  /* @__PURE__ */ jsx20(Box19, { flexDirection: "row", justifyContent: "space-between", height: 1, marginBottom: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", gap: 1, children: [
@@ -3500,7 +3335,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3500
3335
  let line2 = "";
3501
3336
  if (langName) line2 += chalk15.hex(langColor)("\u25CF ") + tc.muted(`${langName} `);
3502
3337
  line2 += tc.muted(`\u2605 ${deleteTarget.stargazerCount} \u2442 ${deleteTarget.forkCount} Updated ${formatDate(deleteTarget.updatedAt)}`);
3503
- return /* @__PURE__ */ jsxs19(Fragment10, { children: [
3338
+ return /* @__PURE__ */ jsxs19(Fragment9, { children: [
3504
3339
  /* @__PURE__ */ jsx20(Text20, { children: line1 }),
3505
3340
  /* @__PURE__ */ jsx20(Text20, { children: line2 })
3506
3341
  ] });
@@ -3899,7 +3734,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3899
3734
  error: starError,
3900
3735
  theme
3901
3736
  }
3902
- ) }) : /* @__PURE__ */ jsxs19(Fragment10, { children: [
3737
+ ) }) : /* @__PURE__ */ jsxs19(Fragment9, { children: [
3903
3738
  /* @__PURE__ */ jsx20(
3904
3739
  RepoListHeader,
3905
3740
  {
@@ -3908,8 +3743,7 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3908
3743
  sortDir,
3909
3744
  forkTracking,
3910
3745
  filter,
3911
- searchActive,
3912
- searchLoading,
3746
+ filterActive,
3913
3747
  visibilityFilter,
3914
3748
  archiveFilter,
3915
3749
  isEnterprise: isEnterpriseOrg,
@@ -3924,49 +3758,23 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3924
3758
  {
3925
3759
  value: filter,
3926
3760
  onChange: (val) => {
3927
- addDebugMessage(`[onChange] val="${val}"`);
3928
3761
  setFilter(val);
3929
- const q = (val || "").trim();
3930
- addDebugMessage(`[onChange] trimmed="${q}", len=${q.length}`);
3931
- if (q.length >= 3) {
3932
- addDebugMessage(`[onChange] Triggering search for "${q}"`);
3933
- let policy = "cache-first";
3934
- try {
3935
- const key = makeSearchKey({
3936
- viewer: viewerLogin || "unknown",
3937
- q,
3938
- sortKey,
3939
- sortDir,
3940
- pageSize: PAGE_SIZE,
3941
- forkTracking
3942
- });
3943
- policy = isFresh(key, 90 * 1e3) ? "cache-first" : "network-only";
3944
- } catch {
3945
- }
3946
- addDebugMessage(`[onChange] Calling fetchSearchPage with q="${q}"`);
3947
- fetchSearchPage(null, true, policy, q);
3948
- } else {
3949
- setSearchItems([]);
3950
- setSearchEndCursor(null);
3951
- setSearchHasNextPage(false);
3952
- setSearchTotalCount(0);
3953
- }
3954
3762
  },
3955
3763
  onSubmit: () => {
3956
3764
  setFilterMode(false);
3957
3765
  },
3958
- placeholder: starsMode ? "Type to filter starred repositories..." : "Type to search (3+ chars for server search)..."
3766
+ placeholder: starsMode ? "Type to filter starred repositories..." : "Type to fuzzy-search repositories..."
3959
3767
  }
3960
3768
  )
3961
3769
  ] }),
3962
3770
  /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", height: listHeight, children: [
3963
- filterMode && filter.trim().length > 0 && filter.trim().length < 3 ? /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "gray", dimColor: true, children: "Type at least 3 characters to search" }) }) : visibleItems.slice(windowed.start, windowed.end).map((repo, i) => {
3771
+ visibleItems.slice(windowed.start, windowed.end).map((repo, i) => {
3964
3772
  const idx = windowed.start + i;
3965
3773
  return /* @__PURE__ */ jsx20(
3966
3774
  RepoRow,
3967
3775
  {
3968
3776
  repo,
3969
- selected: filterMode && searchActive ? false : idx === cursor,
3777
+ selected: filterMode ? false : idx === cursor,
3970
3778
  index: idx + 1,
3971
3779
  maxWidth: terminalWidth - 6,
3972
3780
  spacingLines,
@@ -3977,24 +3785,33 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
3977
3785
  repo.nameWithOwner
3978
3786
  );
3979
3787
  }),
3980
- loadingMore && hasNextPage && !starsMode && !searchActive && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", children: [
3788
+ loadingMore && hasNextPage && !starsMode && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", children: [
3981
3789
  /* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginRight: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) }),
3982
3790
  /* @__PURE__ */ jsxs19(Text20, { color: "cyan", children: [
3983
3791
  "Loading repositories\u2026 ",
3984
3792
  totalCount > 0 ? `(${items.length}/${totalCount})` : `(${items.length})`
3985
3793
  ] })
3986
3794
  ] }) }),
3987
- loadingMore && hasNextPage && (starsMode || searchActive) && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", children: [
3795
+ loadingMore && hasNextPage && starsMode && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Box19, { flexDirection: "row", children: [
3988
3796
  /* @__PURE__ */ jsx20(Box19, { width: 2, flexShrink: 0, flexGrow: 0, marginRight: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: /* @__PURE__ */ jsx20(SlowSpinner, {}) }) }),
3989
3797
  /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: "Loading more repositories..." })
3990
3798
  ] }) }),
3991
- !loading && !searchLoading && visibleItems.length === 0 && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "gray", dimColor: true, children: searchActive ? "No repositories match your search" : filter ? "No repositories match your filter" : "No repositories found" }) })
3799
+ filterActive && hasNextPage && !starsMode && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsxs19(Text20, { color: "yellow", dimColor: true, children: [
3800
+ "Still loading repos (",
3801
+ items.length,
3802
+ "/",
3803
+ totalCount > 0 ? totalCount : "?",
3804
+ ") \u2014 fuzzy results may be incomplete"
3805
+ ] }) }),
3806
+ !loading && visibleItems.length === 0 && !(filterActive && hasNextPage && !starsMode) && /* @__PURE__ */ jsx20(Box19, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: /* @__PURE__ */ jsx20(Text20, { color: "gray", dimColor: true, children: filter ? "No repositories match your search" : "No repositories found" }) })
3992
3807
  ] })
3993
3808
  ] }) }),
3994
3809
  /* @__PURE__ */ jsxs19(Box19, { marginTop: 1, paddingX: 1, flexDirection: "column", children: [
3995
3810
  /* @__PURE__ */ jsx20(Box19, { width: terminalWidth, justifyContent: "center", children: /* @__PURE__ */ jsx20(Text20, { color: theme.muted, dimColor: modalOpen ? true : void 0, children: "\u2191\u2193 Navigate \u2022 Ctrl+G Top \u2022 G Bottom \u2022 \u23CE/O Open \u2022 R Refresh" }) }),
3996
3811
  /* @__PURE__ */ jsx20(Box19, { width: terminalWidth, justifyContent: "center", children: /* @__PURE__ */ jsxs19(Text20, { color: theme.muted, dimColor: modalOpen ? true : void 0, children: [
3997
- "/ Search \u2022 S Sort \u2022 D Direction \u2022 T Density \u2022 Shift+T Theme \u2022 A Archive Filter",
3812
+ "/ Search",
3813
+ !filterActive && " \u2022 S Sort \u2022 D Direction",
3814
+ " \u2022 T Density \u2022 Shift+T Theme \u2022 A Archive Filter",
3998
3815
  !starsMode && " \u2022 V Visibility Filter"
3999
3816
  ] }) }),
4000
3817
  /* @__PURE__ */ jsx20(Box19, { width: terminalWidth, justifyContent: "center", children: /* @__PURE__ */ jsx20(Text20, { color: theme.muted, dimColor: modalOpen ? true : void 0, children: starsMode ? "Shift+S My Repos \u2022 I Info \u2022 C Copy URL \u2022 U Unstar Repository" : `${ownerContext === "personal" ? "Shift+S Starred \u2022 " : ""}I Info \u2022 C Copy URL \u2022 Ctrl+S Un/Star \u2022 Ctrl+R Rename \u2022 Ctrl+A Un/Archive \u2022 Ctrl+V Change Visibility \u2022 Ctrl+F Sync Fork` }) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-manager-cli",
3
- "version": "1.41.0",
3
+ "version": "1.42.0",
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",
@@ -63,6 +63,7 @@
63
63
  "chalk": "^5.6.0",
64
64
  "dotenv": "^17.2.1",
65
65
  "env-paths": "^3.0.0",
66
+ "fuse.js": "^7.4.1",
66
67
  "graphql": "^16.11.0",
67
68
  "ink": "^6.2.3",
68
69
  "ink-spinner": "^5.0.0",