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 +21 -0
- package/README.md +66 -0
- package/dist/index.js +387 -13
- package/package.json +6 -1
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
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(
|
|
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.
|
|
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",
|