gh-manager-cli 1.4.2 → 1.6.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,24 @@
1
+ # [1.6.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.5.0...v1.6.0) (2025-08-31)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * make cache inspection visible in terminal ([2030466](https://github.com/wiiiimm/gh-manager-cli/commit/2030466e1b2bf377a22e07eba5a0334b9c3a6bc5))
7
+
8
+
9
+ ### Features
10
+
11
+ * add Apollo cache debugging and verification tools ([e4828c4](https://github.com/wiiiimm/gh-manager-cli/commit/e4828c4463b6ebb58f419f6e6e17c06c699b31ac))
12
+ * add cache testing scripts and environment template ([2b4d840](https://github.com/wiiiimm/gh-manager-cli/commit/2b4d840f0a32a4089c47bca9664ac393e824643a))
13
+ * add repository info modal and always-on Apollo cache ([aecfd31](https://github.com/wiiiimm/gh-manager-cli/commit/aecfd311feaf5e674d2f8f15062f12d6deffcfe5))
14
+
15
+ # [1.5.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.4.2...v1.5.0) (2025-08-31)
16
+
17
+
18
+ ### Features
19
+
20
+ * add fork sync functionality and update documentation ([1d37a10](https://github.com/wiiiimm/gh-manager-cli/commit/1d37a10d90636731c436679b0a4ff2d1c3e1daae))
21
+
1
22
  ## [1.4.2](https://github.com/wiiiimm/gh-manager-cli/compare/v1.4.1...v1.4.2) (2025-08-31)
2
23
 
3
24
 
package/README.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  Interactive terminal app to browse and manage your personal GitHub repositories. Built with Ink (React for CLIs) and the GitHub GraphQL API.
4
4
 
5
+ ## Screenshots
6
+
7
+ <div align="center">
8
+
9
+ ### Repository Listing
10
+ Browse your repositories with rich metadata, sorting, and filtering options.
11
+
12
+ <img src="docs/demo_repo_listing.png" alt="Repository listing interface showing repositories with metadata" width="800">
13
+
14
+ ### Authentication Flow
15
+ Secure GitHub token authentication with validation and persistence.
16
+
17
+ <img src="docs/demo_login.png" alt="Login screen prompting for GitHub token" width="800">
18
+
19
+ ### Delete Confirmation
20
+ Safe repository deletion with two-step confirmation process.
21
+
22
+ <img src="docs/demo_delete_confirmation.png" alt="Delete confirmation dialog with security prompts" width="800">
23
+
24
+ </div>
25
+
5
26
  ## Quick Start
6
27
 
7
28
  ```bash
@@ -20,9 +41,11 @@ On first run, you'll be prompted for a GitHub Personal Access Token.
20
41
  - **Real-time Sorting**: Server-side sorting by updated, pushed, name, or stars (with direction toggle)
21
42
  - **Smart Filtering**: Client-side search through repository names and descriptions
22
43
  - **Repository Actions**:
44
+ - View detailed info (`I`) - Shows repository metadata, language, size, and timestamps
23
45
  - Open in browser (Enter/`O`)
24
46
  - Delete repository (`Del` or `Ctrl+Backspace`) with secure two-step confirmation
25
47
  - Archive/unarchive repositories (`Ctrl+A`) with confirmation prompts
48
+ - Sync forks with upstream (`Ctrl+U`) with automatic conflict detection
26
49
 
27
50
  ### User Interface & Experience
28
51
  - **Keyboard Navigation**: Full keyboard control (arrow keys, PageUp/Down, `Ctrl+G`/`G`)
@@ -117,6 +140,7 @@ Launch the app, then use the keys below:
117
140
  - Filter: `/` to enter, type query, Enter to apply (Esc cancels)
118
141
  - Sorting: `S` to cycle field (updated → pushed → name → stars → forks), `D` to toggle direction
119
142
  - Display density: `T` to toggle compact/cozy/comfy
143
+ - Repository info: `I` to view detailed metadata (size, language, timestamps)
120
144
  - Open in browser: Enter or `O`
121
145
  - Delete repository: `Del` or `Ctrl+Backspace` (with confirmation modal)
122
146
  - Uses GitHub REST API (requires `delete_repo` scope and admin rights)
@@ -124,6 +148,7 @@ Launch the app, then use the keys below:
124
148
  - Confirm: press `Y` or Enter
125
149
  - Cancel: press `C` or Esc
126
150
  - Archive/Unarchive: `Ctrl+A`
151
+ - Sync fork with upstream: `Ctrl+U` (for forks only, shows commit status and handles conflicts)
127
152
  - Logout: `Ctrl+L`
128
153
  - Toggle fork metrics: `F`
129
154
  - Quit: `Q`
@@ -166,6 +191,47 @@ Project layout:
166
191
  - `src/config.ts` — token read/write and file perms
167
192
  - `src/types.ts` — shared types
168
193
 
194
+ ## Apollo Cache (Performance)
195
+
196
+ gh-manager-cli includes built-in Apollo Client caching to reduce GitHub API calls and improve performance. Caching is **always enabled** for optimal performance.
197
+
198
+ ### Verifying Cache is Working
199
+
200
+ 1. **Debug Output**: Run with `GH_MANAGER_DEBUG=1` to see cache status:
201
+ ```bash
202
+ GH_MANAGER_DEBUG=1 npx gh-manager
203
+ ```
204
+
205
+ 2. **Cache Inspection**: Press `i` (in debug mode) to inspect cache status:
206
+ - Shows cache file size and age
207
+ - Lists recent cache entries with timestamps
208
+ - Displays cache directory location
209
+
210
+ 3. **Performance Indicators**:
211
+ - **From cache: YES** = Data served from cache
212
+ - **Query time < 50ms** = Likely cache hit
213
+ - **API credits stable** = Fewer API calls being made
214
+
215
+ ### Why API Credits Might Still Decrease
216
+
217
+ Even with caching enabled, API credits may decrease due to:
218
+
219
+ - **First-time requests**: Initial data must be fetched and cached
220
+ - **Cache expiration**: Default 30-minute TTL (customize with `APOLLO_TTL_MS`)
221
+ - **Pagination**: New pages beyond the cache are fetched from API
222
+ - **Cache-and-network policy**: Updates stale cache data in background
223
+ - **Sorting changes**: Different sort orders create new cache entries
224
+
225
+ ### Cache Configuration
226
+
227
+ ```bash
228
+ # Custom cache TTL (milliseconds) - default: 30 minutes
229
+ APOLLO_TTL_MS=1800000 npx gh-manager
230
+
231
+ # Enable debug mode to see cache performance
232
+ GH_MANAGER_DEBUG=1 npx gh-manager
233
+ ```
234
+
169
235
  ## Troubleshooting
170
236
 
171
237
  - Invalid token: enter a valid PAT (recommended scope: `repo`).
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var require_package = __commonJS({
9
9
  "package.json"(exports, module) {
10
10
  module.exports = {
11
11
  name: "gh-manager-cli",
12
- version: "1.4.2",
12
+ version: "1.6.0",
13
13
  private: false,
14
14
  description: "Interactive CLI to manage your GitHub repos (personal) with Ink",
15
15
  license: "MIT",
@@ -38,13 +38,18 @@ var require_package = __commonJS({
38
38
  "build:binaries": "npm run build && pkg dist/index.js --targets node18-linux-x64,node18-macos-x64,node18-windows-x64 --out-path ./binaries",
39
39
  dev: "tsup --watch",
40
40
  start: "node dist/index.js",
41
+ "start:cache": "GH_MANAGER_APOLLO=1 GH_MANAGER_DEBUG=1 node dist/index.js",
42
+ "start:no-cache": "GH_MANAGER_APOLLO=0 GH_MANAGER_DEBUG=1 node dist/index.js",
43
+ "test:cache": "pnpm build && pnpm start:cache",
41
44
  prepublishOnly: "pnpm run build"
42
45
  },
43
46
  engines: {
44
47
  node: ">=18"
45
48
  },
46
49
  dependencies: {
50
+ "@apollo/client": "^3.11.10",
47
51
  "@octokit/graphql": "^9.0.1",
52
+ "apollo3-cache-persist": "^0.14.1",
48
53
  chalk: "^5.6.0",
49
54
  dotenv: "^17.2.1",
50
55
  "env-paths": "^3.0.0",
@@ -187,6 +192,63 @@ function makeClient(token) {
187
192
  headers: { authorization: `token ${token}` }
188
193
  });
189
194
  }
195
+ async function makeApolloClient(token) {
196
+ const apollo = await import("@apollo/client/core");
197
+ const { persistCache } = await import("apollo3-cache-persist");
198
+ const { ApolloClient, InMemoryCache, HttpLink, gql } = apollo;
199
+ const cache = new InMemoryCache();
200
+ const storage = {
201
+ async getItem(key) {
202
+ try {
203
+ const fs3 = await import("fs");
204
+ const path3 = await import("path");
205
+ const envPaths3 = (await import("env-paths")).default;
206
+ const p = envPaths3("gh-manager-cli").data;
207
+ const file = path3.join(p, "apollo-cache.json");
208
+ return fs3.readFileSync(file, "utf8");
209
+ } catch {
210
+ return null;
211
+ }
212
+ },
213
+ async setItem(key, value) {
214
+ try {
215
+ const fs3 = await import("fs");
216
+ const path3 = await import("path");
217
+ const envPaths3 = (await import("env-paths")).default;
218
+ const p = envPaths3("gh-manager-cli").data;
219
+ fs3.mkdirSync(p, { recursive: true });
220
+ const file = path3.join(p, "apollo-cache.json");
221
+ fs3.writeFileSync(file, value, "utf8");
222
+ if (process.platform !== "win32") {
223
+ try {
224
+ fs3.chmodSync(file, 384);
225
+ } catch {
226
+ }
227
+ }
228
+ } catch {
229
+ }
230
+ },
231
+ async removeItem(key) {
232
+ try {
233
+ const fs3 = await import("fs");
234
+ const path3 = await import("path");
235
+ const envPaths3 = (await import("env-paths")).default;
236
+ const p = envPaths3("gh-manager-cli").data;
237
+ const file = path3.join(p, "apollo-cache.json");
238
+ fs3.unlinkSync(file);
239
+ } catch {
240
+ }
241
+ }
242
+ };
243
+ await persistCache({ cache, storage, debounce: 500, maxSize: 5 * 1024 * 1024 });
244
+ const link = new HttpLink({
245
+ uri: "https://api.github.com/graphql",
246
+ fetch: globalThis.fetch,
247
+ headers: { authorization: `Bearer ${token}` }
248
+ });
249
+ const client = new ApolloClient({ cache, link });
250
+ return { client, gql };
251
+ }
190
252
  async function getViewerLogin(client) {
191
253
  const query = (
192
254
  /* GraphQL */
@@ -252,6 +314,7 @@ async function fetchViewerReposPage(client, first, after, orderBy, includeForkTr
252
314
  parent {
253
315
  nameWithOwner
254
316
  defaultBranchRef {
317
+ name
255
318
  target {
256
319
  ... on Commit {
257
320
  history(first: 0) {
@@ -262,6 +325,7 @@ async function fetchViewerReposPage(client, first, after, orderBy, includeForkTr
262
325
  }
263
326
  }
264
327
  defaultBranchRef {
328
+ name
265
329
  target {
266
330
  ... on Commit {
267
331
  history(first: 0) {
@@ -272,7 +336,9 @@ async function fetchViewerReposPage(client, first, after, orderBy, includeForkTr
272
336
  }` : `
273
337
  parent {
274
338
  nameWithOwner
275
- }`}
339
+ }
340
+ defaultBranchRef { name }
341
+ `}
276
342
  }
277
343
  }
278
344
  }
@@ -294,6 +360,78 @@ async function fetchViewerReposPage(client, first, after, orderBy, includeForkTr
294
360
  rateLimit: res.rateLimit
295
361
  };
296
362
  }
363
+ async function fetchViewerReposPageUnified(token, first, after, orderBy, includeForkTracking = true, fetchPolicy = "cache-first") {
364
+ const isApolloEnabled = true;
365
+ const debug = process.env.GH_MANAGER_DEBUG === "1";
366
+ if (debug) {
367
+ console.log(`\u{1F50D} Apollo enabled: ${isApolloEnabled}, Policy: ${fetchPolicy}, After: ${after || "null"}`);
368
+ }
369
+ try {
370
+ if (isApolloEnabled) {
371
+ if (debug) console.log("\u{1F680} Attempting Apollo Client...");
372
+ const ap = await makeApolloClient(token);
373
+ const sortField = orderBy?.field || "UPDATED_AT";
374
+ const sortDirection = orderBy?.direction || "DESC";
375
+ const q = ap.gql`
376
+ query ViewerRepos($first: Int!, $after: String, $sortField: RepositoryOrderField!, $sortDirection: OrderDirection!) {
377
+ rateLimit { limit remaining resetAt }
378
+ viewer {
379
+ repositories(ownerAffiliations: OWNER, first: $first, after: $after, orderBy: { field: $sortField, direction: $sortDirection }) {
380
+ totalCount
381
+ pageInfo { endCursor hasNextPage }
382
+ nodes {
383
+ id
384
+ name
385
+ nameWithOwner
386
+ description
387
+ visibility
388
+ isPrivate
389
+ isFork
390
+ isArchived
391
+ stargazerCount
392
+ forkCount
393
+ primaryLanguage { name color }
394
+ updatedAt
395
+ pushedAt
396
+ diskUsage
397
+ ${includeForkTracking ? `
398
+ parent { nameWithOwner defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } } }
399
+ defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } }` : `
400
+ parent { nameWithOwner }
401
+ defaultBranchRef { name }`}
402
+ }
403
+ }
404
+ }
405
+ }
406
+ `;
407
+ const startTime = Date.now();
408
+ const res = await ap.client.query({
409
+ query: q,
410
+ variables: { first, after: after ?? null, sortField, sortDirection },
411
+ fetchPolicy
412
+ });
413
+ const duration = Date.now() - startTime;
414
+ if (debug) {
415
+ console.log(`\u26A1 Apollo query completed in ${duration}ms`);
416
+ console.log(`\u{1F4CA} From cache: ${res.loading === false && duration < 50 ? "YES" : "NO"}`);
417
+ console.log(`\u{1F504} Network status: ${res.networkStatus}`);
418
+ }
419
+ const data = res.data.viewer.repositories;
420
+ return {
421
+ nodes: data.nodes,
422
+ endCursor: data.pageInfo.endCursor,
423
+ hasNextPage: data.pageInfo.hasNextPage,
424
+ totalCount: data.totalCount,
425
+ rateLimit: res.data.rateLimit
426
+ };
427
+ }
428
+ } catch (e) {
429
+ if (debug) console.log(`\u274C Apollo failed, falling back to Octokit:`, e.message);
430
+ }
431
+ if (debug) console.log("\u{1F4E1} Using Octokit fallback...");
432
+ const octo = makeClient(token);
433
+ return fetchViewerReposPage(octo, first, after, orderBy, includeForkTracking);
434
+ }
297
435
  async function deleteRepositoryRest(token, owner, repo) {
298
436
  const url = `https://api.github.com/repos/${owner}/${repo}`;
299
437
  const res = await fetch(url, {
@@ -370,12 +508,128 @@ async function syncForkWithUpstream(token, owner, repo, branch = "main") {
370
508
  }
371
509
  throw new Error(msg);
372
510
  }
511
+ async function purgeApolloCacheFiles() {
512
+ try {
513
+ const fs3 = await import("fs");
514
+ const path3 = await import("path");
515
+ const envPaths3 = (await import("env-paths")).default;
516
+ const p = envPaths3("gh-manager-cli").data;
517
+ const cacheFile = path3.join(p, "apollo-cache.json");
518
+ const metaFile = path3.join(p, "apollo-cache-meta.json");
519
+ if (process.env.GH_MANAGER_DEBUG === "1") {
520
+ console.log(`\u{1F5D1}\uFE0F Purging cache files from: ${p}`);
521
+ }
522
+ try {
523
+ fs3.unlinkSync(cacheFile);
524
+ } catch {
525
+ }
526
+ try {
527
+ fs3.unlinkSync(metaFile);
528
+ } catch {
529
+ }
530
+ } catch {
531
+ }
532
+ }
533
+ async function inspectCacheStatus() {
534
+ try {
535
+ const fs3 = await import("fs");
536
+ const path3 = await import("path");
537
+ const envPaths3 = (await import("env-paths")).default;
538
+ const p = envPaths3("gh-manager-cli").data;
539
+ const cacheFile = path3.join(p, "apollo-cache.json");
540
+ const metaFile = path3.join(p, "apollo-cache-meta.json");
541
+ process.stderr.write(`
542
+ \u{1F4C2} Cache directory: ${p}
543
+ `);
544
+ try {
545
+ const cacheStats = fs3.statSync(cacheFile);
546
+ process.stderr.write(`\u{1F4BE} Cache file: ${Math.round(cacheStats.size / 1024)}KB (${cacheStats.mtime.toISOString()})
547
+ `);
548
+ } catch {
549
+ process.stderr.write(`\u{1F4BE} Cache file: NOT FOUND
550
+ `);
551
+ }
552
+ try {
553
+ const metaStats = fs3.statSync(metaFile);
554
+ const metaContent = fs3.readFileSync(metaFile, "utf8");
555
+ const meta = JSON.parse(metaContent);
556
+ process.stderr.write(`\u{1F4CA} Meta file: ${Object.keys(meta.fetched || {}).length} entries (${metaStats.mtime.toISOString()})
557
+ `);
558
+ const entries = Object.entries(meta.fetched || {});
559
+ if (entries.length > 0) {
560
+ process.stderr.write("\u{1F4CB} Recent cache entries:\n");
561
+ entries.slice(-3).forEach(([key, timestamp]) => {
562
+ const age = Date.now() - Date.parse(timestamp);
563
+ process.stderr.write(` ${key} (${Math.round(age / 1e3)}s ago)
564
+ `);
565
+ });
566
+ }
567
+ } catch {
568
+ process.stderr.write(`\u{1F4CA} Meta file: NOT FOUND
569
+ `);
570
+ }
571
+ process.stderr.write("\n");
572
+ } catch (e) {
573
+ process.stderr.write(`\u274C Cache inspection failed: ${e.message}
574
+ `);
575
+ }
576
+ }
373
577
 
374
578
  // src/ui/RepoList.tsx
375
579
  import { useEffect, useMemo, useState } from "react";
376
580
  import { Box, Text, useApp, useInput, useStdout } from "ink";
377
581
  import TextInput from "ink-text-input";
378
582
  import chalk from "chalk";
583
+
584
+ // src/apolloMeta.ts
585
+ import fs2 from "fs";
586
+ import path2 from "path";
587
+ import envPaths2 from "env-paths";
588
+ var paths2 = envPaths2("gh-manager-cli");
589
+ var dataDir = paths2.data;
590
+ var metaPath = path2.join(dataDir, "apollo-cache-meta.json");
591
+ var VERSION = 1;
592
+ function readMeta() {
593
+ try {
594
+ const raw = fs2.readFileSync(metaPath, "utf8");
595
+ const parsed = JSON.parse(raw);
596
+ if (parsed && typeof parsed === "object" && parsed.fetched) return parsed;
597
+ } catch {
598
+ }
599
+ return { version: VERSION, fetched: {} };
600
+ }
601
+ function writeMeta(meta) {
602
+ try {
603
+ fs2.mkdirSync(dataDir, { recursive: true });
604
+ fs2.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf8");
605
+ if (process.platform !== "win32") {
606
+ try {
607
+ fs2.chmodSync(metaPath, 384);
608
+ } catch {
609
+ }
610
+ }
611
+ } catch {
612
+ }
613
+ }
614
+ function makeApolloKey(opts) {
615
+ const v = opts.viewer || "unknown";
616
+ return `viewer:${v}|sort:${opts.sortKey}:${opts.sortDir}|ps:${opts.pageSize}|forks:${opts.forkTracking ? "1" : "0"}`;
617
+ }
618
+ function isFresh(key, ttlMs = Number(process.env.APOLLO_TTL_MS || 30 * 60 * 1e3)) {
619
+ const meta = readMeta();
620
+ const iso = meta.fetched[key];
621
+ if (!iso) return false;
622
+ const t = Date.parse(iso);
623
+ if (!isFinite(t)) return false;
624
+ return Date.now() - t < ttlMs;
625
+ }
626
+ function markFetched(key) {
627
+ const meta = readMeta();
628
+ meta.fetched[key] = (/* @__PURE__ */ new Date()).toISOString();
629
+ writeMeta(meta);
630
+ }
631
+
632
+ // src/ui/RepoList.tsx
379
633
  import { exec } from "child_process";
380
634
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
381
635
  var PAGE_SIZE = process.env.GH_MANAGER_DEV === "1" || process.env.NODE_ENV === "development" ? 5 : 15;
@@ -441,7 +695,7 @@ function RepoRow({ repo, selected, index, maxWidth, spacingLines, dim, forkTrack
441
695
  spacingLines > 0 && /* @__PURE__ */ jsx(Box, { height: spacingLines, children: /* @__PURE__ */ jsx(Text, { children: " " }) })
442
696
  ] });
443
697
  }
444
- function RepoList({ token, maxVisibleRows, onLogout }) {
698
+ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin }) {
445
699
  const { exit } = useApp();
446
700
  const { stdout } = useStdout();
447
701
  const client = useMemo(() => makeClient(token), [token]);
@@ -479,6 +733,7 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
479
733
  const [syncing, setSyncing] = useState(false);
480
734
  const [syncError, setSyncError] = useState(null);
481
735
  const [syncFocus, setSyncFocus] = useState("confirm");
736
+ const [infoMode, setInfoMode] = useState(false);
482
737
  const [logoutMode, setLogoutMode] = useState(false);
483
738
  const [logoutFocus, setLogoutFocus] = useState("confirm");
484
739
  const [logoutError, setLogoutError] = useState(null);
@@ -536,7 +791,7 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
536
791
  "name": "NAME",
537
792
  "stars": "STARGAZERS"
538
793
  };
539
- const fetchPage = async (after, reset = false, isSortChange = false, overrideForkTracking) => {
794
+ const fetchPage = async (after, reset = false, isSortChange = false, overrideForkTracking, policy) => {
540
795
  if (isSortChange) {
541
796
  setSortingLoading(true);
542
797
  } else if (after && !reset) {
@@ -549,11 +804,31 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
549
804
  field: sortFieldMap[sortKey],
550
805
  direction: sortDir.toUpperCase()
551
806
  };
552
- const page = await fetchViewerReposPage(client, PAGE_SIZE, after ?? null, orderBy, overrideForkTracking ?? forkTracking);
807
+ const page = await fetchViewerReposPageUnified(
808
+ token,
809
+ PAGE_SIZE,
810
+ after ?? null,
811
+ orderBy,
812
+ overrideForkTracking ?? forkTracking,
813
+ policy ?? (after ? "network-only" : "cache-first")
814
+ );
553
815
  setItems((prev) => reset || !after ? page.nodes : [...prev, ...page.nodes]);
554
816
  setEndCursor(page.endCursor);
555
817
  setHasNextPage(page.hasNextPage);
556
818
  setTotalCount(page.totalCount);
819
+ if (!after) {
820
+ try {
821
+ const key = makeApolloKey({
822
+ viewer: viewerLogin || "unknown",
823
+ sortKey,
824
+ sortDir,
825
+ pageSize: PAGE_SIZE,
826
+ forkTracking: overrideForkTracking ?? forkTracking
827
+ });
828
+ markFetched(key);
829
+ } catch {
830
+ }
831
+ }
557
832
  if (page.rateLimit && rateLimit) {
558
833
  setPrevRateLimit(rateLimit.remaining);
559
834
  }
@@ -583,11 +858,35 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
583
858
  }, []);
584
859
  useEffect(() => {
585
860
  if (!prefsLoaded) return;
586
- fetchPage();
861
+ let policy = "cache-first";
862
+ try {
863
+ const key = makeApolloKey({
864
+ viewer: viewerLogin || "unknown",
865
+ sortKey,
866
+ sortDir,
867
+ pageSize: PAGE_SIZE,
868
+ forkTracking
869
+ });
870
+ policy = isFresh(key) ? "cache-first" : "cache-and-network";
871
+ } catch {
872
+ }
873
+ fetchPage(null, true, false, void 0, policy);
587
874
  }, [client, prefsLoaded]);
588
875
  useEffect(() => {
589
876
  if (items.length > 0) {
590
- fetchPage(null, true, true);
877
+ let policy = "cache-first";
878
+ try {
879
+ const key = makeApolloKey({
880
+ viewer: viewerLogin || "unknown",
881
+ sortKey,
882
+ sortDir,
883
+ pageSize: PAGE_SIZE,
884
+ forkTracking
885
+ });
886
+ policy = isFresh(key) ? "cache-first" : "cache-and-network";
887
+ } catch {
888
+ }
889
+ fetchPage(null, true, true, void 0, policy);
591
890
  }
592
891
  }, [sortKey, sortDir]);
593
892
  useInput((input, key) => {
@@ -677,7 +976,8 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
677
976
  try {
678
977
  setSyncing(true);
679
978
  const [owner, repo] = syncTarget.nameWithOwner.split("/");
680
- const result = await syncForkWithUpstream(token, owner, repo);
979
+ const branchName = syncTarget.defaultBranchRef?.name || "main";
980
+ const result = await syncForkWithUpstream(token, owner, repo, branchName);
681
981
  setItems((prev) => prev.map((r) => {
682
982
  if (r.id === syncTarget.id && r.parent && r.defaultBranchRef?.target?.history && r.parent.defaultBranchRef?.target?.history) {
683
983
  return {
@@ -734,6 +1034,13 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
734
1034
  }
735
1035
  return;
736
1036
  }
1037
+ if (infoMode) {
1038
+ if (key.escape || input && input.toUpperCase() === "I") {
1039
+ setInfoMode(false);
1040
+ return;
1041
+ }
1042
+ return;
1043
+ }
737
1044
  if (filterMode) {
738
1045
  if (key.escape) {
739
1046
  setFilterMode(false);
@@ -786,7 +1093,14 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
786
1093
  setCursor(0);
787
1094
  setRefreshing(true);
788
1095
  setSortingLoading(true);
789
- fetchPage(null, true, true);
1096
+ ;
1097
+ (async () => {
1098
+ try {
1099
+ await purgeApolloCacheFiles();
1100
+ } catch {
1101
+ }
1102
+ fetchPage(null, true, true, void 0, "network-only");
1103
+ })();
790
1104
  }
791
1105
  if (key.ctrl && (input === "a" || input === "A")) {
792
1106
  const repo = filteredAndSorted[cursor];
@@ -818,10 +1132,24 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
818
1132
  setLogoutFocus("confirm");
819
1133
  return;
820
1134
  }
1135
+ if (key.ctrl && key.shift && (input === "d" || input === "D") || process.env.GH_MANAGER_DEBUG === "1" && input === "i") {
1136
+ (async () => {
1137
+ try {
1138
+ await inspectCacheStatus();
1139
+ } catch (e) {
1140
+ console.log("Failed to inspect cache:", e);
1141
+ }
1142
+ })();
1143
+ return;
1144
+ }
821
1145
  if (input === "/") {
822
1146
  setFilterMode(true);
823
1147
  return;
824
1148
  }
1149
+ if (input && input.toUpperCase() === "I") {
1150
+ setInfoMode(true);
1151
+ return;
1152
+ }
825
1153
  if (input && input.toUpperCase() === "S") {
826
1154
  const order = ["updated", "pushed", "name", "stars"];
827
1155
  const idx = order.indexOf(sortKey);
@@ -928,7 +1256,7 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
928
1256
  exec(cmd);
929
1257
  }
930
1258
  const lowRate = rateLimit && rateLimit.remaining <= Math.ceil(rateLimit.limit * 0.1);
931
- const modalOpen = deleteMode || archiveMode || syncMode || logoutMode;
1259
+ const modalOpen = deleteMode || archiveMode || syncMode || logoutMode || infoMode;
932
1260
  const headerBar = useMemo(() => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", height: 1, marginBottom: 1, children: [
933
1261
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
934
1262
  /* @__PURE__ */ jsx(Text, { bold: true, color: modalOpen ? "gray" : void 0, dimColor: modalOpen ? true : void 0, children: " Repositories" }),
@@ -1273,7 +1601,42 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
1273
1601
  logoutFocus === "confirm" ? "Logout" : "Cancel",
1274
1602
  " \u2022 Y to confirm \u2022 C to cancel"
1275
1603
  ] }) })
1276
- ] }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1604
+ ] }) }) : infoMode ? /* @__PURE__ */ jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: (() => {
1605
+ const repo = filteredAndSorted[cursor];
1606
+ if (!repo) return /* @__PURE__ */ jsx(Text, { color: "red", children: "No repository selected." });
1607
+ const langName = repo.primaryLanguage?.name || "N/A";
1608
+ const langColor = repo.primaryLanguage?.color || "#666666";
1609
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 3, paddingY: 2, width: Math.min(terminalWidth - 8, 90), children: [
1610
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Repository Info" }),
1611
+ /* @__PURE__ */ jsx(Box, { height: 1, children: /* @__PURE__ */ jsx(Text, { children: " " }) }),
1612
+ /* @__PURE__ */ jsx(Text, { children: chalk.bold(repo.nameWithOwner) }),
1613
+ repo.description && /* @__PURE__ */ jsx(Text, { color: "gray", children: repo.description }),
1614
+ /* @__PURE__ */ jsx(Box, { height: 1, children: /* @__PURE__ */ jsx(Text, { children: " " }) }),
1615
+ /* @__PURE__ */ jsxs(Text, { children: [
1616
+ repo.isPrivate ? chalk.yellow("Private") : chalk.green("Public"),
1617
+ repo.isArchived ? chalk.gray(" Archived") : "",
1618
+ repo.isFork ? chalk.blue(" Fork") : ""
1619
+ ] }),
1620
+ /* @__PURE__ */ jsx(Text, { children: chalk.gray(`\u2605 ${repo.stargazerCount} \u2442 ${repo.forkCount}`) }),
1621
+ /* @__PURE__ */ jsxs(Text, { children: [
1622
+ chalk.hex(langColor)(`\u25CF `),
1623
+ chalk.gray(`${langName}`)
1624
+ ] }),
1625
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
1626
+ "Updated: ",
1627
+ formatDate(repo.updatedAt),
1628
+ " \u2022 Pushed: ",
1629
+ formatDate(repo.pushedAt)
1630
+ ] }),
1631
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
1632
+ "Size: ",
1633
+ repo.diskUsage,
1634
+ " KB"
1635
+ ] }),
1636
+ /* @__PURE__ */ jsx(Box, { height: 1, children: /* @__PURE__ */ jsx(Text, { children: " " }) }),
1637
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "Press Esc or I to close" })
1638
+ ] });
1639
+ })() }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1277
1640
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, marginBottom: 1, children: [
1278
1641
  /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
1279
1642
  "Sort: ",
@@ -1328,7 +1691,10 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
1328
1691
  ] }) }),
1329
1692
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, paddingX: 1, flexDirection: "column", children: [
1330
1693
  /* @__PURE__ */ jsx(Box, { width: terminalWidth, justifyContent: "center", children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: modalOpen ? true : void 0, children: "\u2191\u2193 Navigate \u2022 Ctrl+G Top \u2022 G Bottom \u2022 / Filter \u2022 S Sort \u2022 D Direction \u2022 T Density \u2022 F Forks - Commits Behind \u2022 \u23CE/O Open" }) }),
1331
- /* @__PURE__ */ jsx(Box, { width: terminalWidth, justifyContent: "center", children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: modalOpen ? true : void 0, children: "Del/Ctrl+Backspace Delete \u2022 Ctrl+A Un/Archive \u2022 Ctrl+U Sync Fork \u2022 Ctrl+L Logout \u2022 R Refresh \u2022 Q Quit" }) })
1694
+ /* @__PURE__ */ jsx(Box, { width: terminalWidth, justifyContent: "center", children: /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: modalOpen ? true : void 0, children: [
1695
+ "Del/Ctrl+Backspace Delete \u2022 Ctrl+A Un/Archive \u2022 Ctrl+U Sync Fork \u2022 Ctrl+L Logout \u2022 R Refresh \u2022 Q Quit",
1696
+ process.env.GH_MANAGER_DEBUG === "1" && " \u2022 I Cache Info"
1697
+ ] }) })
1332
1698
  ] })
1333
1699
  ] });
1334
1700
  }
@@ -1605,7 +1971,15 @@ function App() {
1605
1971
  }
1606
1972
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
1607
1973
  header,
1608
- /* @__PURE__ */ jsx2(RepoList, { token, maxVisibleRows: dims.rows - verticalPadding * 2 - 4, onLogout: handleLogout })
1974
+ /* @__PURE__ */ jsx2(
1975
+ RepoList,
1976
+ {
1977
+ token,
1978
+ maxVisibleRows: dims.rows - verticalPadding * 2 - 4,
1979
+ onLogout: handleLogout,
1980
+ viewerLogin: viewer ?? void 0
1981
+ }
1982
+ )
1609
1983
  ] });
1610
1984
  }
1611
1985
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-manager-cli",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "description": "Interactive CLI to manage your GitHub repos (personal) with Ink",
6
6
  "license": "MIT",
@@ -29,13 +29,18 @@
29
29
  "build:binaries": "npm run build && pkg dist/index.js --targets node18-linux-x64,node18-macos-x64,node18-windows-x64 --out-path ./binaries",
30
30
  "dev": "tsup --watch",
31
31
  "start": "node dist/index.js",
32
+ "start:cache": "GH_MANAGER_APOLLO=1 GH_MANAGER_DEBUG=1 node dist/index.js",
33
+ "start:no-cache": "GH_MANAGER_APOLLO=0 GH_MANAGER_DEBUG=1 node dist/index.js",
34
+ "test:cache": "pnpm build && pnpm start:cache",
32
35
  "prepublishOnly": "pnpm run build"
33
36
  },
34
37
  "engines": {
35
38
  "node": ">=18"
36
39
  },
37
40
  "dependencies": {
41
+ "@apollo/client": "^3.11.10",
38
42
  "@octokit/graphql": "^9.0.1",
43
+ "apollo3-cache-persist": "^0.14.1",
39
44
  "chalk": "^5.6.0",
40
45
  "dotenv": "^17.2.1",
41
46
  "env-paths": "^3.0.0",