gh-manager-cli 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +43 -0
- package/dist/index.js +377 -11
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [1.6.1](https://github.com/wiiiimm/gh-manager-cli/compare/v1.6.0...v1.6.1) (2025-08-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* correct GitHub Packages publishing in workflow ([191fcd5](https://github.com/wiiiimm/gh-manager-cli/commit/191fcd503389cb09774305ebde58481cccc9e518))
|
|
7
|
+
|
|
8
|
+
# [1.6.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.5.0...v1.6.0) (2025-08-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* make cache inspection visible in terminal ([2030466](https://github.com/wiiiimm/gh-manager-cli/commit/2030466e1b2bf377a22e07eba5a0334b9c3a6bc5))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* add Apollo cache debugging and verification tools ([e4828c4](https://github.com/wiiiimm/gh-manager-cli/commit/e4828c4463b6ebb58f419f6e6e17c06c699b31ac))
|
|
19
|
+
* add cache testing scripts and environment template ([2b4d840](https://github.com/wiiiimm/gh-manager-cli/commit/2b4d840f0a32a4089c47bca9664ac393e824643a))
|
|
20
|
+
* add repository info modal and always-on Apollo cache ([aecfd31](https://github.com/wiiiimm/gh-manager-cli/commit/aecfd311feaf5e674d2f8f15062f12d6deffcfe5))
|
|
21
|
+
|
|
1
22
|
# [1.5.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.4.2...v1.5.0) (2025-08-31)
|
|
2
23
|
|
|
3
24
|
|
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.
|
|
12
|
+
version: "1.6.1",
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__ */
|
|
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,7 @@ 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__ */ 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 I Info \u2022 Ctrl+L Logout \u2022 R Refresh \u2022 Q Quit" }) })
|
|
1337
1695
|
] })
|
|
1338
1696
|
] });
|
|
1339
1697
|
}
|
|
@@ -1610,7 +1968,15 @@ function App() {
|
|
|
1610
1968
|
}
|
|
1611
1969
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
|
|
1612
1970
|
header,
|
|
1613
|
-
/* @__PURE__ */ jsx2(
|
|
1971
|
+
/* @__PURE__ */ jsx2(
|
|
1972
|
+
RepoList,
|
|
1973
|
+
{
|
|
1974
|
+
token,
|
|
1975
|
+
maxVisibleRows: dims.rows - verticalPadding * 2 - 4,
|
|
1976
|
+
onLogout: handleLogout,
|
|
1977
|
+
viewerLogin: viewer ?? void 0
|
|
1978
|
+
}
|
|
1979
|
+
)
|
|
1614
1980
|
] });
|
|
1615
1981
|
}
|
|
1616
1982
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gh-manager-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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",
|