gh-manager-cli 1.5.0 → 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,17 @@
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
+
1
15
  # [1.5.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.4.2...v1.5.0) (2025-08-31)
2
16
 
3
17
 
package/README.md CHANGED
@@ -41,6 +41,7 @@ On first run, you'll be prompted for a GitHub Personal Access Token.
41
41
  - **Real-time Sorting**: Server-side sorting by updated, pushed, name, or stars (with direction toggle)
42
42
  - **Smart Filtering**: Client-side search through repository names and descriptions
43
43
  - **Repository Actions**:
44
+ - View detailed info (`I`) - Shows repository metadata, language, size, and timestamps
44
45
  - Open in browser (Enter/`O`)
45
46
  - Delete repository (`Del` or `Ctrl+Backspace`) with secure two-step confirmation
46
47
  - Archive/unarchive repositories (`Ctrl+A`) with confirmation prompts
@@ -139,6 +140,7 @@ Launch the app, then use the keys below:
139
140
  - Filter: `/` to enter, type query, Enter to apply (Esc cancels)
140
141
  - Sorting: `S` to cycle field (updated → pushed → name → stars → forks), `D` to toggle direction
141
142
  - Display density: `T` to toggle compact/cozy/comfy
143
+ - Repository info: `I` to view detailed metadata (size, language, timestamps)
142
144
  - Open in browser: Enter or `O`
143
145
  - Delete repository: `Del` or `Ctrl+Backspace` (with confirmation modal)
144
146
  - Uses GitHub REST API (requires `delete_repo` scope and admin rights)
@@ -189,6 +191,47 @@ Project layout:
189
191
  - `src/config.ts` — token read/write and file perms
190
192
  - `src/types.ts` — shared types
191
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
+
192
235
  ## Troubleshooting
193
236
 
194
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.5.0",
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 */
@@ -298,6 +360,78 @@ async function fetchViewerReposPage(client, first, after, orderBy, includeForkTr
298
360
  rateLimit: res.rateLimit
299
361
  };
300
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
+ }
301
435
  async function deleteRepositoryRest(token, owner, repo) {
302
436
  const url = `https://api.github.com/repos/${owner}/${repo}`;
303
437
  const res = await fetch(url, {
@@ -374,12 +508,128 @@ async function syncForkWithUpstream(token, owner, repo, branch = "main") {
374
508
  }
375
509
  throw new Error(msg);
376
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
+ }
377
577
 
378
578
  // src/ui/RepoList.tsx
379
579
  import { useEffect, useMemo, useState } from "react";
380
580
  import { Box, Text, useApp, useInput, useStdout } from "ink";
381
581
  import TextInput from "ink-text-input";
382
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
383
633
  import { exec } from "child_process";
384
634
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
385
635
  var PAGE_SIZE = process.env.GH_MANAGER_DEV === "1" || process.env.NODE_ENV === "development" ? 5 : 15;
@@ -445,7 +695,7 @@ function RepoRow({ repo, selected, index, maxWidth, spacingLines, dim, forkTrack
445
695
  spacingLines > 0 && /* @__PURE__ */ jsx(Box, { height: spacingLines, children: /* @__PURE__ */ jsx(Text, { children: " " }) })
446
696
  ] });
447
697
  }
448
- function RepoList({ token, maxVisibleRows, onLogout }) {
698
+ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin }) {
449
699
  const { exit } = useApp();
450
700
  const { stdout } = useStdout();
451
701
  const client = useMemo(() => makeClient(token), [token]);
@@ -483,6 +733,7 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
483
733
  const [syncing, setSyncing] = useState(false);
484
734
  const [syncError, setSyncError] = useState(null);
485
735
  const [syncFocus, setSyncFocus] = useState("confirm");
736
+ const [infoMode, setInfoMode] = useState(false);
486
737
  const [logoutMode, setLogoutMode] = useState(false);
487
738
  const [logoutFocus, setLogoutFocus] = useState("confirm");
488
739
  const [logoutError, setLogoutError] = useState(null);
@@ -540,7 +791,7 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
540
791
  "name": "NAME",
541
792
  "stars": "STARGAZERS"
542
793
  };
543
- const fetchPage = async (after, reset = false, isSortChange = false, overrideForkTracking) => {
794
+ const fetchPage = async (after, reset = false, isSortChange = false, overrideForkTracking, policy) => {
544
795
  if (isSortChange) {
545
796
  setSortingLoading(true);
546
797
  } else if (after && !reset) {
@@ -553,11 +804,31 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
553
804
  field: sortFieldMap[sortKey],
554
805
  direction: sortDir.toUpperCase()
555
806
  };
556
- 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
+ );
557
815
  setItems((prev) => reset || !after ? page.nodes : [...prev, ...page.nodes]);
558
816
  setEndCursor(page.endCursor);
559
817
  setHasNextPage(page.hasNextPage);
560
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
+ }
561
832
  if (page.rateLimit && rateLimit) {
562
833
  setPrevRateLimit(rateLimit.remaining);
563
834
  }
@@ -587,11 +858,35 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
587
858
  }, []);
588
859
  useEffect(() => {
589
860
  if (!prefsLoaded) return;
590
- 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);
591
874
  }, [client, prefsLoaded]);
592
875
  useEffect(() => {
593
876
  if (items.length > 0) {
594
- 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);
595
890
  }
596
891
  }, [sortKey, sortDir]);
597
892
  useInput((input, key) => {
@@ -739,6 +1034,13 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
739
1034
  }
740
1035
  return;
741
1036
  }
1037
+ if (infoMode) {
1038
+ if (key.escape || input && input.toUpperCase() === "I") {
1039
+ setInfoMode(false);
1040
+ return;
1041
+ }
1042
+ return;
1043
+ }
742
1044
  if (filterMode) {
743
1045
  if (key.escape) {
744
1046
  setFilterMode(false);
@@ -791,7 +1093,14 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
791
1093
  setCursor(0);
792
1094
  setRefreshing(true);
793
1095
  setSortingLoading(true);
794
- 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
+ })();
795
1104
  }
796
1105
  if (key.ctrl && (input === "a" || input === "A")) {
797
1106
  const repo = filteredAndSorted[cursor];
@@ -823,10 +1132,24 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
823
1132
  setLogoutFocus("confirm");
824
1133
  return;
825
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
+ }
826
1145
  if (input === "/") {
827
1146
  setFilterMode(true);
828
1147
  return;
829
1148
  }
1149
+ if (input && input.toUpperCase() === "I") {
1150
+ setInfoMode(true);
1151
+ return;
1152
+ }
830
1153
  if (input && input.toUpperCase() === "S") {
831
1154
  const order = ["updated", "pushed", "name", "stars"];
832
1155
  const idx = order.indexOf(sortKey);
@@ -933,7 +1256,7 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
933
1256
  exec(cmd);
934
1257
  }
935
1258
  const lowRate = rateLimit && rateLimit.remaining <= Math.ceil(rateLimit.limit * 0.1);
936
- const modalOpen = deleteMode || archiveMode || syncMode || logoutMode;
1259
+ const modalOpen = deleteMode || archiveMode || syncMode || logoutMode || infoMode;
937
1260
  const headerBar = useMemo(() => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", height: 1, marginBottom: 1, children: [
938
1261
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
939
1262
  /* @__PURE__ */ jsx(Text, { bold: true, color: modalOpen ? "gray" : void 0, dimColor: modalOpen ? true : void 0, children: " Repositories" }),
@@ -1278,7 +1601,42 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
1278
1601
  logoutFocus === "confirm" ? "Logout" : "Cancel",
1279
1602
  " \u2022 Y to confirm \u2022 C to cancel"
1280
1603
  ] }) })
1281
- ] }) }) : /* @__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: [
1282
1640
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, marginBottom: 1, children: [
1283
1641
  /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
1284
1642
  "Sort: ",
@@ -1333,7 +1691,10 @@ function RepoList({ token, maxVisibleRows, onLogout }) {
1333
1691
  ] }) }),
1334
1692
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, paddingX: 1, flexDirection: "column", children: [
1335
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" }) }),
1336
- /* @__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
+ ] }) })
1337
1698
  ] })
1338
1699
  ] });
1339
1700
  }
@@ -1610,7 +1971,15 @@ function App() {
1610
1971
  }
1611
1972
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
1612
1973
  header,
1613
- /* @__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
+ )
1614
1983
  ] });
1615
1984
  }
1616
1985
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-manager-cli",
3
- "version": "1.5.0",
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",