@gh-symphony/cli 0.1.3 → 0.2.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.
@@ -1,1648 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- DEFAULT_WORKFLOW_LIFECYCLE
4
- } from "./chunk-WCOIVNHH.js";
5
- import {
6
- loadGlobalConfig,
7
- loadProjectConfig
8
- } from "./chunk-WOVNN5NW.js";
9
-
10
- // ../tracker-github/src/adapter.ts
11
- import { createHash } from "crypto";
12
- var DEFAULT_API_URL = "https://api.github.com/graphql";
13
- var DEFAULT_PAGE_SIZE = 25;
14
- var DEFAULT_NETWORK_TIMEOUT_MS = 3e4;
15
- var RATE_LIMIT_THRESHOLD = 100;
16
- var MAX_RATE_LIMIT_WAIT_MS = 6e4;
17
- var GitHubTrackerError = class extends Error {
18
- };
19
- var GitHubTrackerHttpError = class extends GitHubTrackerError {
20
- constructor(message, status, details) {
21
- super(message);
22
- this.status = status;
23
- this.details = details;
24
- }
25
- };
26
- var GitHubTrackerQueryError = class extends GitHubTrackerError {
27
- };
28
- var cachedGitHubGraphQLRateLimits = /* @__PURE__ */ new Map();
29
- function normalizeProjectItem(projectId, item, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
30
- if (item.content?.__typename !== "Issue" && item.content?.__typename !== "PullRequest") {
31
- return null;
32
- }
33
- const fieldValues = extractFieldValues(item.fieldValues?.nodes ?? []);
34
- const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
35
- if (item.content.__typename === "PullRequest") {
36
- return normalizePullRequestProjectItem(
37
- projectId,
38
- item,
39
- item.content,
40
- fieldValues,
41
- state,
42
- priority,
43
- rateLimits
44
- );
45
- }
46
- const repository = item.content.repository;
47
- const blockedBy = (item.content.blockedBy?.nodes ?? []).flatMap(
48
- (node) => node ? [
49
- {
50
- id: node.id,
51
- identifier: `${node.repository.owner.login}/${node.repository.name}#${node.number}`,
52
- state: normalizeBlockerState(node.state, lifecycle)
53
- }
54
- ] : []
55
- );
56
- const issueUpdatedAtMs = parseTimestampMs(item.content.updatedAt);
57
- const itemUpdatedAtMs = parseTimestampMs(item.updatedAt);
58
- const trackedUpdatedAt = itemUpdatedAtMs !== null && (issueUpdatedAtMs === null || itemUpdatedAtMs > issueUpdatedAtMs) ? item.updatedAt : item.content.updatedAt ?? item.updatedAt;
59
- const linkedPullRequests = normalizePullRequestNodes(
60
- item.content.closedByPullRequestsReferences?.nodes ?? []
61
- );
62
- const linkedPullRequestsTruncated = item.content.closedByPullRequestsReferences?.pageInfo?.hasNextPage ?? false;
63
- return {
64
- id: item.content.id,
65
- identifier: `${repository.owner.login}/${repository.name}#${item.content.number}`,
66
- number: item.content.number,
67
- title: item.content.title,
68
- description: item.content.body,
69
- priority: resolvePriority(item, priority),
70
- state,
71
- branchName: null,
72
- url: item.content.url,
73
- labels: normalizeLabelNames(item.content.labels?.nodes ?? []),
74
- blockedBy,
75
- createdAt: item.content.createdAt,
76
- updatedAt: trackedUpdatedAt,
77
- repository: {
78
- owner: repository.owner.login,
79
- name: repository.name,
80
- url: repository.url,
81
- cloneUrl: deriveCloneUrl(repository.url)
82
- },
83
- tracker: {
84
- adapter: "github-project",
85
- bindingId: projectId,
86
- itemId: item.id
87
- },
88
- metadata: withIssueMetadata(
89
- fieldValues,
90
- linkedPullRequests,
91
- linkedPullRequestsTruncated
92
- ),
93
- rateLimits
94
- };
95
- }
96
- function normalizePullRequestProjectItem(projectId, item, content, fieldValues, state, priority, rateLimits) {
97
- const pullRequest = normalizePullRequestNode(content);
98
- const itemUpdatedAtMs = parseTimestampMs(item.updatedAt);
99
- const pullRequestUpdatedAtMs = parseTimestampMs(content.updatedAt);
100
- const trackedUpdatedAt = itemUpdatedAtMs !== null && (pullRequestUpdatedAtMs === null || itemUpdatedAtMs > pullRequestUpdatedAtMs) ? item.updatedAt : content.updatedAt ?? item.updatedAt;
101
- return {
102
- id: content.id,
103
- identifier: pullRequest.identifier,
104
- number: content.number,
105
- title: content.title,
106
- description: content.body,
107
- priority: resolvePriority(item, priority),
108
- state,
109
- branchName: content.headRefName,
110
- url: content.url,
111
- labels: pullRequest.labels,
112
- blockedBy: [],
113
- createdAt: content.createdAt,
114
- updatedAt: trackedUpdatedAt,
115
- repository: pullRequest.repository,
116
- tracker: {
117
- adapter: "github-project",
118
- bindingId: projectId,
119
- itemId: item.id
120
- },
121
- metadata: withGitHubMetadata(fieldValues, {
122
- contentType: "PullRequest",
123
- pullRequest,
124
- linkedPullRequests: []
125
- }),
126
- rateLimits
127
- };
128
- }
129
- async function fetchProjectIssues(config, fetchImpl = fetch) {
130
- const issues = [];
131
- let cursor = null;
132
- const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(
133
- config,
134
- config.priorityFieldName,
135
- fetchImpl
136
- ) : void 0;
137
- const currentUserLogin = config.assignedOnly ? await fetchCurrentUserLogin(config, fetchImpl) : null;
138
- let excludedCount = 0;
139
- let latestRateLimits = null;
140
- do {
141
- const pageResult = await fetchProjectItemsPage(config, cursor, fetchImpl);
142
- const page = pageResult.page;
143
- latestRateLimits = pageResult.rateLimits ?? latestRateLimits;
144
- const pageIssues = (page.nodes ?? []).flatMap((item) => {
145
- if (!item) {
146
- return [];
147
- }
148
- const normalized = normalizeProjectItem(
149
- config.projectId,
150
- item,
151
- config.lifecycle,
152
- {
153
- fieldName: config.priorityFieldName,
154
- optionIds: priorityOptionIds
155
- },
156
- latestRateLimits
157
- );
158
- if (!normalized) {
159
- return [];
160
- }
161
- if (currentUserLogin && !isIssueAssignedToLogin(item, currentUserLogin)) {
162
- excludedCount += 1;
163
- return [];
164
- }
165
- return [normalized];
166
- });
167
- issues.push(...pageIssues);
168
- cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
169
- } while (cursor);
170
- if (currentUserLogin) {
171
- emitAssignedOnlyFilterEvent({
172
- projectId: config.projectId,
173
- currentUserLogin,
174
- includedCount: issues.length,
175
- excludedCount
176
- });
177
- }
178
- if (latestRateLimits) {
179
- for (const issue of issues) {
180
- issue.rateLimits = latestRateLimits;
181
- }
182
- }
183
- return issues;
184
- }
185
- async function fetchIssueStatesByIds(config, issueIds, fetchImpl = fetch) {
186
- if (issueIds.length === 0) {
187
- return [];
188
- }
189
- const issues = [];
190
- for (const issueIdBatch of chunkValues([...new Set(issueIds)], 100)) {
191
- const result = await executeGraphQLQueryWithMetadata(
192
- config,
193
- ISSUE_STATES_BY_IDS_QUERY,
194
- {
195
- issueIds: issueIdBatch
196
- },
197
- fetchImpl
198
- );
199
- const data = result.data;
200
- const rateLimits = result.rateLimits;
201
- for (const node of data.nodes ?? []) {
202
- const projectItem = await resolveIssueProjectItemForStateLookup(
203
- config,
204
- node,
205
- fetchImpl
206
- );
207
- const normalized = normalizeIssueStateLookupNode(
208
- config.projectId,
209
- node,
210
- projectItem,
211
- config.lifecycle,
212
- rateLimits
213
- );
214
- if (normalized) {
215
- issues.push(normalized);
216
- }
217
- }
218
- }
219
- return issues;
220
- }
221
- async function fetchProjectIssueByRepositoryAndNumber(config, repository, issueNumber, fetchImpl = fetch) {
222
- const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(
223
- config,
224
- config.priorityFieldName,
225
- fetchImpl
226
- ) : void 0;
227
- const result = await executeGraphQLQueryWithMetadata(
228
- config,
229
- REPOSITORY_ISSUE_QUERY,
230
- {
231
- owner: repository.owner,
232
- name: repository.name,
233
- issueNumber
234
- },
235
- fetchImpl
236
- );
237
- const issue = result.data.repository?.issue ?? null;
238
- if (!issue) {
239
- return null;
240
- }
241
- const projectItem = await resolveIssueProjectItemForStateLookup(
242
- config,
243
- issue,
244
- fetchImpl
245
- );
246
- if (!projectItem) {
247
- return null;
248
- }
249
- return normalizeRepositoryIssueLookup(
250
- config.projectId,
251
- issue,
252
- projectItem,
253
- config.lifecycle,
254
- {
255
- fieldName: config.priorityFieldName,
256
- optionIds: priorityOptionIds
257
- },
258
- result.rateLimits
259
- );
260
- }
261
- async function fetchProjectItemsPage(config, cursor, fetchImpl) {
262
- const result = await executeGraphQLQueryWithMetadata(
263
- config,
264
- PROJECT_ITEMS_QUERY,
265
- {
266
- projectId: config.projectId,
267
- cursor,
268
- pageSize: config.pageSize ?? DEFAULT_PAGE_SIZE
269
- },
270
- fetchImpl
271
- );
272
- const data = result.data;
273
- const items = data.node?.items;
274
- if (!items) {
275
- throw new GitHubTrackerQueryError(
276
- "GitHub GraphQL response did not include project items."
277
- );
278
- }
279
- return {
280
- page: items,
281
- rateLimits: result.rateLimits
282
- };
283
- }
284
- var fetchGithubProjectIssues = fetchProjectIssues;
285
- var fetchGithubIssueStatesByIds = fetchIssueStatesByIds;
286
- var fetchGithubProjectIssueByRepositoryAndNumber = fetchProjectIssueByRepositoryAndNumber;
287
- var upsertGithubIssueComment = upsertIssueComment;
288
- async function upsertIssueComment(config, issueId, input, fetchImpl = fetch) {
289
- const existingComment = await findIssueCommentByMarker(
290
- config,
291
- issueId,
292
- input.marker,
293
- fetchImpl
294
- );
295
- if (!existingComment) {
296
- await executeGraphQLQuery(
297
- config,
298
- ADD_ISSUE_COMMENT_MUTATION,
299
- {
300
- subjectId: issueId,
301
- body: input.body
302
- },
303
- fetchImpl
304
- );
305
- return "created";
306
- }
307
- if (existingComment.body === input.body) {
308
- return "unchanged";
309
- }
310
- await executeGraphQLQuery(
311
- config,
312
- UPDATE_ISSUE_COMMENT_MUTATION,
313
- {
314
- commentId: existingComment.id,
315
- body: input.body
316
- },
317
- fetchImpl
318
- );
319
- return "updated";
320
- }
321
- async function findIssueCommentByMarker(config, issueId, marker, fetchImpl) {
322
- let cursor = null;
323
- while (true) {
324
- const data = await executeGraphQLQuery(
325
- config,
326
- ISSUE_COMMENTS_BY_ID_QUERY,
327
- {
328
- issueId,
329
- cursor
330
- },
331
- fetchImpl
332
- );
333
- if (data.node?.__typename !== "Issue") {
334
- throw new GitHubTrackerQueryError(
335
- "GitHub GraphQL response did not include issue comments."
336
- );
337
- }
338
- const issueNode = data.node;
339
- const match = issueNode.comments.nodes?.find(
340
- (comment) => comment?.body.includes(marker)
341
- ) ?? null;
342
- if (match) {
343
- return match;
344
- }
345
- if (!issueNode.comments.pageInfo.hasNextPage) {
346
- return null;
347
- }
348
- cursor = issueNode.comments.pageInfo.endCursor;
349
- }
350
- }
351
- async function fetchCurrentUserLogin(config, fetchImpl) {
352
- const response = await fetchImpl(resolveRestUserApiUrl(config.apiUrl), {
353
- method: "GET",
354
- headers: {
355
- authorization: `Bearer ${config.token}`,
356
- "user-agent": "gh-symphony",
357
- accept: "application/vnd.github+json"
358
- },
359
- signal: buildRequestSignal(config.timeoutMs)
360
- });
361
- if (!response.ok) {
362
- const details = await response.text();
363
- throw new GitHubTrackerHttpError(
364
- `GitHub REST request failed with status ${response.status}`,
365
- response.status,
366
- details
367
- );
368
- }
369
- const payload = await response.json();
370
- if (!payload.login) {
371
- throw new GitHubTrackerQueryError(
372
- "GitHub REST response did not include the authenticated user login."
373
- );
374
- }
375
- return payload.login;
376
- }
377
- function isIssueAssignedToLogin(item, login) {
378
- if (item.content?.__typename !== "Issue" && item.content?.__typename !== "PullRequest") {
379
- return false;
380
- }
381
- return (item.content.assignees?.nodes ?? []).some(
382
- (assignee) => assignee?.login === login
383
- );
384
- }
385
- function emitAssignedOnlyFilterEvent(input) {
386
- console.info(
387
- JSON.stringify({
388
- event: "tracker-assigned-only-filtered",
389
- projectId: input.projectId,
390
- currentUserLogin: input.currentUserLogin,
391
- includedCount: input.includedCount,
392
- excludedCount: input.excludedCount
393
- })
394
- );
395
- }
396
- function extractFieldValues(nodes) {
397
- return nodes.reduce((values, node) => {
398
- const fieldName = node?.field?.name;
399
- if (!fieldName) {
400
- return values;
401
- }
402
- if (node.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.name) {
403
- values[fieldName] = node.name;
404
- }
405
- if (node.__typename === "ProjectV2ItemFieldTextValue" && node.text) {
406
- values[fieldName] = node.text;
407
- }
408
- return values;
409
- }, {});
410
- }
411
- function normalizeIssueStateLookupNode(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, rateLimits = null) {
412
- if (issue?.__typename !== "Issue" && issue?.__typename !== "PullRequest") {
413
- return null;
414
- }
415
- if (!projectItem) {
416
- return null;
417
- }
418
- const fieldValues = extractFieldValues(projectItem.fieldValues?.nodes ?? []);
419
- const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
420
- const repository = issue.repository;
421
- const identifier = `${repository.owner.login}/${repository.name}#${issue.number}`;
422
- const url = issue.url ?? `${repository.url}/${issue.__typename === "PullRequest" ? "pull" : "issues"}/${issue.number}`;
423
- return {
424
- id: issue.id,
425
- identifier,
426
- number: issue.number,
427
- title: identifier,
428
- description: null,
429
- priority: null,
430
- state,
431
- branchName: issue.__typename === "PullRequest" ? issue.headRefName ?? null : null,
432
- url,
433
- labels: [],
434
- blockedBy: [],
435
- createdAt: null,
436
- updatedAt: projectItem.updatedAt ?? issue.updatedAt,
437
- repository: {
438
- owner: repository.owner.login,
439
- name: repository.name,
440
- url: repository.url,
441
- cloneUrl: deriveCloneUrl(repository.url)
442
- },
443
- tracker: {
444
- adapter: "github-project",
445
- bindingId: projectId,
446
- itemId: projectItem.id
447
- },
448
- metadata: issue.__typename === "PullRequest" ? withGitHubMetadata(fieldValues, { contentType: "PullRequest" }) : fieldValues,
449
- rateLimits
450
- };
451
- }
452
- function normalizeRepositoryIssueLookup(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
453
- if (!issue || !projectItem) {
454
- return null;
455
- }
456
- return normalizeProjectItem(
457
- projectId,
458
- {
459
- id: projectItem.id,
460
- updatedAt: projectItem.updatedAt,
461
- fieldValues: projectItem.fieldValues,
462
- content: issue
463
- },
464
- lifecycle,
465
- priority,
466
- rateLimits
467
- );
468
- }
469
- function normalizePullRequestNodes(nodes) {
470
- return nodes.flatMap(
471
- (node) => node ? [normalizePullRequestNode(node)] : []
472
- );
473
- }
474
- function normalizePullRequestNode(node) {
475
- const repository = normalizeRepositoryRef(node.repository);
476
- return {
477
- id: node.id,
478
- number: node.number,
479
- identifier: `${repository.owner}/${repository.name}#${node.number}`,
480
- title: node.title,
481
- body: node.body,
482
- url: node.url,
483
- state: node.state,
484
- isDraft: node.isDraft,
485
- merged: node.merged,
486
- headRefName: node.headRefName,
487
- baseRefName: node.baseRefName,
488
- headRepository: node.headRepository ? normalizeRepositoryRef(node.headRepository) : null,
489
- repository,
490
- labels: normalizeLabelNames(node.labels?.nodes ?? []),
491
- assignees: normalizeAssigneeLogins(node.assignees?.nodes ?? []),
492
- createdAt: node.createdAt,
493
- updatedAt: node.updatedAt
494
- };
495
- }
496
- function normalizeRepositoryRef(repository) {
497
- return {
498
- owner: repository.owner.login,
499
- name: repository.name,
500
- url: repository.url,
501
- cloneUrl: deriveCloneUrl(repository.url)
502
- };
503
- }
504
- function normalizeLabelNames(nodes) {
505
- return nodes.flatMap((label) => label?.name ? [label.name.toLowerCase()] : []).sort();
506
- }
507
- function normalizeAssigneeLogins(nodes) {
508
- return nodes.flatMap((assignee) => assignee?.login ? [assignee.login] : []);
509
- }
510
- function withGitHubMetadata(fieldValues, metadata) {
511
- return {
512
- ...fieldValues,
513
- ...metadata
514
- };
515
- }
516
- function withIssueMetadata(fieldValues, linkedPullRequests, linkedPullRequestsTruncated = false) {
517
- if (linkedPullRequests.length === 0 && !linkedPullRequestsTruncated) {
518
- return fieldValues;
519
- }
520
- return withGitHubMetadata(fieldValues, {
521
- linkedPullRequests,
522
- linkedPullRequestsTruncated
523
- });
524
- }
525
- async function resolveIssueProjectItemForStateLookup(config, issue, fetchImpl) {
526
- if (issue?.__typename !== "Issue" && issue?.__typename !== "PullRequest") {
527
- return null;
528
- }
529
- let connection = issue.projectItems;
530
- let projectItem = findProjectItemByProjectId(
531
- connection?.nodes ?? [],
532
- config.projectId
533
- );
534
- let cursor = connection?.pageInfo.endCursor ?? null;
535
- while (!projectItem && connection?.pageInfo.hasNextPage) {
536
- const nextPage = await fetchIssueProjectItemsPage(
537
- config,
538
- issue.id,
539
- cursor,
540
- fetchImpl
541
- );
542
- projectItem = findProjectItemByProjectId(
543
- nextPage.nodes ?? [],
544
- config.projectId
545
- );
546
- connection = nextPage;
547
- cursor = nextPage.pageInfo.endCursor;
548
- }
549
- return projectItem;
550
- }
551
- async function fetchIssueProjectItemsPage(config, issueId, cursor, fetchImpl) {
552
- const result = await executeGraphQLQueryWithMetadata(
553
- config,
554
- ISSUE_PROJECT_ITEMS_PAGE_QUERY,
555
- {
556
- issueId,
557
- cursor
558
- },
559
- fetchImpl
560
- );
561
- const data = result.data;
562
- const issue = data.node;
563
- if (issue?.__typename !== "Issue" && issue?.__typename !== "PullRequest" || !issue.projectItems) {
564
- throw new GitHubTrackerQueryError(
565
- "GitHub GraphQL response did not include issue project items."
566
- );
567
- }
568
- return issue.projectItems;
569
- }
570
- function findProjectItemByProjectId(nodes, projectId) {
571
- return nodes.find((item) => item?.project?.id === projectId) ?? null;
572
- }
573
- function resolvePriority(item, priority) {
574
- if (!priority.fieldName || !priority.optionIds) {
575
- return null;
576
- }
577
- for (const node of item.fieldValues?.nodes ?? []) {
578
- if (node?.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.field?.name === priority.fieldName && node.optionId) {
579
- return priority.optionIds[node.optionId] ?? null;
580
- }
581
- }
582
- return null;
583
- }
584
- function extractPriorityOptionOrder(fields, priorityFieldName) {
585
- for (const field of fields) {
586
- if (isSingleSelectProjectField(field) && field.name === priorityFieldName) {
587
- let nextPriority = 0;
588
- const optionEntries = (field.options ?? []).flatMap((option) => {
589
- if (!option?.id) {
590
- return [];
591
- }
592
- const entry = [option.id, nextPriority];
593
- nextPriority += 1;
594
- return [entry];
595
- });
596
- return Object.fromEntries(optionEntries);
597
- }
598
- }
599
- return void 0;
600
- }
601
- async function fetchPriorityOptionOrder(config, priorityFieldName, fetchImpl) {
602
- const data = await executeGraphQLQuery(
603
- config,
604
- PROJECT_FIELDS_QUERY,
605
- { projectId: config.projectId },
606
- fetchImpl
607
- );
608
- return extractPriorityOptionOrder(
609
- data.node?.fields?.nodes ?? [],
610
- priorityFieldName
611
- );
612
- }
613
- function isSingleSelectProjectField(field) {
614
- return field?.__typename === "ProjectV2SingleSelectField";
615
- }
616
- function deriveCloneUrl(repositoryUrl) {
617
- if (repositoryUrl.startsWith("file://") || repositoryUrl.endsWith(".git")) {
618
- return repositoryUrl;
619
- }
620
- return `${repositoryUrl}.git`;
621
- }
622
- function normalizeBlockerState(state, lifecycle) {
623
- if (!state) {
624
- return null;
625
- }
626
- const normalized = state.trim().toLowerCase();
627
- if (normalized === "closed") {
628
- return lifecycle.terminalStates[0] ?? state;
629
- }
630
- if (normalized === "open") {
631
- return null;
632
- }
633
- return state;
634
- }
635
- function resolveRestUserApiUrl(apiUrl) {
636
- const parsed = new URL(apiUrl ?? DEFAULT_API_URL);
637
- const pathSegments = parsed.pathname.split("/").filter(Boolean);
638
- if (pathSegments.at(-1) === "graphql") {
639
- pathSegments.pop();
640
- }
641
- parsed.pathname = `/${pathSegments.join("/")}/user`.replace(/\/{2,}/g, "/");
642
- parsed.search = "";
643
- parsed.hash = "";
644
- return parsed.toString();
645
- }
646
- function chunkValues(values, size) {
647
- const chunks = [];
648
- for (let index = 0; index < values.length; index += size) {
649
- chunks.push(values.slice(index, index + size));
650
- }
651
- return chunks;
652
- }
653
- function buildRequestSignal(timeoutMs) {
654
- return AbortSignal.timeout(resolveNetworkTimeoutMs(timeoutMs));
655
- }
656
- function resolveNetworkTimeoutMs(timeoutMs) {
657
- if (timeoutMs !== void 0 && Number.isInteger(timeoutMs) && timeoutMs > 0) {
658
- return timeoutMs;
659
- }
660
- return DEFAULT_NETWORK_TIMEOUT_MS;
661
- }
662
- async function executeGraphQLQuery(config, query, variables, fetchImpl) {
663
- const result = await executeGraphQLQueryWithMetadata(
664
- config,
665
- query,
666
- variables,
667
- fetchImpl
668
- );
669
- return result.data;
670
- }
671
- async function executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl) {
672
- const tokenFingerprint = fingerprintToken(config.token);
673
- await guardGraphQLRateLimit(tokenFingerprint);
674
- const response = await fetchImpl(config.apiUrl ?? DEFAULT_API_URL, {
675
- method: "POST",
676
- headers: {
677
- "content-type": "application/json",
678
- authorization: `Bearer ${config.token}`
679
- },
680
- body: JSON.stringify({
681
- query,
682
- variables
683
- }),
684
- signal: buildRequestSignal(config.timeoutMs)
685
- });
686
- if (!response.ok) {
687
- const details = await response.text();
688
- throw new GitHubTrackerHttpError(
689
- `GitHub GraphQL request failed with status ${response.status}`,
690
- response.status,
691
- details
692
- );
693
- }
694
- const payload = await response.json();
695
- if (payload.errors?.length) {
696
- throw new GitHubTrackerQueryError(
697
- payload.errors.map((error) => error.message).join("; ")
698
- );
699
- }
700
- if (!payload.data) {
701
- throw new GitHubTrackerQueryError(
702
- "GitHub GraphQL response did not include data."
703
- );
704
- }
705
- const data = payload.data;
706
- const rateLimits = extractGitHubRateLimits(response.headers);
707
- cachedGitHubGraphQLRateLimits.set(tokenFingerprint, rateLimits);
708
- return {
709
- data,
710
- rateLimits
711
- };
712
- }
713
- async function guardGraphQLRateLimit(tokenFingerprint) {
714
- const rateLimit = cachedGitHubGraphQLRateLimits.get(tokenFingerprint) ?? null;
715
- if (!rateLimit) {
716
- return;
717
- }
718
- const remaining = rateLimit.remaining;
719
- if (remaining === null || remaining > RATE_LIMIT_THRESHOLD) {
720
- return;
721
- }
722
- const resetAtMs = parseTimestampMs(rateLimit.resetAt);
723
- if (resetAtMs === null) {
724
- throw new GitHubTrackerError("Rate limit near exhaustion");
725
- }
726
- const waitMs = Math.max(0, resetAtMs - Date.now());
727
- if (waitMs > MAX_RATE_LIMIT_WAIT_MS) {
728
- throw new GitHubTrackerError("Rate limit near exhaustion");
729
- }
730
- cachedGitHubGraphQLRateLimits.delete(tokenFingerprint);
731
- if (waitMs > 0) {
732
- await sleep(waitMs);
733
- }
734
- }
735
- function fingerprintToken(token) {
736
- return createHash("sha256").update(token).digest("hex");
737
- }
738
- function extractGitHubRateLimits(headers) {
739
- if (!headers || typeof headers.get !== "function") {
740
- return null;
741
- }
742
- const limit = parseIntegerHeader(headers.get("x-ratelimit-limit"));
743
- const remaining = parseIntegerHeader(headers.get("x-ratelimit-remaining"));
744
- const used = parseIntegerHeader(headers.get("x-ratelimit-used"));
745
- const reset = parseIntegerHeader(headers.get("x-ratelimit-reset"));
746
- const resource = headers.get("x-ratelimit-resource");
747
- if (limit === null && remaining === null && used === null && reset === null && resource === null) {
748
- return null;
749
- }
750
- return {
751
- source: "github",
752
- limit,
753
- remaining,
754
- used,
755
- reset,
756
- resetAt: reset === null ? null : new Date(reset * 1e3).toISOString(),
757
- resource
758
- };
759
- }
760
- function parseIntegerHeader(value) {
761
- if (value === null) {
762
- return null;
763
- }
764
- const parsed = Number.parseInt(value, 10);
765
- return Number.isFinite(parsed) ? parsed : null;
766
- }
767
- function parseTimestampMs(value) {
768
- if (!value) {
769
- return null;
770
- }
771
- const timestampMs = Date.parse(value);
772
- return Number.isFinite(timestampMs) ? timestampMs : null;
773
- }
774
- function sleep(ms) {
775
- return new Promise((resolve) => {
776
- setTimeout(resolve, ms);
777
- });
778
- }
779
- var PROJECT_ITEMS_QUERY = `
780
- query ProjectItems($projectId: ID!, $cursor: String, $pageSize: Int!) {
781
- node(id: $projectId) {
782
- __typename
783
- ... on ProjectV2 {
784
- items(first: $pageSize, after: $cursor) {
785
- nodes {
786
- id
787
- updatedAt
788
- fieldValues(first: 20) {
789
- nodes {
790
- __typename
791
- ... on ProjectV2ItemFieldSingleSelectValue {
792
- name
793
- optionId
794
- field {
795
- ... on ProjectV2SingleSelectField {
796
- name
797
- }
798
- }
799
- }
800
- ... on ProjectV2ItemFieldTextValue {
801
- text
802
- field {
803
- ... on ProjectV2FieldCommon {
804
- name
805
- }
806
- }
807
- }
808
- }
809
- }
810
- content {
811
- __typename
812
- ... on Issue {
813
- id
814
- number
815
- title
816
- body
817
- url
818
- createdAt
819
- updatedAt
820
- labels(first: 20) {
821
- nodes {
822
- name
823
- }
824
- }
825
- assignees(first: 20) {
826
- nodes {
827
- login
828
- }
829
- }
830
- repository {
831
- name
832
- url
833
- owner {
834
- login
835
- }
836
- }
837
- blockedBy(first: 100) {
838
- nodes {
839
- id
840
- number
841
- state
842
- repository {
843
- name
844
- owner {
845
- login
846
- }
847
- }
848
- }
849
- }
850
- closedByPullRequestsReferences(first: 20) {
851
- nodes {
852
- ...PullRequestMetadata
853
- }
854
- pageInfo {
855
- hasNextPage
856
- }
857
- }
858
- }
859
- ... on PullRequest {
860
- ...PullRequestMetadata
861
- }
862
- }
863
- }
864
- pageInfo {
865
- endCursor
866
- hasNextPage
867
- }
868
- }
869
- }
870
- }
871
- }
872
-
873
- fragment PullRequestMetadata on PullRequest {
874
- id
875
- number
876
- title
877
- body
878
- url
879
- state
880
- isDraft
881
- merged
882
- headRefName
883
- baseRefName
884
- headRepository {
885
- name
886
- url
887
- owner {
888
- login
889
- }
890
- }
891
- repository {
892
- name
893
- url
894
- owner {
895
- login
896
- }
897
- }
898
- labels(first: 20) {
899
- nodes {
900
- name
901
- }
902
- }
903
- assignees(first: 20) {
904
- nodes {
905
- login
906
- }
907
- }
908
- createdAt
909
- updatedAt
910
- }
911
- `;
912
- var PROJECT_FIELDS_QUERY = `
913
- query ProjectFields($projectId: ID!) {
914
- node(id: $projectId) {
915
- __typename
916
- ... on ProjectV2 {
917
- fields(first: 100) {
918
- nodes {
919
- __typename
920
- ... on ProjectV2SingleSelectField {
921
- name
922
- options {
923
- id
924
- name
925
- }
926
- }
927
- }
928
- }
929
- }
930
- }
931
- }
932
- `;
933
- var ISSUE_STATES_BY_IDS_QUERY = `
934
- query IssueStatesByIds($issueIds: [ID!]!) {
935
- nodes(ids: $issueIds) {
936
- __typename
937
- ... on Issue {
938
- id
939
- number
940
- url
941
- updatedAt
942
- repository {
943
- name
944
- url
945
- owner {
946
- login
947
- }
948
- }
949
- projectItems(first: 100, includeArchived: false) {
950
- nodes {
951
- id
952
- updatedAt
953
- project {
954
- id
955
- }
956
- fieldValues(first: 20) {
957
- nodes {
958
- __typename
959
- ... on ProjectV2ItemFieldSingleSelectValue {
960
- name
961
- optionId
962
- field {
963
- ... on ProjectV2SingleSelectField {
964
- name
965
- }
966
- }
967
- }
968
- ... on ProjectV2ItemFieldTextValue {
969
- text
970
- field {
971
- ... on ProjectV2FieldCommon {
972
- name
973
- }
974
- }
975
- }
976
- }
977
- }
978
- }
979
- pageInfo {
980
- endCursor
981
- hasNextPage
982
- }
983
- }
984
- }
985
- ... on PullRequest {
986
- id
987
- number
988
- url
989
- updatedAt
990
- headRefName
991
- repository {
992
- name
993
- url
994
- owner {
995
- login
996
- }
997
- }
998
- projectItems(first: 100, includeArchived: false) {
999
- nodes {
1000
- id
1001
- updatedAt
1002
- project {
1003
- id
1004
- }
1005
- fieldValues(first: 20) {
1006
- nodes {
1007
- __typename
1008
- ... on ProjectV2ItemFieldSingleSelectValue {
1009
- name
1010
- optionId
1011
- field {
1012
- ... on ProjectV2SingleSelectField {
1013
- name
1014
- }
1015
- }
1016
- }
1017
- ... on ProjectV2ItemFieldTextValue {
1018
- text
1019
- field {
1020
- ... on ProjectV2FieldCommon {
1021
- name
1022
- }
1023
- }
1024
- }
1025
- }
1026
- }
1027
- }
1028
- pageInfo {
1029
- endCursor
1030
- hasNextPage
1031
- }
1032
- }
1033
- }
1034
- }
1035
- }
1036
- `;
1037
- var ISSUE_PROJECT_ITEMS_PAGE_QUERY = `
1038
- query IssueProjectItemsPage($issueId: ID!, $cursor: String) {
1039
- node(id: $issueId) {
1040
- __typename
1041
- ... on Issue {
1042
- id
1043
- number
1044
- url
1045
- updatedAt
1046
- repository {
1047
- name
1048
- url
1049
- owner {
1050
- login
1051
- }
1052
- }
1053
- projectItems(first: 100, after: $cursor, includeArchived: false) {
1054
- nodes {
1055
- id
1056
- updatedAt
1057
- project {
1058
- id
1059
- }
1060
- fieldValues(first: 20) {
1061
- nodes {
1062
- __typename
1063
- ... on ProjectV2ItemFieldSingleSelectValue {
1064
- name
1065
- optionId
1066
- field {
1067
- ... on ProjectV2SingleSelectField {
1068
- name
1069
- }
1070
- }
1071
- }
1072
- ... on ProjectV2ItemFieldTextValue {
1073
- text
1074
- field {
1075
- ... on ProjectV2FieldCommon {
1076
- name
1077
- }
1078
- }
1079
- }
1080
- }
1081
- }
1082
- }
1083
- pageInfo {
1084
- endCursor
1085
- hasNextPage
1086
- }
1087
- }
1088
- }
1089
- ... on PullRequest {
1090
- id
1091
- number
1092
- url
1093
- updatedAt
1094
- headRefName
1095
- repository {
1096
- name
1097
- url
1098
- owner {
1099
- login
1100
- }
1101
- }
1102
- projectItems(first: 100, after: $cursor, includeArchived: false) {
1103
- nodes {
1104
- id
1105
- updatedAt
1106
- project {
1107
- id
1108
- }
1109
- fieldValues(first: 20) {
1110
- nodes {
1111
- __typename
1112
- ... on ProjectV2ItemFieldSingleSelectValue {
1113
- name
1114
- optionId
1115
- field {
1116
- ... on ProjectV2SingleSelectField {
1117
- name
1118
- }
1119
- }
1120
- }
1121
- ... on ProjectV2ItemFieldTextValue {
1122
- text
1123
- field {
1124
- ... on ProjectV2FieldCommon {
1125
- name
1126
- }
1127
- }
1128
- }
1129
- }
1130
- }
1131
- }
1132
- pageInfo {
1133
- endCursor
1134
- hasNextPage
1135
- }
1136
- }
1137
- }
1138
- }
1139
- }
1140
- `;
1141
- var ISSUE_COMMENTS_BY_ID_QUERY = `
1142
- query IssueCommentsById($issueId: ID!, $cursor: String) {
1143
- node(id: $issueId) {
1144
- __typename
1145
- ... on Issue {
1146
- comments(first: 100, after: $cursor) {
1147
- nodes {
1148
- id
1149
- body
1150
- }
1151
- pageInfo {
1152
- endCursor
1153
- hasNextPage
1154
- }
1155
- }
1156
- }
1157
- }
1158
- }
1159
- `;
1160
- var ADD_ISSUE_COMMENT_MUTATION = `
1161
- mutation AddIssueComment($subjectId: ID!, $body: String!) {
1162
- addComment(input: { subjectId: $subjectId, body: $body }) {
1163
- commentEdge {
1164
- node {
1165
- id
1166
- body
1167
- }
1168
- }
1169
- }
1170
- }
1171
- `;
1172
- var UPDATE_ISSUE_COMMENT_MUTATION = `
1173
- mutation UpdateIssueComment($commentId: ID!, $body: String!) {
1174
- updateIssueComment(input: { id: $commentId, body: $body }) {
1175
- issueComment {
1176
- id
1177
- body
1178
- }
1179
- }
1180
- }
1181
- `;
1182
- var REPOSITORY_ISSUE_QUERY = `
1183
- query RepositoryIssue(
1184
- $owner: String!
1185
- $name: String!
1186
- $issueNumber: Int!
1187
- ) {
1188
- repository(owner: $owner, name: $name) {
1189
- issue(number: $issueNumber) {
1190
- __typename
1191
- id
1192
- number
1193
- title
1194
- body
1195
- url
1196
- createdAt
1197
- updatedAt
1198
- labels(first: 20) {
1199
- nodes {
1200
- name
1201
- }
1202
- }
1203
- assignees(first: 20) {
1204
- nodes {
1205
- login
1206
- }
1207
- }
1208
- repository {
1209
- name
1210
- url
1211
- owner {
1212
- login
1213
- }
1214
- }
1215
- blockedBy(first: 100) {
1216
- nodes {
1217
- id
1218
- number
1219
- state
1220
- repository {
1221
- name
1222
- owner {
1223
- login
1224
- }
1225
- }
1226
- }
1227
- }
1228
- closedByPullRequestsReferences(first: 20) {
1229
- nodes {
1230
- ...RepositoryIssuePullRequestMetadata
1231
- }
1232
- pageInfo {
1233
- hasNextPage
1234
- }
1235
- }
1236
- projectItems(first: 20, includeArchived: false) {
1237
- nodes {
1238
- id
1239
- updatedAt
1240
- project {
1241
- id
1242
- }
1243
- fieldValues(first: 20) {
1244
- nodes {
1245
- __typename
1246
- ... on ProjectV2ItemFieldSingleSelectValue {
1247
- name
1248
- optionId
1249
- field {
1250
- ... on ProjectV2SingleSelectField {
1251
- name
1252
- }
1253
- }
1254
- }
1255
- ... on ProjectV2ItemFieldTextValue {
1256
- text
1257
- field {
1258
- ... on ProjectV2FieldCommon {
1259
- name
1260
- }
1261
- }
1262
- }
1263
- }
1264
- }
1265
- }
1266
- pageInfo {
1267
- endCursor
1268
- hasNextPage
1269
- }
1270
- }
1271
- }
1272
- }
1273
- }
1274
-
1275
- fragment RepositoryIssuePullRequestMetadata on PullRequest {
1276
- id
1277
- number
1278
- title
1279
- body
1280
- url
1281
- state
1282
- isDraft
1283
- merged
1284
- headRefName
1285
- baseRefName
1286
- headRepository {
1287
- name
1288
- url
1289
- owner {
1290
- login
1291
- }
1292
- }
1293
- repository {
1294
- name
1295
- url
1296
- owner {
1297
- login
1298
- }
1299
- }
1300
- labels(first: 20) {
1301
- nodes {
1302
- name
1303
- }
1304
- }
1305
- assignees(first: 20) {
1306
- nodes {
1307
- login
1308
- }
1309
- }
1310
- createdAt
1311
- updatedAt
1312
- }
1313
- `;
1314
-
1315
- // ../tracker-github/src/orchestrator-adapter.ts
1316
- import { createHash as createHash2 } from "crypto";
1317
- var githubProjectTrackerAdapter = {
1318
- async listIssues(project, dependencies = {}) {
1319
- return listProjectIssues(project, dependencies);
1320
- },
1321
- async listIssuesByStates(project, states, dependencies = {}) {
1322
- if (states.length === 0) {
1323
- return [];
1324
- }
1325
- const issues = await listProjectIssues(project, dependencies);
1326
- const normalizedStates = new Set(
1327
- states.map((state) => state.trim().toLowerCase())
1328
- );
1329
- return issues.filter(
1330
- (issue) => normalizedStates.has(issue.state.trim().toLowerCase())
1331
- );
1332
- },
1333
- async fetchIssueStatesByIds(project, issueIds, dependencies = {}) {
1334
- if (issueIds.length === 0) {
1335
- return [];
1336
- }
1337
- return fetchProjectIssueStatesByIds(project, issueIds, dependencies);
1338
- },
1339
- buildWorkerEnvironment(project) {
1340
- return {
1341
- GITHUB_PROJECT_ID: requireTrackerSetting(project.tracker, "projectId")
1342
- };
1343
- },
1344
- reviveIssue(project, run) {
1345
- return {
1346
- id: run.issueId,
1347
- identifier: run.issueIdentifier,
1348
- number: parseIssueNumber(run.issueIdentifier),
1349
- title: run.issueTitle ?? run.issueIdentifier,
1350
- description: null,
1351
- priority: null,
1352
- state: run.issueState,
1353
- branchName: null,
1354
- url: null,
1355
- labels: [],
1356
- blockedBy: [],
1357
- createdAt: null,
1358
- updatedAt: null,
1359
- repository: run.repository,
1360
- tracker: {
1361
- adapter: "github-project",
1362
- bindingId: project.tracker.bindingId,
1363
- itemId: run.issueId
1364
- },
1365
- metadata: {}
1366
- };
1367
- },
1368
- async upsertIssueComment(project, issue, input, dependencies = {}) {
1369
- const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
1370
- return upsertGithubIssueComment(
1371
- trackerConfig,
1372
- issue.id,
1373
- input,
1374
- dependencies.fetchImpl
1375
- );
1376
- }
1377
- };
1378
- async function findGithubProjectIssue(project, identifier, dependencies = {}) {
1379
- const parsed = parseIssueIdentifier(identifier);
1380
- if (!parsed) {
1381
- return null;
1382
- }
1383
- const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
1384
- return fetchGithubProjectIssueByRepositoryAndNumber(
1385
- trackerConfig,
1386
- { owner: parsed.owner, name: parsed.name },
1387
- parsed.number,
1388
- dependencies.fetchImpl
1389
- );
1390
- }
1391
- async function listProjectIssues(project, dependencies = {}) {
1392
- const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
1393
- const loadProjectIssues = () => fetchGithubProjectIssues(trackerConfig, dependencies.fetchImpl);
1394
- return dependencies.projectItemsCache?.getOrLoad(
1395
- buildProjectItemsCacheKey(trackerConfig, dependencies),
1396
- loadProjectIssues
1397
- ) ?? loadProjectIssues();
1398
- }
1399
- async function fetchProjectIssueStatesByIds(project, issueIds, dependencies = {}) {
1400
- const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
1401
- return fetchGithubIssueStatesByIds(
1402
- trackerConfig,
1403
- [...issueIds],
1404
- dependencies.fetchImpl
1405
- );
1406
- }
1407
- function resolveGitHubTrackerConfig(project, dependencies = {}) {
1408
- const token = dependencies.token ?? process.env.GITHUB_GRAPHQL_TOKEN;
1409
- if (!token) {
1410
- throw new Error(
1411
- "GITHUB_GRAPHQL_TOKEN environment variable is required. Run 'gh auth token' or set the variable."
1412
- );
1413
- }
1414
- const githubProjectId = requireTrackerSetting(project.tracker, "projectId");
1415
- return {
1416
- projectId: githubProjectId,
1417
- token,
1418
- apiUrl: project.tracker.apiUrl,
1419
- assignedOnly: readBooleanTrackerSetting(project.tracker, "assignedOnly"),
1420
- priorityFieldName: readOptionalStringTrackerSetting(
1421
- project.tracker,
1422
- "priorityFieldName"
1423
- ),
1424
- timeoutMs: readNumberTrackerSetting(project.tracker, "timeoutMs")
1425
- };
1426
- }
1427
- function buildProjectItemsCacheKey(config, _dependencies) {
1428
- return JSON.stringify({
1429
- adapter: "github-project",
1430
- apiUrl: config.apiUrl,
1431
- assignedOnly: config.assignedOnly ?? false,
1432
- priorityFieldName: config.priorityFieldName ?? null,
1433
- projectId: config.projectId,
1434
- timeoutMs: config.timeoutMs,
1435
- tokenFingerprint: hashToken(config.token)
1436
- });
1437
- }
1438
- function hashToken(token) {
1439
- if (!token) {
1440
- return null;
1441
- }
1442
- return createHash2("sha256").update(token).digest("hex");
1443
- }
1444
- var trackerAdapters = {
1445
- "github-project": githubProjectTrackerAdapter
1446
- };
1447
- function resolveTrackerAdapter(tracker) {
1448
- const adapter = trackerAdapters[tracker.adapter];
1449
- if (!adapter) {
1450
- throw new Error(`Unsupported tracker adapter: ${tracker.adapter}`);
1451
- }
1452
- return adapter;
1453
- }
1454
- function requireTrackerSetting(tracker, key) {
1455
- const value = tracker.settings?.[key];
1456
- if (typeof value !== "string" || value.length === 0) {
1457
- throw new Error(
1458
- `Tracker adapter "${tracker.adapter}" requires the "${key}" setting.`
1459
- );
1460
- }
1461
- return value;
1462
- }
1463
- function readBooleanTrackerSetting(tracker, key) {
1464
- const value = tracker.settings?.[key];
1465
- return value === true || value === "true";
1466
- }
1467
- function readNumberTrackerSetting(tracker, key) {
1468
- const value = tracker.settings?.[key];
1469
- if (value === void 0) {
1470
- return void 0;
1471
- }
1472
- if (typeof value === "number" && Number.isInteger(value) && value > 0) {
1473
- return value;
1474
- }
1475
- if (typeof value === "string") {
1476
- const parsed = Number(value);
1477
- if (Number.isInteger(parsed) && parsed > 0) {
1478
- return parsed;
1479
- }
1480
- }
1481
- throw new Error(
1482
- `Tracker adapter "${tracker.adapter}" requires the "${key}" setting to be a positive integer when provided.`
1483
- );
1484
- }
1485
- function readOptionalStringTrackerSetting(tracker, key) {
1486
- const value = tracker.settings?.[key];
1487
- return typeof value === "string" && value.length > 0 ? value : void 0;
1488
- }
1489
- function parseIssueNumber(identifier) {
1490
- const match = identifier.match(/#(\d+)$/);
1491
- return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
1492
- }
1493
- function parseIssueIdentifier(identifier) {
1494
- const match = identifier.match(/^([^/\s#]+)\/([^/\s#]+)#(\d+)$/);
1495
- if (!match) {
1496
- return null;
1497
- }
1498
- return {
1499
- owner: match[1],
1500
- name: match[2],
1501
- number: Number.parseInt(match[3], 10)
1502
- };
1503
- }
1504
-
1505
- // src/project-selection.ts
1506
- import * as p from "@clack/prompts";
1507
- function isInteractiveTerminal() {
1508
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
1509
- }
1510
- function explicitProjectRequiredMessage() {
1511
- return "Multiple repository runtime configs are present. Run 'gh-symphony repo init' from the target repository to refresh the cwd runtime.\n";
1512
- }
1513
- async function inspectManagedProjectSelection(input) {
1514
- if (input.requestedProjectId) {
1515
- const projectConfig = await loadProjectConfig(
1516
- input.configDir,
1517
- input.requestedProjectId
1518
- );
1519
- if (!projectConfig) {
1520
- return {
1521
- kind: "requested_project_missing",
1522
- projectId: input.requestedProjectId,
1523
- message: `Project "${input.requestedProjectId}" is not configured. Run 'gh-symphony repo init' from the target repository.`
1524
- };
1525
- }
1526
- return {
1527
- kind: "resolved",
1528
- projectId: input.requestedProjectId,
1529
- projectConfig
1530
- };
1531
- }
1532
- const global = await loadGlobalConfig(input.configDir);
1533
- if (!global) {
1534
- return {
1535
- kind: "missing_global_config",
1536
- message: "No repository runtime config found. Run 'gh-symphony repo init' first."
1537
- };
1538
- }
1539
- const projectIds = global.projects ?? [];
1540
- if (projectIds.length === 0) {
1541
- return {
1542
- kind: "no_projects",
1543
- message: "No repository runtime config is configured. Run 'gh-symphony repo init' first."
1544
- };
1545
- }
1546
- if (projectIds.length > 1 && !isInteractiveTerminal()) {
1547
- return {
1548
- kind: "multiple_projects_require_selection",
1549
- message: explicitProjectRequiredMessage().trimEnd()
1550
- };
1551
- }
1552
- if (global.activeProject) {
1553
- const projectConfig = await loadProjectConfig(
1554
- input.configDir,
1555
- global.activeProject
1556
- );
1557
- if (!projectConfig) {
1558
- return {
1559
- kind: "active_project_missing",
1560
- projectId: global.activeProject,
1561
- message: `Active project "${global.activeProject}" is configured in config.json but its project config is missing. Re-run 'gh-symphony repo init'.`
1562
- };
1563
- }
1564
- return {
1565
- kind: "resolved",
1566
- projectId: global.activeProject,
1567
- projectConfig
1568
- };
1569
- }
1570
- if (projectIds.length === 1) {
1571
- const projectId = projectIds[0];
1572
- const projectConfig = await loadProjectConfig(input.configDir, projectId);
1573
- if (!projectConfig) {
1574
- return {
1575
- kind: "configured_project_missing",
1576
- projectId,
1577
- message: `Configured project "${projectId}" is missing its project config file. Re-run 'gh-symphony repo init'.`
1578
- };
1579
- }
1580
- return {
1581
- kind: "resolved",
1582
- projectId,
1583
- projectConfig
1584
- };
1585
- }
1586
- return {
1587
- kind: "multiple_projects_require_selection",
1588
- message: "Multiple repository runtime configs are present and no active project is set. Re-run 'gh-symphony repo init' from the target repository."
1589
- };
1590
- }
1591
- async function resolveManagedProjectConfig(input) {
1592
- if (input.requestedProjectId) {
1593
- return loadProjectConfig(input.configDir, input.requestedProjectId);
1594
- }
1595
- const global = await loadGlobalConfig(input.configDir);
1596
- const projectIds = global?.projects ?? [];
1597
- if (projectIds.length === 0) {
1598
- return null;
1599
- }
1600
- if (projectIds.length === 1) {
1601
- return loadProjectConfig(input.configDir, projectIds[0]);
1602
- }
1603
- if (!isInteractiveTerminal()) {
1604
- process.stderr.write(explicitProjectRequiredMessage());
1605
- process.exitCode = 1;
1606
- return null;
1607
- }
1608
- const projects = await Promise.all(
1609
- projectIds.map(async (projectId) => ({
1610
- projectId,
1611
- config: await loadProjectConfig(input.configDir, projectId)
1612
- }))
1613
- );
1614
- const selected = await p.select({
1615
- message: "Select a project:",
1616
- options: projects.map(({ projectId, config }) => ({
1617
- value: projectId,
1618
- label: config?.displayName ?? config?.slug ?? projectId,
1619
- hint: projectId === global?.activeProject ? "current" : config && config.displayName && config.displayName !== projectId ? projectId : void 0
1620
- })),
1621
- maxItems: 10
1622
- });
1623
- if (p.isCancel(selected)) {
1624
- p.cancel("Cancelled.");
1625
- process.exitCode = 130;
1626
- return null;
1627
- }
1628
- return loadProjectConfig(input.configDir, selected);
1629
- }
1630
- function handleMissingManagedProjectConfig() {
1631
- if (process.exitCode) {
1632
- return;
1633
- }
1634
- process.stderr.write(
1635
- "No repository runtime config found. Run 'gh-symphony repo init' first.\n"
1636
- );
1637
- process.exitCode = 1;
1638
- }
1639
-
1640
- export {
1641
- fetchGithubProjectIssues,
1642
- fetchGithubProjectIssueByRepositoryAndNumber,
1643
- findGithubProjectIssue,
1644
- resolveTrackerAdapter,
1645
- inspectManagedProjectSelection,
1646
- resolveManagedProjectConfig,
1647
- handleMissingManagedProjectConfig
1648
- };