@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.
@@ -0,0 +1,1060 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_WORKFLOW_LIFECYCLE
4
+ } from "./chunk-QEONJ5DZ.js";
5
+
6
+ // ../tracker-github/src/adapter.ts
7
+ import { createHash } from "crypto";
8
+ var DEFAULT_API_URL = "https://api.github.com/graphql";
9
+ var DEFAULT_PAGE_SIZE = 25;
10
+ var DEFAULT_NETWORK_TIMEOUT_MS = 3e4;
11
+ var RATE_LIMIT_THRESHOLD = 100;
12
+ var MAX_RATE_LIMIT_WAIT_MS = 6e4;
13
+ var GitHubTrackerError = class extends Error {
14
+ };
15
+ var GitHubTrackerHttpError = class extends GitHubTrackerError {
16
+ constructor(message, status, details) {
17
+ super(message);
18
+ this.status = status;
19
+ this.details = details;
20
+ }
21
+ };
22
+ var GitHubTrackerQueryError = class extends GitHubTrackerError {
23
+ };
24
+ var cachedGitHubGraphQLRateLimits = /* @__PURE__ */ new Map();
25
+ function normalizeProjectItem(projectId, item, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
26
+ if (item.content?.__typename !== "Issue") {
27
+ return null;
28
+ }
29
+ const fieldValues = extractFieldValues(item.fieldValues?.nodes ?? []);
30
+ const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
31
+ const repository = item.content.repository;
32
+ const blockedBy = (item.content.blockedBy?.nodes ?? []).flatMap(
33
+ (node) => node ? [
34
+ {
35
+ id: node.id,
36
+ identifier: `${node.repository.owner.login}/${node.repository.name}#${node.number}`,
37
+ state: normalizeBlockerState(node.state, lifecycle)
38
+ }
39
+ ] : []
40
+ );
41
+ const issueUpdatedAtMs = parseTimestampMs(item.content.updatedAt);
42
+ const itemUpdatedAtMs = parseTimestampMs(item.updatedAt);
43
+ const trackedUpdatedAt = itemUpdatedAtMs !== null && (issueUpdatedAtMs === null || itemUpdatedAtMs > issueUpdatedAtMs) ? item.updatedAt : item.content.updatedAt ?? item.updatedAt;
44
+ return {
45
+ id: item.content.id,
46
+ identifier: `${repository.owner.login}/${repository.name}#${item.content.number}`,
47
+ number: item.content.number,
48
+ title: item.content.title,
49
+ description: item.content.body,
50
+ priority: resolvePriority(item, priority),
51
+ state,
52
+ branchName: null,
53
+ url: item.content.url,
54
+ labels: (item.content.labels?.nodes ?? []).flatMap((label) => label?.name ? [label.name.toLowerCase()] : []).sort(),
55
+ blockedBy,
56
+ createdAt: item.content.createdAt,
57
+ updatedAt: trackedUpdatedAt,
58
+ repository: {
59
+ owner: repository.owner.login,
60
+ name: repository.name,
61
+ url: repository.url,
62
+ cloneUrl: deriveCloneUrl(repository.url)
63
+ },
64
+ tracker: {
65
+ adapter: "github-project",
66
+ bindingId: projectId,
67
+ itemId: item.id
68
+ },
69
+ metadata: fieldValues,
70
+ rateLimits
71
+ };
72
+ }
73
+ async function fetchProjectIssues(config, fetchImpl = fetch) {
74
+ const issues = [];
75
+ let cursor = null;
76
+ const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(
77
+ config,
78
+ config.priorityFieldName,
79
+ fetchImpl
80
+ ) : void 0;
81
+ const currentUserLogin = config.assignedOnly ? await fetchCurrentUserLogin(config, fetchImpl) : null;
82
+ let excludedCount = 0;
83
+ let latestRateLimits = null;
84
+ do {
85
+ const pageResult = await fetchProjectItemsPage(config, cursor, fetchImpl);
86
+ const page = pageResult.page;
87
+ latestRateLimits = pageResult.rateLimits ?? latestRateLimits;
88
+ const pageIssues = (page.nodes ?? []).flatMap((item) => {
89
+ if (!item) {
90
+ return [];
91
+ }
92
+ const normalized = normalizeProjectItem(
93
+ config.projectId,
94
+ item,
95
+ config.lifecycle,
96
+ {
97
+ fieldName: config.priorityFieldName,
98
+ optionIds: priorityOptionIds
99
+ },
100
+ latestRateLimits
101
+ );
102
+ if (!normalized) {
103
+ return [];
104
+ }
105
+ if (currentUserLogin && !isIssueAssignedToLogin(item, currentUserLogin)) {
106
+ excludedCount += 1;
107
+ return [];
108
+ }
109
+ return [normalized];
110
+ });
111
+ issues.push(...pageIssues);
112
+ cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
113
+ } while (cursor);
114
+ if (currentUserLogin) {
115
+ emitAssignedOnlyFilterEvent({
116
+ projectId: config.projectId,
117
+ currentUserLogin,
118
+ includedCount: issues.length,
119
+ excludedCount
120
+ });
121
+ }
122
+ if (latestRateLimits) {
123
+ for (const issue of issues) {
124
+ issue.rateLimits = latestRateLimits;
125
+ }
126
+ }
127
+ return issues;
128
+ }
129
+ async function fetchIssueStatesByIds(config, issueIds, fetchImpl = fetch) {
130
+ if (issueIds.length === 0) {
131
+ return [];
132
+ }
133
+ const issues = [];
134
+ for (const issueIdBatch of chunkValues([...new Set(issueIds)], 100)) {
135
+ const result = await executeGraphQLQueryWithMetadata(
136
+ config,
137
+ ISSUE_STATES_BY_IDS_QUERY,
138
+ {
139
+ issueIds: issueIdBatch
140
+ },
141
+ fetchImpl
142
+ );
143
+ const data = result.data;
144
+ const rateLimits = result.rateLimits;
145
+ for (const node of data.nodes ?? []) {
146
+ const projectItem = await resolveIssueProjectItemForStateLookup(
147
+ config,
148
+ node,
149
+ fetchImpl
150
+ );
151
+ const normalized = normalizeIssueStateLookupNode(
152
+ config.projectId,
153
+ node,
154
+ projectItem,
155
+ config.lifecycle,
156
+ rateLimits
157
+ );
158
+ if (normalized) {
159
+ issues.push(normalized);
160
+ }
161
+ }
162
+ }
163
+ return issues;
164
+ }
165
+ async function fetchProjectIssueByRepositoryAndNumber(config, repository, issueNumber, fetchImpl = fetch) {
166
+ const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(
167
+ config,
168
+ config.priorityFieldName,
169
+ fetchImpl
170
+ ) : void 0;
171
+ const result = await executeGraphQLQueryWithMetadata(
172
+ config,
173
+ REPOSITORY_ISSUE_QUERY,
174
+ {
175
+ owner: repository.owner,
176
+ name: repository.name,
177
+ issueNumber
178
+ },
179
+ fetchImpl
180
+ );
181
+ const issue = result.data.repository?.issue ?? null;
182
+ if (!issue) {
183
+ return null;
184
+ }
185
+ const projectItem = await resolveIssueProjectItemForStateLookup(
186
+ config,
187
+ issue,
188
+ fetchImpl
189
+ );
190
+ if (!projectItem) {
191
+ return null;
192
+ }
193
+ return normalizeRepositoryIssueLookup(
194
+ config.projectId,
195
+ issue,
196
+ projectItem,
197
+ config.lifecycle,
198
+ {
199
+ fieldName: config.priorityFieldName,
200
+ optionIds: priorityOptionIds
201
+ },
202
+ result.rateLimits
203
+ );
204
+ }
205
+ async function fetchProjectItemsPage(config, cursor, fetchImpl) {
206
+ const result = await executeGraphQLQueryWithMetadata(
207
+ config,
208
+ PROJECT_ITEMS_QUERY,
209
+ {
210
+ projectId: config.projectId,
211
+ cursor,
212
+ pageSize: config.pageSize ?? DEFAULT_PAGE_SIZE
213
+ },
214
+ fetchImpl
215
+ );
216
+ const data = result.data;
217
+ const items = data.node?.items;
218
+ if (!items) {
219
+ throw new GitHubTrackerQueryError(
220
+ "GitHub GraphQL response did not include project items."
221
+ );
222
+ }
223
+ return {
224
+ page: items,
225
+ rateLimits: result.rateLimits
226
+ };
227
+ }
228
+ var fetchGithubProjectIssues = fetchProjectIssues;
229
+ var fetchGithubIssueStatesByIds = fetchIssueStatesByIds;
230
+ var fetchGithubProjectIssueByRepositoryAndNumber = fetchProjectIssueByRepositoryAndNumber;
231
+ async function fetchCurrentUserLogin(config, fetchImpl) {
232
+ const response = await fetchImpl(resolveRestUserApiUrl(config.apiUrl), {
233
+ method: "GET",
234
+ headers: {
235
+ authorization: `Bearer ${config.token}`,
236
+ "user-agent": "gh-symphony",
237
+ accept: "application/vnd.github+json"
238
+ },
239
+ signal: buildRequestSignal(config.timeoutMs)
240
+ });
241
+ if (!response.ok) {
242
+ const details = await response.text();
243
+ throw new GitHubTrackerHttpError(
244
+ `GitHub REST request failed with status ${response.status}`,
245
+ response.status,
246
+ details
247
+ );
248
+ }
249
+ const payload = await response.json();
250
+ if (!payload.login) {
251
+ throw new GitHubTrackerQueryError(
252
+ "GitHub REST response did not include the authenticated user login."
253
+ );
254
+ }
255
+ return payload.login;
256
+ }
257
+ function isIssueAssignedToLogin(item, login) {
258
+ if (item.content?.__typename !== "Issue") {
259
+ return false;
260
+ }
261
+ return (item.content.assignees?.nodes ?? []).some(
262
+ (assignee) => assignee?.login === login
263
+ );
264
+ }
265
+ function emitAssignedOnlyFilterEvent(input) {
266
+ console.info(
267
+ JSON.stringify({
268
+ event: "tracker-assigned-only-filtered",
269
+ projectId: input.projectId,
270
+ currentUserLogin: input.currentUserLogin,
271
+ includedCount: input.includedCount,
272
+ excludedCount: input.excludedCount
273
+ })
274
+ );
275
+ }
276
+ function extractFieldValues(nodes) {
277
+ return nodes.reduce((values, node) => {
278
+ const fieldName = node?.field?.name;
279
+ if (!fieldName) {
280
+ return values;
281
+ }
282
+ if (node.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.name) {
283
+ values[fieldName] = node.name;
284
+ }
285
+ if (node.__typename === "ProjectV2ItemFieldTextValue" && node.text) {
286
+ values[fieldName] = node.text;
287
+ }
288
+ return values;
289
+ }, {});
290
+ }
291
+ function normalizeIssueStateLookupNode(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, rateLimits = null) {
292
+ if (issue?.__typename !== "Issue") {
293
+ return null;
294
+ }
295
+ if (!projectItem) {
296
+ return null;
297
+ }
298
+ const fieldValues = extractFieldValues(projectItem.fieldValues?.nodes ?? []);
299
+ const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
300
+ const repository = issue.repository;
301
+ const identifier = `${repository.owner.login}/${repository.name}#${issue.number}`;
302
+ return {
303
+ id: issue.id,
304
+ identifier,
305
+ number: issue.number,
306
+ title: identifier,
307
+ description: null,
308
+ priority: null,
309
+ state,
310
+ branchName: null,
311
+ url: `${repository.url}/issues/${issue.number}`,
312
+ labels: [],
313
+ blockedBy: [],
314
+ createdAt: null,
315
+ updatedAt: projectItem.updatedAt ?? issue.updatedAt,
316
+ repository: {
317
+ owner: repository.owner.login,
318
+ name: repository.name,
319
+ url: repository.url,
320
+ cloneUrl: deriveCloneUrl(repository.url)
321
+ },
322
+ tracker: {
323
+ adapter: "github-project",
324
+ bindingId: projectId,
325
+ itemId: projectItem.id
326
+ },
327
+ metadata: fieldValues,
328
+ rateLimits
329
+ };
330
+ }
331
+ function normalizeRepositoryIssueLookup(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
332
+ if (!issue || !projectItem) {
333
+ return null;
334
+ }
335
+ return normalizeProjectItem(
336
+ projectId,
337
+ {
338
+ id: projectItem.id,
339
+ updatedAt: projectItem.updatedAt,
340
+ fieldValues: projectItem.fieldValues,
341
+ content: issue
342
+ },
343
+ lifecycle,
344
+ priority,
345
+ rateLimits
346
+ );
347
+ }
348
+ async function resolveIssueProjectItemForStateLookup(config, issue, fetchImpl) {
349
+ if (issue?.__typename !== "Issue") {
350
+ return null;
351
+ }
352
+ let connection = issue.projectItems;
353
+ let projectItem = findProjectItemByProjectId(
354
+ connection?.nodes ?? [],
355
+ config.projectId
356
+ );
357
+ let cursor = connection?.pageInfo.endCursor ?? null;
358
+ while (!projectItem && connection?.pageInfo.hasNextPage) {
359
+ const nextPage = await fetchIssueProjectItemsPage(
360
+ config,
361
+ issue.id,
362
+ cursor,
363
+ fetchImpl
364
+ );
365
+ projectItem = findProjectItemByProjectId(
366
+ nextPage.nodes ?? [],
367
+ config.projectId
368
+ );
369
+ connection = nextPage;
370
+ cursor = nextPage.pageInfo.endCursor;
371
+ }
372
+ return projectItem;
373
+ }
374
+ async function fetchIssueProjectItemsPage(config, issueId, cursor, fetchImpl) {
375
+ const result = await executeGraphQLQueryWithMetadata(
376
+ config,
377
+ ISSUE_PROJECT_ITEMS_PAGE_QUERY,
378
+ {
379
+ issueId,
380
+ cursor
381
+ },
382
+ fetchImpl
383
+ );
384
+ const data = result.data;
385
+ const issue = data.node;
386
+ if (issue?.__typename !== "Issue" || !issue.projectItems) {
387
+ throw new GitHubTrackerQueryError(
388
+ "GitHub GraphQL response did not include issue project items."
389
+ );
390
+ }
391
+ return issue.projectItems;
392
+ }
393
+ function findProjectItemByProjectId(nodes, projectId) {
394
+ return nodes.find((item) => item?.project?.id === projectId) ?? null;
395
+ }
396
+ function resolvePriority(item, priority) {
397
+ if (!priority.fieldName || !priority.optionIds) {
398
+ return null;
399
+ }
400
+ for (const node of item.fieldValues?.nodes ?? []) {
401
+ if (node?.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.field?.name === priority.fieldName && node.optionId) {
402
+ return priority.optionIds[node.optionId] ?? null;
403
+ }
404
+ }
405
+ return null;
406
+ }
407
+ function extractPriorityOptionOrder(fields, priorityFieldName) {
408
+ for (const field of fields) {
409
+ if (isSingleSelectProjectField(field) && field.name === priorityFieldName) {
410
+ let nextPriority = 0;
411
+ const optionEntries = (field.options ?? []).flatMap((option) => {
412
+ if (!option?.id) {
413
+ return [];
414
+ }
415
+ const entry = [option.id, nextPriority];
416
+ nextPriority += 1;
417
+ return [entry];
418
+ });
419
+ return Object.fromEntries(optionEntries);
420
+ }
421
+ }
422
+ return void 0;
423
+ }
424
+ async function fetchPriorityOptionOrder(config, priorityFieldName, fetchImpl) {
425
+ const data = await executeGraphQLQuery(
426
+ config,
427
+ PROJECT_FIELDS_QUERY,
428
+ { projectId: config.projectId },
429
+ fetchImpl
430
+ );
431
+ return extractPriorityOptionOrder(
432
+ data.node?.fields?.nodes ?? [],
433
+ priorityFieldName
434
+ );
435
+ }
436
+ function isSingleSelectProjectField(field) {
437
+ return field?.__typename === "ProjectV2SingleSelectField";
438
+ }
439
+ function deriveCloneUrl(repositoryUrl) {
440
+ if (repositoryUrl.startsWith("file://") || repositoryUrl.endsWith(".git")) {
441
+ return repositoryUrl;
442
+ }
443
+ return `${repositoryUrl}.git`;
444
+ }
445
+ function normalizeBlockerState(state, lifecycle) {
446
+ if (!state) {
447
+ return null;
448
+ }
449
+ const normalized = state.trim().toLowerCase();
450
+ if (normalized === "closed") {
451
+ return lifecycle.terminalStates[0] ?? state;
452
+ }
453
+ if (normalized === "open") {
454
+ return null;
455
+ }
456
+ return state;
457
+ }
458
+ function resolveRestUserApiUrl(apiUrl) {
459
+ const parsed = new URL(apiUrl ?? DEFAULT_API_URL);
460
+ const pathSegments = parsed.pathname.split("/").filter(Boolean);
461
+ if (pathSegments.at(-1) === "graphql") {
462
+ pathSegments.pop();
463
+ }
464
+ parsed.pathname = `/${pathSegments.join("/")}/user`.replace(/\/{2,}/g, "/");
465
+ parsed.search = "";
466
+ parsed.hash = "";
467
+ return parsed.toString();
468
+ }
469
+ function chunkValues(values, size) {
470
+ const chunks = [];
471
+ for (let index = 0; index < values.length; index += size) {
472
+ chunks.push(values.slice(index, index + size));
473
+ }
474
+ return chunks;
475
+ }
476
+ function buildRequestSignal(timeoutMs) {
477
+ return AbortSignal.timeout(resolveNetworkTimeoutMs(timeoutMs));
478
+ }
479
+ function resolveNetworkTimeoutMs(timeoutMs) {
480
+ if (timeoutMs !== void 0 && Number.isInteger(timeoutMs) && timeoutMs > 0) {
481
+ return timeoutMs;
482
+ }
483
+ return DEFAULT_NETWORK_TIMEOUT_MS;
484
+ }
485
+ async function executeGraphQLQuery(config, query, variables, fetchImpl) {
486
+ const result = await executeGraphQLQueryWithMetadata(
487
+ config,
488
+ query,
489
+ variables,
490
+ fetchImpl
491
+ );
492
+ return result.data;
493
+ }
494
+ async function executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl) {
495
+ const tokenFingerprint = fingerprintToken(config.token);
496
+ await guardGraphQLRateLimit(tokenFingerprint);
497
+ const response = await fetchImpl(config.apiUrl ?? DEFAULT_API_URL, {
498
+ method: "POST",
499
+ headers: {
500
+ "content-type": "application/json",
501
+ authorization: `Bearer ${config.token}`
502
+ },
503
+ body: JSON.stringify({
504
+ query,
505
+ variables
506
+ }),
507
+ signal: buildRequestSignal(config.timeoutMs)
508
+ });
509
+ if (!response.ok) {
510
+ const details = await response.text();
511
+ throw new GitHubTrackerHttpError(
512
+ `GitHub GraphQL request failed with status ${response.status}`,
513
+ response.status,
514
+ details
515
+ );
516
+ }
517
+ const payload = await response.json();
518
+ if (payload.errors?.length) {
519
+ throw new GitHubTrackerQueryError(
520
+ payload.errors.map((error) => error.message).join("; ")
521
+ );
522
+ }
523
+ if (!payload.data) {
524
+ throw new GitHubTrackerQueryError(
525
+ "GitHub GraphQL response did not include data."
526
+ );
527
+ }
528
+ const data = payload.data;
529
+ const rateLimits = extractGitHubRateLimits(response.headers);
530
+ cachedGitHubGraphQLRateLimits.set(tokenFingerprint, rateLimits);
531
+ return {
532
+ data,
533
+ rateLimits
534
+ };
535
+ }
536
+ async function guardGraphQLRateLimit(tokenFingerprint) {
537
+ const rateLimit = cachedGitHubGraphQLRateLimits.get(tokenFingerprint) ?? null;
538
+ if (!rateLimit) {
539
+ return;
540
+ }
541
+ const remaining = rateLimit.remaining;
542
+ if (remaining === null || remaining > RATE_LIMIT_THRESHOLD) {
543
+ return;
544
+ }
545
+ const resetAtMs = parseTimestampMs(rateLimit.resetAt);
546
+ if (resetAtMs === null) {
547
+ throw new GitHubTrackerError("Rate limit near exhaustion");
548
+ }
549
+ const waitMs = Math.max(0, resetAtMs - Date.now());
550
+ if (waitMs > MAX_RATE_LIMIT_WAIT_MS) {
551
+ throw new GitHubTrackerError("Rate limit near exhaustion");
552
+ }
553
+ cachedGitHubGraphQLRateLimits.delete(tokenFingerprint);
554
+ if (waitMs > 0) {
555
+ await sleep(waitMs);
556
+ }
557
+ }
558
+ function fingerprintToken(token) {
559
+ return createHash("sha256").update(token).digest("hex");
560
+ }
561
+ function extractGitHubRateLimits(headers) {
562
+ if (!headers || typeof headers.get !== "function") {
563
+ return null;
564
+ }
565
+ const limit = parseIntegerHeader(headers.get("x-ratelimit-limit"));
566
+ const remaining = parseIntegerHeader(headers.get("x-ratelimit-remaining"));
567
+ const used = parseIntegerHeader(headers.get("x-ratelimit-used"));
568
+ const reset = parseIntegerHeader(headers.get("x-ratelimit-reset"));
569
+ const resource = headers.get("x-ratelimit-resource");
570
+ if (limit === null && remaining === null && used === null && reset === null && resource === null) {
571
+ return null;
572
+ }
573
+ return {
574
+ source: "github",
575
+ limit,
576
+ remaining,
577
+ used,
578
+ reset,
579
+ resetAt: reset === null ? null : new Date(reset * 1e3).toISOString(),
580
+ resource
581
+ };
582
+ }
583
+ function parseIntegerHeader(value) {
584
+ if (value === null) {
585
+ return null;
586
+ }
587
+ const parsed = Number.parseInt(value, 10);
588
+ return Number.isFinite(parsed) ? parsed : null;
589
+ }
590
+ function parseTimestampMs(value) {
591
+ if (!value) {
592
+ return null;
593
+ }
594
+ const timestampMs = Date.parse(value);
595
+ return Number.isFinite(timestampMs) ? timestampMs : null;
596
+ }
597
+ function sleep(ms) {
598
+ return new Promise((resolve) => {
599
+ setTimeout(resolve, ms);
600
+ });
601
+ }
602
+ var PROJECT_ITEMS_QUERY = `
603
+ query ProjectItems($projectId: ID!, $cursor: String, $pageSize: Int!) {
604
+ node(id: $projectId) {
605
+ __typename
606
+ ... on ProjectV2 {
607
+ items(first: $pageSize, after: $cursor) {
608
+ nodes {
609
+ id
610
+ updatedAt
611
+ fieldValues(first: 20) {
612
+ nodes {
613
+ __typename
614
+ ... on ProjectV2ItemFieldSingleSelectValue {
615
+ name
616
+ optionId
617
+ field {
618
+ ... on ProjectV2SingleSelectField {
619
+ name
620
+ }
621
+ }
622
+ }
623
+ ... on ProjectV2ItemFieldTextValue {
624
+ text
625
+ field {
626
+ ... on ProjectV2FieldCommon {
627
+ name
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
+ content {
634
+ __typename
635
+ ... on Issue {
636
+ id
637
+ number
638
+ title
639
+ body
640
+ url
641
+ createdAt
642
+ updatedAt
643
+ labels(first: 20) {
644
+ nodes {
645
+ name
646
+ }
647
+ }
648
+ assignees(first: 20) {
649
+ nodes {
650
+ login
651
+ }
652
+ }
653
+ repository {
654
+ name
655
+ url
656
+ owner {
657
+ login
658
+ }
659
+ }
660
+ blockedBy(first: 100) {
661
+ nodes {
662
+ id
663
+ number
664
+ state
665
+ repository {
666
+ name
667
+ owner {
668
+ login
669
+ }
670
+ }
671
+ }
672
+ }
673
+ }
674
+ }
675
+ }
676
+ pageInfo {
677
+ endCursor
678
+ hasNextPage
679
+ }
680
+ }
681
+ }
682
+ }
683
+ }
684
+ `;
685
+ var PROJECT_FIELDS_QUERY = `
686
+ query ProjectFields($projectId: ID!) {
687
+ node(id: $projectId) {
688
+ __typename
689
+ ... on ProjectV2 {
690
+ fields(first: 100) {
691
+ nodes {
692
+ __typename
693
+ ... on ProjectV2SingleSelectField {
694
+ name
695
+ options {
696
+ id
697
+ name
698
+ }
699
+ }
700
+ }
701
+ }
702
+ }
703
+ }
704
+ }
705
+ `;
706
+ var ISSUE_STATES_BY_IDS_QUERY = `
707
+ query IssueStatesByIds($issueIds: [ID!]!) {
708
+ nodes(ids: $issueIds) {
709
+ __typename
710
+ ... on Issue {
711
+ id
712
+ number
713
+ updatedAt
714
+ repository {
715
+ name
716
+ url
717
+ owner {
718
+ login
719
+ }
720
+ }
721
+ projectItems(first: 100, includeArchived: false) {
722
+ nodes {
723
+ id
724
+ updatedAt
725
+ project {
726
+ id
727
+ }
728
+ fieldValues(first: 20) {
729
+ nodes {
730
+ __typename
731
+ ... on ProjectV2ItemFieldSingleSelectValue {
732
+ name
733
+ optionId
734
+ field {
735
+ ... on ProjectV2SingleSelectField {
736
+ name
737
+ }
738
+ }
739
+ }
740
+ ... on ProjectV2ItemFieldTextValue {
741
+ text
742
+ field {
743
+ ... on ProjectV2FieldCommon {
744
+ name
745
+ }
746
+ }
747
+ }
748
+ }
749
+ }
750
+ }
751
+ pageInfo {
752
+ endCursor
753
+ hasNextPage
754
+ }
755
+ }
756
+ }
757
+ }
758
+ }
759
+ `;
760
+ var ISSUE_PROJECT_ITEMS_PAGE_QUERY = `
761
+ query IssueProjectItemsPage($issueId: ID!, $cursor: String) {
762
+ node(id: $issueId) {
763
+ __typename
764
+ ... on Issue {
765
+ id
766
+ number
767
+ updatedAt
768
+ repository {
769
+ name
770
+ url
771
+ owner {
772
+ login
773
+ }
774
+ }
775
+ projectItems(first: 100, after: $cursor, includeArchived: false) {
776
+ nodes {
777
+ id
778
+ updatedAt
779
+ project {
780
+ id
781
+ }
782
+ fieldValues(first: 20) {
783
+ nodes {
784
+ __typename
785
+ ... on ProjectV2ItemFieldSingleSelectValue {
786
+ name
787
+ optionId
788
+ field {
789
+ ... on ProjectV2SingleSelectField {
790
+ name
791
+ }
792
+ }
793
+ }
794
+ ... on ProjectV2ItemFieldTextValue {
795
+ text
796
+ field {
797
+ ... on ProjectV2FieldCommon {
798
+ name
799
+ }
800
+ }
801
+ }
802
+ }
803
+ }
804
+ }
805
+ pageInfo {
806
+ endCursor
807
+ hasNextPage
808
+ }
809
+ }
810
+ }
811
+ }
812
+ }
813
+ `;
814
+ var REPOSITORY_ISSUE_QUERY = `
815
+ query RepositoryIssue(
816
+ $owner: String!
817
+ $name: String!
818
+ $issueNumber: Int!
819
+ ) {
820
+ repository(owner: $owner, name: $name) {
821
+ issue(number: $issueNumber) {
822
+ __typename
823
+ id
824
+ number
825
+ title
826
+ body
827
+ url
828
+ createdAt
829
+ updatedAt
830
+ labels(first: 20) {
831
+ nodes {
832
+ name
833
+ }
834
+ }
835
+ assignees(first: 20) {
836
+ nodes {
837
+ login
838
+ }
839
+ }
840
+ repository {
841
+ name
842
+ url
843
+ owner {
844
+ login
845
+ }
846
+ }
847
+ blockedBy(first: 100) {
848
+ nodes {
849
+ id
850
+ number
851
+ state
852
+ repository {
853
+ name
854
+ owner {
855
+ login
856
+ }
857
+ }
858
+ }
859
+ }
860
+ projectItems(first: 20, includeArchived: false) {
861
+ nodes {
862
+ id
863
+ updatedAt
864
+ project {
865
+ id
866
+ }
867
+ fieldValues(first: 20) {
868
+ nodes {
869
+ __typename
870
+ ... on ProjectV2ItemFieldSingleSelectValue {
871
+ name
872
+ optionId
873
+ field {
874
+ ... on ProjectV2SingleSelectField {
875
+ name
876
+ }
877
+ }
878
+ }
879
+ ... on ProjectV2ItemFieldTextValue {
880
+ text
881
+ field {
882
+ ... on ProjectV2FieldCommon {
883
+ name
884
+ }
885
+ }
886
+ }
887
+ }
888
+ }
889
+ }
890
+ pageInfo {
891
+ endCursor
892
+ hasNextPage
893
+ }
894
+ }
895
+ }
896
+ }
897
+ }
898
+ `;
899
+
900
+ // ../tracker-github/src/orchestrator-adapter.ts
901
+ import { createHash as createHash2 } from "crypto";
902
+ var githubProjectTrackerAdapter = {
903
+ async listIssues(project, dependencies = {}) {
904
+ return listProjectIssues(project, dependencies);
905
+ },
906
+ async listIssuesByStates(project, states, dependencies = {}) {
907
+ if (states.length === 0) {
908
+ return [];
909
+ }
910
+ const issues = await listProjectIssues(project, dependencies);
911
+ const normalizedStates = new Set(
912
+ states.map((state) => state.trim().toLowerCase())
913
+ );
914
+ return issues.filter(
915
+ (issue) => normalizedStates.has(issue.state.trim().toLowerCase())
916
+ );
917
+ },
918
+ async fetchIssueStatesByIds(project, issueIds, dependencies = {}) {
919
+ if (issueIds.length === 0) {
920
+ return [];
921
+ }
922
+ return fetchProjectIssueStatesByIds(project, issueIds, dependencies);
923
+ },
924
+ buildWorkerEnvironment(project) {
925
+ return {
926
+ GITHUB_PROJECT_ID: requireTrackerSetting(project.tracker, "projectId")
927
+ };
928
+ },
929
+ reviveIssue(project, run) {
930
+ return {
931
+ id: run.issueId,
932
+ identifier: run.issueIdentifier,
933
+ number: parseIssueNumber(run.issueIdentifier),
934
+ title: run.issueTitle ?? run.issueIdentifier,
935
+ description: null,
936
+ priority: null,
937
+ state: run.issueState,
938
+ branchName: null,
939
+ url: null,
940
+ labels: [],
941
+ blockedBy: [],
942
+ createdAt: null,
943
+ updatedAt: null,
944
+ repository: run.repository,
945
+ tracker: {
946
+ adapter: "github-project",
947
+ bindingId: project.tracker.bindingId,
948
+ itemId: run.issueId
949
+ },
950
+ metadata: {}
951
+ };
952
+ }
953
+ };
954
+ async function listProjectIssues(project, dependencies = {}) {
955
+ const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
956
+ const loadProjectIssues = () => fetchGithubProjectIssues(trackerConfig, dependencies.fetchImpl);
957
+ return dependencies.projectItemsCache?.getOrLoad(
958
+ buildProjectItemsCacheKey(trackerConfig, dependencies),
959
+ loadProjectIssues
960
+ ) ?? loadProjectIssues();
961
+ }
962
+ async function fetchProjectIssueStatesByIds(project, issueIds, dependencies = {}) {
963
+ const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
964
+ return fetchGithubIssueStatesByIds(
965
+ trackerConfig,
966
+ [...issueIds],
967
+ dependencies.fetchImpl
968
+ );
969
+ }
970
+ function resolveGitHubTrackerConfig(project, dependencies = {}) {
971
+ const token = dependencies.token ?? process.env.GITHUB_GRAPHQL_TOKEN;
972
+ if (!token) {
973
+ throw new Error(
974
+ "GITHUB_GRAPHQL_TOKEN environment variable is required. Run 'gh auth token' or set the variable."
975
+ );
976
+ }
977
+ const githubProjectId = requireTrackerSetting(project.tracker, "projectId");
978
+ return {
979
+ projectId: githubProjectId,
980
+ token,
981
+ apiUrl: project.tracker.apiUrl,
982
+ assignedOnly: readBooleanTrackerSetting(project.tracker, "assignedOnly"),
983
+ priorityFieldName: readOptionalStringTrackerSetting(
984
+ project.tracker,
985
+ "priorityFieldName"
986
+ ),
987
+ timeoutMs: readNumberTrackerSetting(project.tracker, "timeoutMs")
988
+ };
989
+ }
990
+ function buildProjectItemsCacheKey(config, _dependencies) {
991
+ return JSON.stringify({
992
+ adapter: "github-project",
993
+ apiUrl: config.apiUrl,
994
+ assignedOnly: config.assignedOnly ?? false,
995
+ priorityFieldName: config.priorityFieldName ?? null,
996
+ projectId: config.projectId,
997
+ timeoutMs: config.timeoutMs,
998
+ tokenFingerprint: hashToken(config.token)
999
+ });
1000
+ }
1001
+ function hashToken(token) {
1002
+ if (!token) {
1003
+ return null;
1004
+ }
1005
+ return createHash2("sha256").update(token).digest("hex");
1006
+ }
1007
+ var trackerAdapters = {
1008
+ "github-project": githubProjectTrackerAdapter
1009
+ };
1010
+ function resolveTrackerAdapter(tracker) {
1011
+ const adapter = trackerAdapters[tracker.adapter];
1012
+ if (!adapter) {
1013
+ throw new Error(`Unsupported tracker adapter: ${tracker.adapter}`);
1014
+ }
1015
+ return adapter;
1016
+ }
1017
+ function requireTrackerSetting(tracker, key) {
1018
+ const value = tracker.settings?.[key];
1019
+ if (typeof value !== "string" || value.length === 0) {
1020
+ throw new Error(
1021
+ `Tracker adapter "${tracker.adapter}" requires the "${key}" setting.`
1022
+ );
1023
+ }
1024
+ return value;
1025
+ }
1026
+ function readBooleanTrackerSetting(tracker, key) {
1027
+ const value = tracker.settings?.[key];
1028
+ return value === true || value === "true";
1029
+ }
1030
+ function readNumberTrackerSetting(tracker, key) {
1031
+ const value = tracker.settings?.[key];
1032
+ if (value === void 0) {
1033
+ return void 0;
1034
+ }
1035
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
1036
+ return value;
1037
+ }
1038
+ if (typeof value === "string") {
1039
+ const parsed = Number(value);
1040
+ if (Number.isInteger(parsed) && parsed > 0) {
1041
+ return parsed;
1042
+ }
1043
+ }
1044
+ throw new Error(
1045
+ `Tracker adapter "${tracker.adapter}" requires the "${key}" setting to be a positive integer when provided.`
1046
+ );
1047
+ }
1048
+ function readOptionalStringTrackerSetting(tracker, key) {
1049
+ const value = tracker.settings?.[key];
1050
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1051
+ }
1052
+ function parseIssueNumber(identifier) {
1053
+ const match = identifier.match(/#(\d+)$/);
1054
+ return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
1055
+ }
1056
+
1057
+ export {
1058
+ fetchGithubProjectIssueByRepositoryAndNumber,
1059
+ resolveTrackerAdapter
1060
+ };