gh-manager-cli 1.7.0 → 1.8.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.
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __commonJS = (cb, mod) => function __require() {
4
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5
+ };
6
+
7
+ // src/github.ts
8
+ import { graphql as makeGraphQL } from "@octokit/graphql";
9
+ import { ApolloClient, InMemoryCache, HttpLink, gql } from "@apollo/client/core/index.js";
10
+ import { persistCache } from "apollo3-cache-persist";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import envPaths from "env-paths";
14
+ function makeClient(token) {
15
+ return makeGraphQL.defaults({
16
+ headers: { authorization: `token ${token}` }
17
+ });
18
+ }
19
+ async function makeApolloClient(token) {
20
+ try {
21
+ if (typeof globalThis.fetch === "undefined") {
22
+ throw new Error("Fetch API not available. Node 18+ is required.");
23
+ }
24
+ const cache = new InMemoryCache();
25
+ const storage = {
26
+ async getItem(key) {
27
+ try {
28
+ const p = envPaths("gh-manager-cli").data;
29
+ const file = path.join(p, "apollo-cache.json");
30
+ return fs.readFileSync(file, "utf8");
31
+ } catch {
32
+ return null;
33
+ }
34
+ },
35
+ async setItem(key, value) {
36
+ try {
37
+ const p = envPaths("gh-manager-cli").data;
38
+ fs.mkdirSync(p, { recursive: true });
39
+ const file = path.join(p, "apollo-cache.json");
40
+ fs.writeFileSync(file, value, "utf8");
41
+ if (process.platform !== "win32") {
42
+ try {
43
+ fs.chmodSync(file, 384);
44
+ } catch {
45
+ }
46
+ }
47
+ } catch {
48
+ }
49
+ },
50
+ async removeItem(key) {
51
+ try {
52
+ const p = envPaths("gh-manager-cli").data;
53
+ const file = path.join(p, "apollo-cache.json");
54
+ fs.unlinkSync(file);
55
+ } catch {
56
+ }
57
+ }
58
+ };
59
+ await persistCache({ cache, storage, debounce: 500, maxSize: 5 * 1024 * 1024 });
60
+ const link = new HttpLink({
61
+ uri: "https://api.github.com/graphql",
62
+ fetch: globalThis.fetch,
63
+ headers: { authorization: `Bearer ${token}` }
64
+ });
65
+ const client = new ApolloClient({ cache, link });
66
+ return { client, gql };
67
+ } catch (error) {
68
+ const debug = process.env.GH_MANAGER_DEBUG === "1";
69
+ if (debug) {
70
+ process.stderr.write(`
71
+ \u274C Failed to initialize Apollo Client: ${error.message}
72
+ `);
73
+ if (error.stack) {
74
+ process.stderr.write(`Stack: ${error.stack}
75
+ `);
76
+ }
77
+ }
78
+ throw new Error(`Apollo Client initialization failed: ${error.message}`);
79
+ }
80
+ }
81
+ async function getViewerLogin(client) {
82
+ const query = (
83
+ /* GraphQL */
84
+ `
85
+ query ViewerLogin {
86
+ viewer {
87
+ login
88
+ }
89
+ }
90
+ `
91
+ );
92
+ const res = await client(query);
93
+ return res.viewer.login;
94
+ }
95
+ async function fetchViewerOrganizations(client) {
96
+ const query = (
97
+ /* GraphQL */
98
+ `
99
+ query ViewerOrganizations {
100
+ viewer {
101
+ organizations(first: 100) {
102
+ nodes {
103
+ id
104
+ login
105
+ name
106
+ avatarUrl
107
+ }
108
+ }
109
+ }
110
+ }
111
+ `
112
+ );
113
+ const res = await client(query);
114
+ return res.viewer.organizations.nodes;
115
+ }
116
+ async function fetchViewerReposPage(client, first, after, orderBy, includeForkTracking = true, ownerAffiliations = ["OWNER"], organizationLogin) {
117
+ const sortField = orderBy?.field || "UPDATED_AT";
118
+ const sortDirection = orderBy?.direction || "DESC";
119
+ const isOrgContext = !!organizationLogin;
120
+ if (isOrgContext) {
121
+ const query2 = (
122
+ /* GraphQL */
123
+ `
124
+ query OrgRepos(
125
+ $first: Int!
126
+ $after: String
127
+ $sortField: RepositoryOrderField!
128
+ $sortDirection: OrderDirection!
129
+ $orgLogin: String!
130
+ ) {
131
+ rateLimit {
132
+ limit
133
+ remaining
134
+ resetAt
135
+ }
136
+ organization(login: $orgLogin) {
137
+ repositories(
138
+ first: $first
139
+ after: $after
140
+ orderBy: { field: $sortField, direction: $sortDirection }
141
+ ) {
142
+ totalCount
143
+ pageInfo {
144
+ endCursor
145
+ hasNextPage
146
+ }
147
+ nodes {
148
+ id
149
+ name
150
+ nameWithOwner
151
+ description
152
+ visibility
153
+ isPrivate
154
+ isFork
155
+ isArchived
156
+ stargazerCount
157
+ forkCount
158
+ primaryLanguage {
159
+ name
160
+ color
161
+ }
162
+ updatedAt
163
+ pushedAt
164
+ diskUsage
165
+ ${includeForkTracking ? `
166
+ parent {
167
+ nameWithOwner
168
+ defaultBranchRef {
169
+ name
170
+ target {
171
+ ... on Commit {
172
+ history(first: 0) {
173
+ totalCount
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ defaultBranchRef {
180
+ name
181
+ target {
182
+ ... on Commit {
183
+ history(first: 0) {
184
+ totalCount
185
+ }
186
+ }
187
+ }
188
+ }` : `
189
+ parent {
190
+ nameWithOwner
191
+ }
192
+ defaultBranchRef { name }
193
+ `}
194
+ }
195
+ }
196
+ }
197
+ }
198
+ `
199
+ );
200
+ const res2 = await client(query2, {
201
+ first,
202
+ after: after ?? null,
203
+ sortField,
204
+ sortDirection,
205
+ orgLogin: organizationLogin
206
+ });
207
+ const data2 = res2.organization.repositories;
208
+ return {
209
+ nodes: data2.nodes,
210
+ endCursor: data2.pageInfo.endCursor,
211
+ hasNextPage: data2.pageInfo.hasNextPage,
212
+ totalCount: data2.totalCount,
213
+ rateLimit: res2.rateLimit
214
+ };
215
+ }
216
+ const query = (
217
+ /* GraphQL */
218
+ `
219
+ query ViewerRepos(
220
+ $first: Int!
221
+ $after: String
222
+ $sortField: RepositoryOrderField!
223
+ $sortDirection: OrderDirection!
224
+ $affiliations: [RepositoryAffiliation!]!
225
+ ) {
226
+ rateLimit {
227
+ limit
228
+ remaining
229
+ resetAt
230
+ }
231
+ viewer {
232
+ repositories(
233
+ ownerAffiliations: $affiliations
234
+ first: $first
235
+ after: $after
236
+ orderBy: { field: $sortField, direction: $sortDirection }
237
+ ) {
238
+ totalCount
239
+ pageInfo {
240
+ endCursor
241
+ hasNextPage
242
+ }
243
+ nodes {
244
+ id
245
+ name
246
+ nameWithOwner
247
+ description
248
+ visibility
249
+ isPrivate
250
+ isFork
251
+ isArchived
252
+ stargazerCount
253
+ forkCount
254
+ primaryLanguage {
255
+ name
256
+ color
257
+ }
258
+ updatedAt
259
+ pushedAt
260
+ diskUsage
261
+ ${includeForkTracking ? `
262
+ parent {
263
+ nameWithOwner
264
+ defaultBranchRef {
265
+ name
266
+ target {
267
+ ... on Commit {
268
+ history(first: 0) {
269
+ totalCount
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ defaultBranchRef {
276
+ name
277
+ target {
278
+ ... on Commit {
279
+ history(first: 0) {
280
+ totalCount
281
+ }
282
+ }
283
+ }
284
+ }` : `
285
+ parent {
286
+ nameWithOwner
287
+ }
288
+ defaultBranchRef { name }
289
+ `}
290
+ }
291
+ }
292
+ }
293
+ }
294
+ `
295
+ );
296
+ const res = await client(query, {
297
+ first,
298
+ after: after ?? null,
299
+ sortField,
300
+ sortDirection,
301
+ affiliations: ownerAffiliations
302
+ });
303
+ const data = res.viewer.repositories;
304
+ return {
305
+ nodes: data.nodes,
306
+ endCursor: data.pageInfo.endCursor,
307
+ hasNextPage: data.pageInfo.hasNextPage,
308
+ totalCount: data.totalCount,
309
+ rateLimit: res.rateLimit
310
+ };
311
+ }
312
+ async function fetchViewerReposPageUnified(token, first, after, orderBy, includeForkTracking = true, fetchPolicy = "cache-first", ownerAffiliations = ["OWNER"], organizationLogin) {
313
+ const isApolloEnabled = true;
314
+ const debug = process.env.GH_MANAGER_DEBUG === "1";
315
+ const isOrgContext = !!organizationLogin;
316
+ if (debug) {
317
+ console.log(`\u{1F50D} Apollo enabled: ${isApolloEnabled}, Policy: ${fetchPolicy}, After: ${after || "null"}, Context: ${isOrgContext ? "Organization" : "Personal"}`);
318
+ }
319
+ try {
320
+ if (isApolloEnabled) {
321
+ if (debug) console.log("\u{1F680} Attempting Apollo Client...");
322
+ const ap = await makeApolloClient(token);
323
+ const sortField = orderBy?.field || "UPDATED_AT";
324
+ const sortDirection = orderBy?.direction || "DESC";
325
+ let q;
326
+ let variables = { first, after: after ?? null, sortField, sortDirection };
327
+ if (isOrgContext) {
328
+ variables.orgLogin = organizationLogin;
329
+ q = ap.gql`
330
+ query OrgRepos($first: Int!, $after: String, $sortField: RepositoryOrderField!, $sortDirection: OrderDirection!, $orgLogin: String!) {
331
+ rateLimit { limit remaining resetAt }
332
+ organization(login: $orgLogin) {
333
+ repositories(first: $first, after: $after, orderBy: { field: $sortField, direction: $sortDirection }) {
334
+ totalCount
335
+ pageInfo { endCursor hasNextPage }
336
+ nodes {
337
+ id
338
+ name
339
+ nameWithOwner
340
+ description
341
+ visibility
342
+ isPrivate
343
+ isFork
344
+ isArchived
345
+ stargazerCount
346
+ forkCount
347
+ primaryLanguage { name color }
348
+ updatedAt
349
+ pushedAt
350
+ diskUsage
351
+ ${includeForkTracking ? `
352
+ parent { nameWithOwner defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } } }
353
+ defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } }` : `
354
+ parent { nameWithOwner }
355
+ defaultBranchRef { name }`}
356
+ }
357
+ }
358
+ }
359
+ }
360
+ `;
361
+ } else {
362
+ variables.affiliations = ownerAffiliations;
363
+ q = ap.gql`
364
+ query ViewerRepos($first: Int!, $after: String, $sortField: RepositoryOrderField!, $sortDirection: OrderDirection!, $affiliations: [RepositoryAffiliation!]!) {
365
+ rateLimit { limit remaining resetAt }
366
+ viewer {
367
+ repositories(ownerAffiliations: $affiliations, first: $first, after: $after, orderBy: { field: $sortField, direction: $sortDirection }) {
368
+ totalCount
369
+ pageInfo { endCursor hasNextPage }
370
+ nodes {
371
+ id
372
+ name
373
+ nameWithOwner
374
+ description
375
+ visibility
376
+ isPrivate
377
+ isFork
378
+ isArchived
379
+ stargazerCount
380
+ forkCount
381
+ primaryLanguage { name color }
382
+ updatedAt
383
+ pushedAt
384
+ diskUsage
385
+ ${includeForkTracking ? `
386
+ parent { nameWithOwner defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } } }
387
+ defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } }` : `
388
+ parent { nameWithOwner }
389
+ defaultBranchRef { name }`}
390
+ }
391
+ }
392
+ }
393
+ }
394
+ `;
395
+ }
396
+ const startTime = Date.now();
397
+ const res = await ap.client.query({
398
+ query: q,
399
+ variables,
400
+ fetchPolicy
401
+ });
402
+ const duration = Date.now() - startTime;
403
+ if (debug) {
404
+ console.log(`\u26A1 Apollo query completed in ${duration}ms`);
405
+ console.log(`\u{1F4CA} From cache: ${res.loading === false && duration < 50 ? "YES" : "NO"}`);
406
+ console.log(`\u{1F504} Network status: ${res.networkStatus}`);
407
+ }
408
+ const data = isOrgContext ? res.data.organization.repositories : res.data.viewer.repositories;
409
+ return {
410
+ nodes: data.nodes,
411
+ endCursor: data.pageInfo.endCursor,
412
+ hasNextPage: data.pageInfo.hasNextPage,
413
+ totalCount: data.totalCount,
414
+ rateLimit: res.data.rateLimit
415
+ };
416
+ }
417
+ } catch (e) {
418
+ if (debug) console.log(`\u274C Apollo failed, falling back to Octokit:`, e.message);
419
+ }
420
+ if (debug) console.log("\u{1F4E1} Using Octokit fallback...");
421
+ const octo = makeClient(token);
422
+ return fetchViewerReposPage(octo, first, after, orderBy, includeForkTracking, ownerAffiliations, organizationLogin);
423
+ }
424
+ async function searchRepositoriesUnified(token, viewer, text, first, after, sortKey = "UPDATED_AT", sortDir = "DESC", includeForkTracking = true, fetchPolicy = "network-only") {
425
+ const q = `${text} user:${viewer} in:name,description fork:true`;
426
+ try {
427
+ const ap = await makeApolloClient(token);
428
+ const queryDoc = ap.gql`
429
+ query SearchRepos($q: String!, $first: Int!, $after: String) {
430
+ rateLimit { limit remaining resetAt }
431
+ search(query: $q, type: REPOSITORY, first: $first, after: $after) {
432
+ repositoryCount
433
+ pageInfo { endCursor hasNextPage }
434
+ nodes {
435
+ ... on Repository {
436
+ id
437
+ name
438
+ nameWithOwner
439
+ description
440
+ visibility
441
+ isPrivate
442
+ isFork
443
+ isArchived
444
+ stargazerCount
445
+ forkCount
446
+ primaryLanguage { name color }
447
+ updatedAt
448
+ pushedAt
449
+ diskUsage
450
+ ${includeForkTracking ? `
451
+ parent { nameWithOwner defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } } }
452
+ defaultBranchRef { name target { ... on Commit { history(first: 0) { totalCount } } } }` : `
453
+ parent { nameWithOwner }
454
+ defaultBranchRef { name }`}
455
+ }
456
+ }
457
+ }
458
+ }
459
+ `;
460
+ const res = await ap.client.query({
461
+ query: queryDoc,
462
+ variables: { q, first, after: after ?? null },
463
+ fetchPolicy
464
+ });
465
+ const data = res.data.search;
466
+ return {
467
+ nodes: data.nodes,
468
+ endCursor: data.pageInfo.endCursor,
469
+ hasNextPage: data.pageInfo.hasNextPage,
470
+ totalCount: data.repositoryCount,
471
+ rateLimit: res.data.rateLimit
472
+ };
473
+ } catch (e) {
474
+ const debug = process.env.GH_MANAGER_DEBUG === "1";
475
+ if (debug) {
476
+ process.stderr.write(`
477
+ \u274C Search failed: ${e.message}
478
+ `);
479
+ if (e.graphQLErrors) {
480
+ process.stderr.write(`GraphQL errors: ${JSON.stringify(e.graphQLErrors, null, 2)}
481
+ `);
482
+ }
483
+ if (e.networkError) {
484
+ process.stderr.write(`Network error: ${e.networkError.message}
485
+ `);
486
+ }
487
+ }
488
+ throw e;
489
+ }
490
+ }
491
+ async function deleteRepositoryRest(token, owner, repo) {
492
+ const url = `https://api.github.com/repos/${owner}/${repo}`;
493
+ const res = await fetch(url, {
494
+ method: "DELETE",
495
+ headers: {
496
+ "Authorization": `token ${token}`,
497
+ "Accept": "application/vnd.github+json",
498
+ "User-Agent": "gh-manager-cli"
499
+ }
500
+ });
501
+ if (res.status === 204) return;
502
+ let msg = `GitHub REST delete failed (status ${res.status})`;
503
+ try {
504
+ const body = await res.json();
505
+ if (body && body.message) msg += `: ${body.message}`;
506
+ } catch {
507
+ }
508
+ throw new Error(msg);
509
+ }
510
+ async function archiveRepositoryById(client, repositoryId) {
511
+ const mutation = (
512
+ /* GraphQL */
513
+ `
514
+ mutation ArchiveRepo($repositoryId: ID!) {
515
+ archiveRepository(input: { repositoryId: $repositoryId }) {
516
+ clientMutationId
517
+ }
518
+ }
519
+ `
520
+ );
521
+ await client(mutation, { repositoryId });
522
+ }
523
+ async function unarchiveRepositoryById(client, repositoryId) {
524
+ const mutation = (
525
+ /* GraphQL */
526
+ `
527
+ mutation UnarchiveRepo($repositoryId: ID!) {
528
+ unarchiveRepository(input: { repositoryId: $repositoryId }) {
529
+ clientMutationId
530
+ }
531
+ }
532
+ `
533
+ );
534
+ await client(mutation, { repositoryId });
535
+ }
536
+ async function syncForkWithUpstream(token, owner, repo, branch = "main") {
537
+ const url = `https://api.github.com/repos/${owner}/${repo}/merge-upstream`;
538
+ const res = await fetch(url, {
539
+ method: "POST",
540
+ headers: {
541
+ "Authorization": `token ${token}`,
542
+ "Accept": "application/vnd.github+json",
543
+ "User-Agent": "gh-manager-cli"
544
+ },
545
+ body: JSON.stringify({ branch })
546
+ });
547
+ if (res.status === 204) {
548
+ return { message: "Already up-to-date", merge_type: "none", base_branch: branch };
549
+ }
550
+ if (res.status === 200) {
551
+ const body = await res.json();
552
+ return body;
553
+ }
554
+ let msg = `Fork sync failed (status ${res.status})`;
555
+ try {
556
+ const body = await res.json();
557
+ if (body && body.message) {
558
+ msg += `: ${body.message}`;
559
+ if (res.status === 409) {
560
+ msg += " (conflicts detected - manual merge required)";
561
+ }
562
+ }
563
+ } catch {
564
+ }
565
+ throw new Error(msg);
566
+ }
567
+ async function purgeApolloCacheFiles() {
568
+ try {
569
+ const fs2 = await import("fs");
570
+ const path2 = await import("path");
571
+ const envPaths2 = (await import("env-paths")).default;
572
+ const p = envPaths2("gh-manager-cli").data;
573
+ const cacheFile = path2.join(p, "apollo-cache.json");
574
+ const metaFile = path2.join(p, "apollo-cache-meta.json");
575
+ if (process.env.GH_MANAGER_DEBUG === "1") {
576
+ console.log(`\u{1F5D1}\uFE0F Purging cache files from: ${p}`);
577
+ }
578
+ try {
579
+ fs2.unlinkSync(cacheFile);
580
+ } catch {
581
+ }
582
+ try {
583
+ fs2.unlinkSync(metaFile);
584
+ } catch {
585
+ }
586
+ } catch {
587
+ }
588
+ }
589
+ async function inspectCacheStatus() {
590
+ try {
591
+ const fs2 = await import("fs");
592
+ const path2 = await import("path");
593
+ const envPaths2 = (await import("env-paths")).default;
594
+ const p = envPaths2("gh-manager-cli").data;
595
+ const cacheFile = path2.join(p, "apollo-cache.json");
596
+ const metaFile = path2.join(p, "apollo-cache-meta.json");
597
+ process.stderr.write(`
598
+ \u{1F4C2} Cache directory: ${p}
599
+ `);
600
+ try {
601
+ const cacheStats = fs2.statSync(cacheFile);
602
+ process.stderr.write(`\u{1F4BE} Cache file: ${Math.round(cacheStats.size / 1024)}KB (${cacheStats.mtime.toISOString()})
603
+ `);
604
+ } catch {
605
+ process.stderr.write(`\u{1F4BE} Cache file: NOT FOUND
606
+ `);
607
+ }
608
+ try {
609
+ const metaStats = fs2.statSync(metaFile);
610
+ const metaContent = fs2.readFileSync(metaFile, "utf8");
611
+ const meta = JSON.parse(metaContent);
612
+ process.stderr.write(`\u{1F4CA} Meta file: ${Object.keys(meta.fetched || {}).length} entries (${metaStats.mtime.toISOString()})
613
+ `);
614
+ const entries = Object.entries(meta.fetched || {});
615
+ if (entries.length > 0) {
616
+ process.stderr.write("\u{1F4CB} Recent cache entries:\n");
617
+ entries.slice(-3).forEach(([key, timestamp]) => {
618
+ const age = Date.now() - Date.parse(timestamp);
619
+ process.stderr.write(` ${key} (${Math.round(age / 1e3)}s ago)
620
+ `);
621
+ });
622
+ }
623
+ } catch {
624
+ process.stderr.write(`\u{1F4CA} Meta file: NOT FOUND
625
+ `);
626
+ }
627
+ process.stderr.write("\n");
628
+ } catch (e) {
629
+ process.stderr.write(`\u274C Cache inspection failed: ${e.message}
630
+ `);
631
+ }
632
+ }
633
+
634
+ export {
635
+ __commonJS,
636
+ makeClient,
637
+ getViewerLogin,
638
+ fetchViewerOrganizations,
639
+ fetchViewerReposPage,
640
+ fetchViewerReposPageUnified,
641
+ searchRepositoriesUnified,
642
+ deleteRepositoryRest,
643
+ archiveRepositoryById,
644
+ unarchiveRepositoryById,
645
+ syncForkWithUpstream,
646
+ purgeApolloCacheFiles,
647
+ inspectCacheStatus
648
+ };
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ archiveRepositoryById,
4
+ deleteRepositoryRest,
5
+ fetchViewerOrganizations,
6
+ fetchViewerReposPage,
7
+ fetchViewerReposPageUnified,
8
+ getViewerLogin,
9
+ inspectCacheStatus,
10
+ makeClient,
11
+ purgeApolloCacheFiles,
12
+ searchRepositoriesUnified,
13
+ syncForkWithUpstream,
14
+ unarchiveRepositoryById
15
+ } from "./chunk-OQGG2X5P.js";
16
+ export {
17
+ archiveRepositoryById,
18
+ deleteRepositoryRest,
19
+ fetchViewerOrganizations,
20
+ fetchViewerReposPage,
21
+ fetchViewerReposPageUnified,
22
+ getViewerLogin,
23
+ inspectCacheStatus,
24
+ makeClient,
25
+ purgeApolloCacheFiles,
26
+ searchRepositoriesUnified,
27
+ syncForkWithUpstream,
28
+ unarchiveRepositoryById
29
+ };