@gh-symphony/cli 0.0.19 → 0.0.21

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.
@@ -3,6 +3,13 @@
3
3
  // src/github/client.ts
4
4
  var DEFAULT_API_URL = "https://api.github.com/graphql";
5
5
  var REST_API_URL = "https://api.github.com";
6
+ function findLinkedRepository(project, owner, name) {
7
+ const normalizedOwner = owner.trim().toLowerCase();
8
+ const normalizedName = name.trim().toLowerCase();
9
+ return project.linkedRepositories.find(
10
+ (repository) => repository.owner.trim().toLowerCase() === normalizedOwner && repository.name.trim().toLowerCase() === normalizedName
11
+ ) ?? null;
12
+ }
6
13
  var GitHubApiError = class extends Error {
7
14
  constructor(message, status) {
8
15
  super(message);
@@ -18,6 +25,14 @@ var GitHubScopeError = class extends GitHubApiError {
18
25
  this.name = "GitHubScopeError";
19
26
  }
20
27
  };
28
+ var GitHubRepositoryLookupError = class extends GitHubApiError {
29
+ constructor(reason, message, remediation, status) {
30
+ super(message, status);
31
+ this.reason = reason;
32
+ this.remediation = remediation;
33
+ this.name = "GitHubRepositoryLookupError";
34
+ }
35
+ };
21
36
  function createClient(token, options) {
22
37
  return {
23
38
  token,
@@ -25,6 +40,77 @@ function createClient(token, options) {
25
40
  fetchImpl: options?.fetchImpl ?? fetch
26
41
  };
27
42
  }
43
+ async function getRepositoryMetadata(client, owner, name) {
44
+ const restUrl = client.apiUrl.replace("/graphql", "");
45
+ const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
46
+ const repoPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
47
+ let response;
48
+ try {
49
+ response = await client.fetchImpl(`${baseUrl}${repoPath}`, {
50
+ headers: {
51
+ authorization: `Bearer ${client.token}`,
52
+ accept: "application/vnd.github+json"
53
+ }
54
+ });
55
+ } catch (error) {
56
+ const detail = error instanceof Error && error.message.length > 0 ? ` ${error.message}` : "";
57
+ throw new GitHubRepositoryLookupError(
58
+ "offline",
59
+ `GitHub repository validation could not reach the API.${detail}`.trim(),
60
+ "Check your network connection and re-run the command to validate before saving."
61
+ );
62
+ }
63
+ if (!response.ok) {
64
+ const payload = await response.json().catch(() => null);
65
+ const message = payload?.message?.trim() || response.statusText;
66
+ if (response.status === 403 && (response.headers.get("x-ratelimit-remaining") === "0" || /rate limit/i.test(message))) {
67
+ throw new GitHubRepositoryLookupError(
68
+ "rate_limited",
69
+ "GitHub API rate limit blocked repository validation.",
70
+ "Wait for the rate limit window to reset, then re-run 'gh-symphony repo add owner/name'.",
71
+ response.status
72
+ );
73
+ }
74
+ if (response.status === 401) {
75
+ throw new GitHubRepositoryLookupError(
76
+ "invalid_token",
77
+ "GitHub token is invalid or expired.",
78
+ "Run 'gh auth login --scopes repo,read:org,project' or refresh GITHUB_GRAPHQL_TOKEN, then retry.",
79
+ response.status
80
+ );
81
+ }
82
+ if (response.status === 403 || /resource not accessible|saml|single sign-on|access denied/i.test(message)) {
83
+ throw new GitHubRepositoryLookupError(
84
+ "no_access",
85
+ `GitHub denied access to ${owner}/${name}.`,
86
+ "Confirm that the authenticated user can read this repository and that the token has the required access.",
87
+ response.status
88
+ );
89
+ }
90
+ if (response.status === 404) {
91
+ throw new GitHubRepositoryLookupError(
92
+ "not_found",
93
+ `Repository ${owner}/${name} was not found.`,
94
+ "Check the owner/name spelling. If the repository is private, confirm the current token can access it.",
95
+ response.status
96
+ );
97
+ }
98
+ throw new GitHubRepositoryLookupError(
99
+ "unknown",
100
+ `GitHub repository validation failed: ${response.status} ${message}`.trim(),
101
+ "Retry the command. If the problem continues, verify GitHub API access separately.",
102
+ response.status
103
+ );
104
+ }
105
+ const repo = await response.json();
106
+ return {
107
+ owner: repo.owner.login,
108
+ name: repo.name,
109
+ url: repo.html_url,
110
+ cloneUrl: repo.clone_url,
111
+ visibility: repo.visibility ?? (repo.private === true ? "private" : "public")
112
+ };
113
+ }
28
114
  async function validateToken(client) {
29
115
  const restUrl = client.apiUrl.replace("/graphql", "");
30
116
  const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
@@ -57,34 +143,123 @@ function checkRequiredScopes(scopes) {
57
143
  const missing = required.filter((r) => !normalizedScopes.includes(r));
58
144
  return { valid: missing.length === 0, missing };
59
145
  }
60
- async function listUserProjects(client) {
61
- const data = await graphql(
62
- client,
63
- VIEWER_PROJECTS_QUERY
64
- );
146
+ var PROJECT_PAGE_SIZE = 50;
147
+ var ORGANIZATION_PAGE_SIZE = 20;
148
+ var MAX_PROJECT_DISCOVERY_REQUESTS = 40;
149
+ var MAX_DISCOVERED_PROJECTS = 1e3;
150
+ async function discoverUserProjects(client) {
65
151
  const projects = [];
66
- for (const node of data.viewer.projectsV2?.nodes ?? []) {
67
- if (!node) continue;
68
- projects.push(
69
- normalizeProjectSummary(node, {
70
- login: data.viewer.login,
71
- type: "User"
72
- })
152
+ const seenProjectIds = /* @__PURE__ */ new Set();
153
+ const orgLogins = [];
154
+ let requestCount = 0;
155
+ let partial = false;
156
+ let reason = null;
157
+ const tryStartRequest = () => {
158
+ if (requestCount >= MAX_PROJECT_DISCOVERY_REQUESTS) {
159
+ partial = true;
160
+ reason ??= "request_limit";
161
+ return false;
162
+ }
163
+ requestCount += 1;
164
+ return true;
165
+ };
166
+ const collectProject = (node, owner) => {
167
+ if (seenProjectIds.has(node.id)) {
168
+ return true;
169
+ }
170
+ if (projects.length >= MAX_DISCOVERED_PROJECTS) {
171
+ partial = true;
172
+ reason ??= "result_limit";
173
+ return false;
174
+ }
175
+ seenProjectIds.add(node.id);
176
+ projects.push(normalizeProjectSummary(node, owner));
177
+ return true;
178
+ };
179
+ let viewerProjectsCursor = null;
180
+ let hasMoreViewerProjects = true;
181
+ let viewerLogin = "";
182
+ while (hasMoreViewerProjects) {
183
+ if (!tryStartRequest()) {
184
+ break;
185
+ }
186
+ const data = await graphql(
187
+ client,
188
+ VIEWER_PROJECTS_PAGE_QUERY,
189
+ { cursor: viewerProjectsCursor }
73
190
  );
74
- }
75
- for (const orgNode of data.viewer.organizations?.nodes ?? []) {
76
- if (!orgNode) continue;
77
- for (const projNode of orgNode.projectsV2?.nodes ?? []) {
78
- if (!projNode) continue;
79
- projects.push(
80
- normalizeProjectSummary(projNode, {
81
- login: orgNode.login,
82
- type: "Organization"
83
- })
191
+ viewerLogin = data.viewer.login;
192
+ const projectPage = data.viewer.projectsV2;
193
+ for (const node of projectPage?.nodes ?? []) {
194
+ if (!node) continue;
195
+ if (!collectProject(node, { login: viewerLogin, type: "User" })) {
196
+ hasMoreViewerProjects = false;
197
+ break;
198
+ }
199
+ }
200
+ if (partial) {
201
+ break;
202
+ }
203
+ hasMoreViewerProjects = projectPage?.pageInfo?.hasNextPage ?? false;
204
+ viewerProjectsCursor = projectPage?.pageInfo?.endCursor ?? null;
205
+ }
206
+ let organizationsCursor = null;
207
+ let hasMoreOrganizations = true;
208
+ while (!partial && hasMoreOrganizations) {
209
+ if (!tryStartRequest()) {
210
+ break;
211
+ }
212
+ const data = await graphql(
213
+ client,
214
+ VIEWER_ORGANIZATIONS_PAGE_QUERY,
215
+ { cursor: organizationsCursor }
216
+ );
217
+ for (const orgNode of data.viewer.organizations?.nodes ?? []) {
218
+ if (!orgNode) continue;
219
+ orgLogins.push(orgNode.login);
220
+ }
221
+ hasMoreOrganizations = data.viewer.organizations?.pageInfo?.hasNextPage ?? false;
222
+ organizationsCursor = data.viewer.organizations?.pageInfo?.endCursor ?? null;
223
+ }
224
+ for (const orgLogin of orgLogins) {
225
+ let orgProjectsCursor = null;
226
+ let hasMoreOrgProjects = true;
227
+ while (!partial && hasMoreOrgProjects) {
228
+ if (!tryStartRequest()) {
229
+ break;
230
+ }
231
+ const data = await graphql(
232
+ client,
233
+ ORGANIZATION_PROJECTS_PAGE_QUERY,
234
+ { login: orgLogin, cursor: orgProjectsCursor }
84
235
  );
236
+ const projectPage = data.organization?.projectsV2 ?? null;
237
+ for (const node of projectPage?.nodes ?? []) {
238
+ if (!node) continue;
239
+ if (!collectProject(node, {
240
+ login: orgLogin,
241
+ type: "Organization"
242
+ })) {
243
+ hasMoreOrgProjects = false;
244
+ break;
245
+ }
246
+ }
247
+ if (partial) {
248
+ break;
249
+ }
250
+ hasMoreOrgProjects = projectPage?.pageInfo?.hasNextPage ?? false;
251
+ orgProjectsCursor = projectPage?.pageInfo?.endCursor ?? null;
85
252
  }
86
253
  }
87
- return projects;
254
+ return {
255
+ projects,
256
+ partial,
257
+ reason,
258
+ requests: requestCount
259
+ };
260
+ }
261
+ async function listUserProjects(client) {
262
+ return (await discoverUserProjects(client)).projects;
88
263
  }
89
264
  function normalizeProjectSummary(node, owner) {
90
265
  return {
@@ -230,11 +405,11 @@ async function graphql(client, query, variables) {
230
405
  }
231
406
  return payload.data;
232
407
  }
233
- var VIEWER_PROJECTS_QUERY = `
234
- query ViewerProjects {
408
+ var VIEWER_PROJECTS_PAGE_QUERY = `
409
+ query ViewerProjectsPage($cursor: String) {
235
410
  viewer {
236
411
  login
237
- projectsV2(first: 50) {
412
+ projectsV2(first: ${PROJECT_PAGE_SIZE}, after: $cursor) {
238
413
  nodes {
239
414
  id
240
415
  title
@@ -242,19 +417,43 @@ var VIEWER_PROJECTS_QUERY = `
242
417
  url
243
418
  items { totalCount }
244
419
  }
420
+ pageInfo {
421
+ endCursor
422
+ hasNextPage
423
+ }
245
424
  }
246
- organizations(first: 20) {
425
+ }
426
+ }
427
+ `;
428
+ var VIEWER_ORGANIZATIONS_PAGE_QUERY = `
429
+ query ViewerOrganizationsPage($cursor: String) {
430
+ viewer {
431
+ organizations(first: ${ORGANIZATION_PAGE_SIZE}, after: $cursor) {
247
432
  nodes {
248
433
  login
249
- projectsV2(first: 50) {
250
- nodes {
251
- id
252
- title
253
- shortDescription
254
- url
255
- items { totalCount }
256
- }
257
- }
434
+ }
435
+ pageInfo {
436
+ endCursor
437
+ hasNextPage
438
+ }
439
+ }
440
+ }
441
+ }
442
+ `;
443
+ var ORGANIZATION_PROJECTS_PAGE_QUERY = `
444
+ query OrganizationProjectsPage($login: String!, $cursor: String) {
445
+ organization(login: $login) {
446
+ projectsV2(first: ${PROJECT_PAGE_SIZE}, after: $cursor) {
447
+ nodes {
448
+ id
449
+ title
450
+ shortDescription
451
+ url
452
+ items { totalCount }
453
+ }
454
+ pageInfo {
455
+ endCursor
456
+ hasNextPage
258
457
  }
259
458
  }
260
459
  }
@@ -615,11 +814,15 @@ function parseScopes(output) {
615
814
  }
616
815
 
617
816
  export {
817
+ findLinkedRepository,
618
818
  GitHubApiError,
619
819
  GitHubScopeError,
820
+ GitHubRepositoryLookupError,
620
821
  createClient,
822
+ getRepositoryMetadata,
621
823
  validateToken,
622
824
  checkRequiredScopes,
825
+ discoverUserProjects,
623
826
  listUserProjects,
624
827
  getProjectDetail,
625
828
  REQUIRED_GH_SCOPES,