@bretwardjames/ghp-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1230 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/github-api.ts
8
+ import { graphql } from "@octokit/graphql";
9
+
10
+ // src/queries.ts
11
+ var queries_exports = {};
12
+ __export(queries_exports, {
13
+ ADD_COMMENT_MUTATION: () => ADD_COMMENT_MUTATION,
14
+ ADD_LABELS_MUTATION: () => ADD_LABELS_MUTATION,
15
+ ADD_TO_PROJECT_MUTATION: () => ADD_TO_PROJECT_MUTATION,
16
+ COLLABORATORS_QUERY: () => COLLABORATORS_QUERY,
17
+ CREATE_ISSUE_MUTATION: () => CREATE_ISSUE_MUTATION,
18
+ ISSUES_WITH_LABEL_QUERY: () => ISSUES_WITH_LABEL_QUERY,
19
+ ISSUE_AND_LABEL_QUERY: () => ISSUE_AND_LABEL_QUERY,
20
+ ISSUE_DETAILS_QUERY: () => ISSUE_DETAILS_QUERY,
21
+ ISSUE_FOR_UPDATE_QUERY: () => ISSUE_FOR_UPDATE_QUERY,
22
+ ISSUE_NODE_ID_QUERY: () => ISSUE_NODE_ID_QUERY,
23
+ ISSUE_TYPES_QUERY: () => ISSUE_TYPES_QUERY,
24
+ LABEL_EXISTS_QUERY: () => LABEL_EXISTS_QUERY,
25
+ PROJECT_FIELDS_QUERY: () => PROJECT_FIELDS_QUERY,
26
+ PROJECT_ITEMS_QUERY: () => PROJECT_ITEMS_QUERY,
27
+ PROJECT_VIEWS_QUERY: () => PROJECT_VIEWS_QUERY,
28
+ RECENT_ISSUES_QUERY: () => RECENT_ISSUES_QUERY,
29
+ REMOVE_LABELS_MUTATION: () => REMOVE_LABELS_MUTATION,
30
+ REPOSITORY_ID_QUERY: () => REPOSITORY_ID_QUERY,
31
+ REPOSITORY_PROJECTS_QUERY: () => REPOSITORY_PROJECTS_QUERY,
32
+ UPDATE_ISSUE_TYPE_MUTATION: () => UPDATE_ISSUE_TYPE_MUTATION,
33
+ UPDATE_ITEM_FIELD_MUTATION: () => UPDATE_ITEM_FIELD_MUTATION,
34
+ UPDATE_ITEM_STATUS_MUTATION: () => UPDATE_ITEM_STATUS_MUTATION,
35
+ VIEWER_QUERY: () => VIEWER_QUERY
36
+ });
37
+ var VIEWER_QUERY = `
38
+ query {
39
+ viewer {
40
+ login
41
+ }
42
+ }
43
+ `;
44
+ var REPOSITORY_PROJECTS_QUERY = `
45
+ query($owner: String!, $name: String!) {
46
+ repository(owner: $owner, name: $name) {
47
+ projectsV2(first: 20) {
48
+ nodes {
49
+ id
50
+ title
51
+ number
52
+ url
53
+ }
54
+ }
55
+ }
56
+ }
57
+ `;
58
+ var REPOSITORY_ID_QUERY = `
59
+ query($owner: String!, $name: String!) {
60
+ repository(owner: $owner, name: $name) {
61
+ id
62
+ }
63
+ }
64
+ `;
65
+ var PROJECT_ITEMS_QUERY = `
66
+ query($projectId: ID!) {
67
+ node(id: $projectId) {
68
+ ... on ProjectV2 {
69
+ items(first: 100) {
70
+ nodes {
71
+ id
72
+ fieldValues(first: 20) {
73
+ nodes {
74
+ __typename
75
+ ... on ProjectV2ItemFieldSingleSelectValue {
76
+ name
77
+ field { ... on ProjectV2SingleSelectField { name } }
78
+ }
79
+ ... on ProjectV2ItemFieldTextValue {
80
+ text
81
+ field { ... on ProjectV2Field { name } }
82
+ }
83
+ ... on ProjectV2ItemFieldNumberValue {
84
+ number
85
+ field { ... on ProjectV2Field { name } }
86
+ }
87
+ ... on ProjectV2ItemFieldDateValue {
88
+ date
89
+ field { ... on ProjectV2Field { name } }
90
+ }
91
+ ... on ProjectV2ItemFieldIterationValue {
92
+ title
93
+ field { ... on ProjectV2IterationField { name } }
94
+ }
95
+ }
96
+ }
97
+ content {
98
+ __typename
99
+ ... on Issue {
100
+ title
101
+ number
102
+ url
103
+ state
104
+ issueType { name }
105
+ assignees(first: 5) { nodes { login } }
106
+ labels(first: 10) { nodes { name color } }
107
+ repository { name }
108
+ }
109
+ ... on PullRequest {
110
+ title
111
+ number
112
+ url
113
+ state
114
+ merged
115
+ assignees(first: 5) { nodes { login } }
116
+ labels(first: 10) { nodes { name color } }
117
+ repository { name }
118
+ }
119
+ ... on DraftIssue {
120
+ title
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ `;
129
+ var PROJECT_FIELDS_QUERY = `
130
+ query($projectId: ID!) {
131
+ node(id: $projectId) {
132
+ ... on ProjectV2 {
133
+ fields(first: 30) {
134
+ nodes {
135
+ __typename
136
+ ... on ProjectV2Field {
137
+ id
138
+ name
139
+ }
140
+ ... on ProjectV2SingleSelectField {
141
+ id
142
+ name
143
+ options { id name }
144
+ }
145
+ ... on ProjectV2IterationField {
146
+ id
147
+ name
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ `;
155
+ var PROJECT_VIEWS_QUERY = `
156
+ query($projectId: ID!) {
157
+ node(id: $projectId) {
158
+ ... on ProjectV2 {
159
+ views(first: 20) {
160
+ nodes {
161
+ name
162
+ filter
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ `;
169
+ var UPDATE_ITEM_FIELD_MUTATION = `
170
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
171
+ updateProjectV2ItemFieldValue(input: {
172
+ projectId: $projectId
173
+ itemId: $itemId
174
+ fieldId: $fieldId
175
+ value: $value
176
+ }) {
177
+ projectV2Item { id }
178
+ }
179
+ }
180
+ `;
181
+ var UPDATE_ITEM_STATUS_MUTATION = `
182
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
183
+ updateProjectV2ItemFieldValue(input: {
184
+ projectId: $projectId
185
+ itemId: $itemId
186
+ fieldId: $fieldId
187
+ value: { singleSelectOptionId: $optionId }
188
+ }) {
189
+ projectV2Item { id }
190
+ }
191
+ }
192
+ `;
193
+ var CREATE_ISSUE_MUTATION = `
194
+ mutation($repositoryId: ID!, $title: String!, $body: String) {
195
+ createIssue(input: {
196
+ repositoryId: $repositoryId
197
+ title: $title
198
+ body: $body
199
+ }) {
200
+ issue {
201
+ id
202
+ number
203
+ }
204
+ }
205
+ }
206
+ `;
207
+ var ADD_TO_PROJECT_MUTATION = `
208
+ mutation($projectId: ID!, $contentId: ID!) {
209
+ addProjectV2ItemById(input: {
210
+ projectId: $projectId
211
+ contentId: $contentId
212
+ }) {
213
+ item { id }
214
+ }
215
+ }
216
+ `;
217
+ var ISSUE_DETAILS_QUERY = `
218
+ query($owner: String!, $name: String!, $number: Int!) {
219
+ repository(owner: $owner, name: $name) {
220
+ issueOrPullRequest(number: $number) {
221
+ __typename
222
+ ... on Issue {
223
+ title
224
+ body
225
+ state
226
+ createdAt
227
+ author { login }
228
+ labels(first: 10) { nodes { name color } }
229
+ comments(first: 50) {
230
+ totalCount
231
+ nodes {
232
+ author { login }
233
+ body
234
+ createdAt
235
+ }
236
+ }
237
+ }
238
+ ... on PullRequest {
239
+ title
240
+ body
241
+ state
242
+ createdAt
243
+ author { login }
244
+ labels(first: 10) { nodes { name color } }
245
+ comments(first: 50) {
246
+ totalCount
247
+ nodes {
248
+ author { login }
249
+ body
250
+ createdAt
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+ `;
258
+ var ISSUE_NODE_ID_QUERY = `
259
+ query($owner: String!, $name: String!, $number: Int!) {
260
+ repository(owner: $owner, name: $name) {
261
+ issueOrPullRequest(number: $number) {
262
+ ... on Issue { id }
263
+ ... on PullRequest { id }
264
+ }
265
+ }
266
+ }
267
+ `;
268
+ var ADD_COMMENT_MUTATION = `
269
+ mutation($subjectId: ID!, $body: String!) {
270
+ addComment(input: { subjectId: $subjectId, body: $body }) {
271
+ commentEdge {
272
+ node { id }
273
+ }
274
+ }
275
+ }
276
+ `;
277
+ var COLLABORATORS_QUERY = `
278
+ query($owner: String!, $name: String!) {
279
+ repository(owner: $owner, name: $name) {
280
+ collaborators(first: 50) {
281
+ nodes { login name }
282
+ }
283
+ assignableUsers(first: 50) {
284
+ nodes { login name }
285
+ }
286
+ }
287
+ }
288
+ `;
289
+ var RECENT_ISSUES_QUERY = `
290
+ query($owner: String!, $name: String!, $limit: Int!) {
291
+ repository(owner: $owner, name: $name) {
292
+ issues(first: $limit, orderBy: { field: UPDATED_AT, direction: DESC }) {
293
+ nodes {
294
+ number
295
+ title
296
+ state
297
+ }
298
+ }
299
+ }
300
+ }
301
+ `;
302
+ var LABEL_EXISTS_QUERY = `
303
+ query($owner: String!, $name: String!, $labelName: String!) {
304
+ repository(owner: $owner, name: $name) {
305
+ label(name: $labelName) {
306
+ id
307
+ }
308
+ }
309
+ }
310
+ `;
311
+ var ISSUE_AND_LABEL_QUERY = `
312
+ query($owner: String!, $name: String!, $number: Int!, $labelName: String!) {
313
+ repository(owner: $owner, name: $name) {
314
+ issue(number: $number) {
315
+ id
316
+ }
317
+ label(name: $labelName) {
318
+ id
319
+ }
320
+ }
321
+ }
322
+ `;
323
+ var ADD_LABELS_MUTATION = `
324
+ mutation($issueId: ID!, $labelIds: [ID!]!) {
325
+ addLabelsToLabelable(input: { labelableId: $issueId, labelIds: $labelIds }) {
326
+ clientMutationId
327
+ }
328
+ }
329
+ `;
330
+ var REMOVE_LABELS_MUTATION = `
331
+ mutation($issueId: ID!, $labelIds: [ID!]!) {
332
+ removeLabelsFromLabelable(input: { labelableId: $issueId, labelIds: $labelIds }) {
333
+ clientMutationId
334
+ }
335
+ }
336
+ `;
337
+ var ISSUES_WITH_LABEL_QUERY = `
338
+ query($owner: String!, $name: String!, $labels: [String!]) {
339
+ repository(owner: $owner, name: $name) {
340
+ issues(first: 10, labels: $labels, states: [OPEN]) {
341
+ nodes {
342
+ number
343
+ }
344
+ }
345
+ }
346
+ }
347
+ `;
348
+ var ISSUE_TYPES_QUERY = `
349
+ query($owner: String!, $name: String!) {
350
+ repository(owner: $owner, name: $name) {
351
+ issueTypes(first: 20) {
352
+ nodes {
353
+ id
354
+ name
355
+ }
356
+ }
357
+ }
358
+ }
359
+ `;
360
+ var ISSUE_FOR_UPDATE_QUERY = `
361
+ query($owner: String!, $name: String!, $number: Int!) {
362
+ repository(owner: $owner, name: $name) {
363
+ issue(number: $number) {
364
+ id
365
+ }
366
+ }
367
+ }
368
+ `;
369
+ var UPDATE_ISSUE_TYPE_MUTATION = `
370
+ mutation($issueId: ID!, $issueTypeId: ID!) {
371
+ updateIssue(input: { id: $issueId, issueTypeId: $issueTypeId }) {
372
+ issue {
373
+ id
374
+ }
375
+ }
376
+ }
377
+ `;
378
+
379
+ // src/github-api.ts
380
+ function createAuthError(message, type, details) {
381
+ const error = new Error(message);
382
+ error.type = type;
383
+ error.requiredScopes = details?.requiredScopes;
384
+ error.ssoUrl = details?.ssoUrl;
385
+ return error;
386
+ }
387
+ function checkAuthError(error) {
388
+ if (error && typeof error === "object" && "errors" in error) {
389
+ const gqlError = error;
390
+ const scopeError = gqlError.errors?.find((e) => e.type === "INSUFFICIENT_SCOPES");
391
+ if (scopeError) {
392
+ return createAuthError(
393
+ "Your GitHub token is missing required scopes. GitHub Projects requires the read:project scope.",
394
+ "INSUFFICIENT_SCOPES",
395
+ { requiredScopes: ["read:project", "project"] }
396
+ );
397
+ }
398
+ const ssoError = gqlError.errors?.find(
399
+ (e) => e.message?.includes("SSO") || e.message?.includes("SAML")
400
+ );
401
+ if (ssoError) {
402
+ return createAuthError(
403
+ "SSO authentication required for this organization.",
404
+ "SSO_REQUIRED"
405
+ );
406
+ }
407
+ }
408
+ return null;
409
+ }
410
+ var GitHubAPI = class {
411
+ graphqlWithAuth = null;
412
+ tokenProvider;
413
+ onAuthError;
414
+ username = null;
415
+ constructor(options) {
416
+ this.tokenProvider = options.tokenProvider;
417
+ this.onAuthError = options.onAuthError;
418
+ }
419
+ /**
420
+ * Handle authentication errors by calling the error handler or throwing
421
+ */
422
+ handleAuthError(error) {
423
+ const authError = checkAuthError(error);
424
+ if (authError) {
425
+ if (this.onAuthError) {
426
+ this.onAuthError(authError);
427
+ }
428
+ throw authError;
429
+ }
430
+ throw error;
431
+ }
432
+ /**
433
+ * Authenticate with GitHub using the token provider
434
+ */
435
+ async authenticate() {
436
+ const token = await this.tokenProvider.getToken();
437
+ if (!token) {
438
+ return false;
439
+ }
440
+ this.graphqlWithAuth = graphql.defaults({
441
+ headers: {
442
+ authorization: `token ${token}`
443
+ }
444
+ });
445
+ try {
446
+ const response = await this.graphqlWithAuth(VIEWER_QUERY);
447
+ this.username = response.viewer.login;
448
+ return true;
449
+ } catch {
450
+ this.graphqlWithAuth = null;
451
+ return false;
452
+ }
453
+ }
454
+ get isAuthenticated() {
455
+ return this.graphqlWithAuth !== null;
456
+ }
457
+ /**
458
+ * Get the current token (for REST API calls)
459
+ */
460
+ async getToken() {
461
+ return this.tokenProvider.getToken();
462
+ }
463
+ /**
464
+ * Get projects linked to a repository
465
+ */
466
+ async getProjects(repo) {
467
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
468
+ try {
469
+ const response = await this.graphqlWithAuth(REPOSITORY_PROJECTS_QUERY, {
470
+ owner: repo.owner,
471
+ name: repo.name
472
+ });
473
+ return response.repository.projectsV2.nodes;
474
+ } catch (error) {
475
+ this.handleAuthError(error);
476
+ }
477
+ }
478
+ /**
479
+ * Get items from a project
480
+ */
481
+ async getProjectItems(projectId, projectTitle) {
482
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
483
+ const statusField = await this.getStatusField(projectId);
484
+ const statusOrderMap = /* @__PURE__ */ new Map();
485
+ if (statusField) {
486
+ statusField.options.forEach((opt, idx) => {
487
+ statusOrderMap.set(opt.name.toLowerCase(), idx);
488
+ });
489
+ }
490
+ const response = await this.graphqlWithAuth(PROJECT_ITEMS_QUERY, { projectId });
491
+ return response.node.items.nodes.filter((item) => item.content).map((item) => {
492
+ const content = item.content;
493
+ const fields = {};
494
+ for (const fv of item.fieldValues.nodes) {
495
+ const fieldName = fv.field?.name;
496
+ if (!fieldName) continue;
497
+ if (fv.__typename === "ProjectV2ItemFieldSingleSelectValue" && fv.name) {
498
+ fields[fieldName] = fv.name;
499
+ } else if (fv.__typename === "ProjectV2ItemFieldTextValue" && fv.text) {
500
+ fields[fieldName] = fv.text;
501
+ } else if (fv.__typename === "ProjectV2ItemFieldNumberValue" && fv.number !== void 0) {
502
+ fields[fieldName] = fv.number.toString();
503
+ } else if (fv.__typename === "ProjectV2ItemFieldDateValue" && fv.date) {
504
+ fields[fieldName] = fv.date;
505
+ } else if (fv.__typename === "ProjectV2ItemFieldIterationValue" && fv.title) {
506
+ fields[fieldName] = fv.title;
507
+ }
508
+ }
509
+ let type = "draft";
510
+ if (content.__typename === "Issue") type = "issue";
511
+ else if (content.__typename === "PullRequest") type = "pull_request";
512
+ const status = fields["Status"] || null;
513
+ const statusIndex = status ? statusOrderMap.get(status.toLowerCase()) ?? 999 : 999;
514
+ let state = null;
515
+ if (content.state) {
516
+ if (content.merged) {
517
+ state = "merged";
518
+ } else if (content.state === "OPEN") {
519
+ state = "open";
520
+ } else {
521
+ state = "closed";
522
+ }
523
+ }
524
+ return {
525
+ id: item.id,
526
+ title: content.title || "Untitled",
527
+ number: content.number || null,
528
+ type,
529
+ issueType: content.issueType?.name || null,
530
+ status,
531
+ statusIndex,
532
+ state,
533
+ assignees: content.assignees?.nodes.map((a) => a.login) || [],
534
+ labels: content.labels?.nodes || [],
535
+ repository: content.repository?.name || null,
536
+ url: content.url || null,
537
+ projectId,
538
+ projectTitle,
539
+ fields
540
+ };
541
+ });
542
+ }
543
+ /**
544
+ * Get the Status field info for a project
545
+ */
546
+ async getStatusField(projectId) {
547
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
548
+ const response = await this.graphqlWithAuth(PROJECT_FIELDS_QUERY, { projectId });
549
+ const statusField = response.node.fields.nodes.find(
550
+ (f) => f.__typename === "ProjectV2SingleSelectField" && f.name === "Status"
551
+ );
552
+ if (!statusField || !statusField.options) return null;
553
+ return {
554
+ fieldId: statusField.id,
555
+ options: statusField.options
556
+ };
557
+ }
558
+ /**
559
+ * Get project views
560
+ */
561
+ async getProjectViews(projectId) {
562
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
563
+ try {
564
+ const response = await this.graphqlWithAuth(PROJECT_VIEWS_QUERY, { projectId });
565
+ return response.node.views.nodes;
566
+ } catch {
567
+ return [];
568
+ }
569
+ }
570
+ /**
571
+ * Update an item's status
572
+ */
573
+ async updateItemStatus(projectId, itemId, fieldId, optionId) {
574
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
575
+ try {
576
+ await this.graphqlWithAuth(UPDATE_ITEM_STATUS_MUTATION, {
577
+ projectId,
578
+ itemId,
579
+ fieldId,
580
+ optionId
581
+ });
582
+ return true;
583
+ } catch {
584
+ return false;
585
+ }
586
+ }
587
+ /**
588
+ * Find an item by issue number across all projects for this repo
589
+ */
590
+ async findItemByNumber(repo, issueNumber) {
591
+ const projects = await this.getProjects(repo);
592
+ for (const project of projects) {
593
+ const items = await this.getProjectItems(project.id, project.title);
594
+ const item = items.find((i) => i.number === issueNumber);
595
+ if (item) return item;
596
+ }
597
+ return null;
598
+ }
599
+ /**
600
+ * Get all fields for a project
601
+ */
602
+ async getProjectFields(projectId) {
603
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
604
+ const response = await this.graphqlWithAuth(PROJECT_FIELDS_QUERY, { projectId });
605
+ return response.node.fields.nodes.map((f) => ({
606
+ id: f.id,
607
+ name: f.name,
608
+ type: f.__typename.replace("ProjectV2", "").replace("Field", ""),
609
+ options: f.options
610
+ }));
611
+ }
612
+ /**
613
+ * Set a field value on a project item
614
+ */
615
+ async setFieldValue(projectId, itemId, fieldId, value) {
616
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
617
+ try {
618
+ await this.graphqlWithAuth(UPDATE_ITEM_FIELD_MUTATION, {
619
+ projectId,
620
+ itemId,
621
+ fieldId,
622
+ value
623
+ });
624
+ return true;
625
+ } catch {
626
+ return false;
627
+ }
628
+ }
629
+ /**
630
+ * Create a new issue
631
+ */
632
+ async createIssue(repo, title, body) {
633
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
634
+ try {
635
+ const repoResponse = await this.graphqlWithAuth(REPOSITORY_ID_QUERY, {
636
+ owner: repo.owner,
637
+ name: repo.name
638
+ });
639
+ const response = await this.graphqlWithAuth(CREATE_ISSUE_MUTATION, {
640
+ repositoryId: repoResponse.repository.id,
641
+ title,
642
+ body: body || ""
643
+ });
644
+ return response.createIssue.issue;
645
+ } catch {
646
+ return null;
647
+ }
648
+ }
649
+ /**
650
+ * Add an issue to a project
651
+ */
652
+ async addToProject(projectId, contentId) {
653
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
654
+ try {
655
+ const response = await this.graphqlWithAuth(ADD_TO_PROJECT_MUTATION, {
656
+ projectId,
657
+ contentId
658
+ });
659
+ return response.addProjectV2ItemById.item.id;
660
+ } catch {
661
+ return null;
662
+ }
663
+ }
664
+ /**
665
+ * Get full issue details including body and comments
666
+ */
667
+ async getIssueDetails(repo, issueNumber) {
668
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
669
+ try {
670
+ const response = await this.graphqlWithAuth(ISSUE_DETAILS_QUERY, {
671
+ owner: repo.owner,
672
+ name: repo.name,
673
+ number: issueNumber
674
+ });
675
+ const issue = response.repository.issueOrPullRequest;
676
+ if (!issue) return null;
677
+ return {
678
+ title: issue.title,
679
+ body: issue.body,
680
+ state: issue.state,
681
+ type: issue.__typename === "PullRequest" ? "pull_request" : "issue",
682
+ createdAt: issue.createdAt,
683
+ author: issue.author?.login || "unknown",
684
+ labels: issue.labels.nodes,
685
+ comments: issue.comments.nodes.map((c) => ({
686
+ author: c.author?.login || "unknown",
687
+ body: c.body,
688
+ createdAt: c.createdAt
689
+ })),
690
+ totalComments: issue.comments.totalCount
691
+ };
692
+ } catch {
693
+ return null;
694
+ }
695
+ }
696
+ /**
697
+ * Add a comment to an issue or PR
698
+ */
699
+ async addComment(repo, issueNumber, body) {
700
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
701
+ try {
702
+ const issueResponse = await this.graphqlWithAuth(ISSUE_NODE_ID_QUERY, {
703
+ owner: repo.owner,
704
+ name: repo.name,
705
+ number: issueNumber
706
+ });
707
+ const subjectId = issueResponse.repository.issueOrPullRequest?.id;
708
+ if (!subjectId) {
709
+ return false;
710
+ }
711
+ await this.graphqlWithAuth(ADD_COMMENT_MUTATION, { subjectId, body });
712
+ return true;
713
+ } catch {
714
+ return false;
715
+ }
716
+ }
717
+ /**
718
+ * Get repository collaborators (for @ mention suggestions)
719
+ */
720
+ async getCollaborators(repo) {
721
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
722
+ try {
723
+ const response = await this.graphqlWithAuth(COLLABORATORS_QUERY, {
724
+ owner: repo.owner,
725
+ name: repo.name
726
+ });
727
+ const users = response.repository.collaborators?.nodes || response.repository.assignableUsers.nodes || [];
728
+ return users.map((u) => ({ login: u.login, name: u.name }));
729
+ } catch {
730
+ return [];
731
+ }
732
+ }
733
+ /**
734
+ * Get recent issues (for # reference suggestions)
735
+ */
736
+ async getRecentIssues(repo, limit = 20) {
737
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
738
+ try {
739
+ const response = await this.graphqlWithAuth(RECENT_ISSUES_QUERY, {
740
+ owner: repo.owner,
741
+ name: repo.name,
742
+ limit
743
+ });
744
+ return response.repository.issues.nodes;
745
+ } catch {
746
+ return [];
747
+ }
748
+ }
749
+ /**
750
+ * Get the active label name for a user
751
+ */
752
+ getActiveLabelName() {
753
+ return `@${this.username}:active`;
754
+ }
755
+ /**
756
+ * Ensure a label exists in the repository, create if it doesn't
757
+ */
758
+ async ensureLabel(repo, labelName, color = "1f883d") {
759
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
760
+ try {
761
+ const checkResponse = await this.graphqlWithAuth(LABEL_EXISTS_QUERY, {
762
+ owner: repo.owner,
763
+ name: repo.name,
764
+ labelName
765
+ });
766
+ if (checkResponse.repository.label) {
767
+ return true;
768
+ }
769
+ const token = await this.getToken();
770
+ const response = await fetch(
771
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/labels`,
772
+ {
773
+ method: "POST",
774
+ headers: {
775
+ "Authorization": `token ${token}`,
776
+ "Content-Type": "application/json",
777
+ "Accept": "application/vnd.github.v3+json"
778
+ },
779
+ body: JSON.stringify({
780
+ name: labelName,
781
+ color,
782
+ description: `Active working indicator for ${this.username}`
783
+ })
784
+ }
785
+ );
786
+ return response.status === 201 || response.status === 422;
787
+ } catch {
788
+ return false;
789
+ }
790
+ }
791
+ /**
792
+ * Add a label to an issue
793
+ */
794
+ async addLabelToIssue(repo, issueNumber, labelName) {
795
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
796
+ try {
797
+ const response = await this.graphqlWithAuth(ISSUE_AND_LABEL_QUERY, {
798
+ owner: repo.owner,
799
+ name: repo.name,
800
+ number: issueNumber,
801
+ labelName
802
+ });
803
+ if (!response.repository.issue || !response.repository.label) {
804
+ return false;
805
+ }
806
+ await this.graphqlWithAuth(ADD_LABELS_MUTATION, {
807
+ issueId: response.repository.issue.id,
808
+ labelIds: [response.repository.label.id]
809
+ });
810
+ return true;
811
+ } catch {
812
+ return false;
813
+ }
814
+ }
815
+ /**
816
+ * Remove a label from an issue
817
+ */
818
+ async removeLabelFromIssue(repo, issueNumber, labelName) {
819
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
820
+ try {
821
+ const response = await this.graphqlWithAuth(ISSUE_AND_LABEL_QUERY, {
822
+ owner: repo.owner,
823
+ name: repo.name,
824
+ number: issueNumber,
825
+ labelName
826
+ });
827
+ if (!response.repository.issue || !response.repository.label) {
828
+ return false;
829
+ }
830
+ await this.graphqlWithAuth(REMOVE_LABELS_MUTATION, {
831
+ issueId: response.repository.issue.id,
832
+ labelIds: [response.repository.label.id]
833
+ });
834
+ return true;
835
+ } catch {
836
+ return false;
837
+ }
838
+ }
839
+ /**
840
+ * Find all issues with a specific label
841
+ */
842
+ async findIssuesWithLabel(repo, labelName) {
843
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
844
+ try {
845
+ const response = await this.graphqlWithAuth(ISSUES_WITH_LABEL_QUERY, {
846
+ owner: repo.owner,
847
+ name: repo.name,
848
+ labels: [labelName]
849
+ });
850
+ return response.repository.issues.nodes.map((i) => i.number);
851
+ } catch {
852
+ return [];
853
+ }
854
+ }
855
+ /**
856
+ * Get available issue types for a repository
857
+ */
858
+ async getIssueTypes(repo) {
859
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
860
+ try {
861
+ const response = await this.graphqlWithAuth(ISSUE_TYPES_QUERY, {
862
+ owner: repo.owner,
863
+ name: repo.name
864
+ });
865
+ return response.repository.issueTypes?.nodes || [];
866
+ } catch {
867
+ return [];
868
+ }
869
+ }
870
+ /**
871
+ * Set the issue type on an issue
872
+ */
873
+ async setIssueType(repo, issueNumber, issueTypeId) {
874
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
875
+ try {
876
+ const issueResponse = await this.graphqlWithAuth(ISSUE_FOR_UPDATE_QUERY, {
877
+ owner: repo.owner,
878
+ name: repo.name,
879
+ number: issueNumber
880
+ });
881
+ if (!issueResponse.repository.issue) {
882
+ return false;
883
+ }
884
+ await this.graphqlWithAuth(UPDATE_ISSUE_TYPE_MUTATION, {
885
+ issueId: issueResponse.repository.issue.id,
886
+ issueTypeId
887
+ });
888
+ return true;
889
+ } catch {
890
+ return false;
891
+ }
892
+ }
893
+ };
894
+
895
+ // src/branch-linker.ts
896
+ var BranchLinker = class {
897
+ storage;
898
+ constructor(storage) {
899
+ this.storage = storage;
900
+ }
901
+ /**
902
+ * Load links from storage (handles both sync and async adapters)
903
+ */
904
+ async loadLinks() {
905
+ const result = this.storage.load();
906
+ return result instanceof Promise ? await result : result;
907
+ }
908
+ /**
909
+ * Save links to storage (handles both sync and async adapters)
910
+ */
911
+ async saveLinks(links) {
912
+ const result = this.storage.save(links);
913
+ if (result instanceof Promise) {
914
+ await result;
915
+ }
916
+ }
917
+ /**
918
+ * Create a link between a branch and an issue.
919
+ * If a link already exists for this branch or issue in this repo, it will be replaced.
920
+ */
921
+ async link(branch, issueNumber, issueTitle, itemId, repo) {
922
+ const links = await this.loadLinks();
923
+ const filtered = links.filter(
924
+ (l) => !(l.repo === repo && (l.branch === branch || l.issueNumber === issueNumber))
925
+ );
926
+ filtered.push({
927
+ branch,
928
+ issueNumber,
929
+ issueTitle,
930
+ itemId,
931
+ repo,
932
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
933
+ });
934
+ await this.saveLinks(filtered);
935
+ }
936
+ /**
937
+ * Remove the link for an issue.
938
+ * @returns true if a link was removed, false if no link existed
939
+ */
940
+ async unlink(repo, issueNumber) {
941
+ const links = await this.loadLinks();
942
+ const filtered = links.filter(
943
+ (l) => !(l.repo === repo && l.issueNumber === issueNumber)
944
+ );
945
+ if (filtered.length === links.length) {
946
+ return false;
947
+ }
948
+ await this.saveLinks(filtered);
949
+ return true;
950
+ }
951
+ /**
952
+ * Remove the link for a branch.
953
+ * @returns true if a link was removed, false if no link existed
954
+ */
955
+ async unlinkBranch(repo, branch) {
956
+ const links = await this.loadLinks();
957
+ const filtered = links.filter(
958
+ (l) => !(l.repo === repo && l.branch === branch)
959
+ );
960
+ if (filtered.length === links.length) {
961
+ return false;
962
+ }
963
+ await this.saveLinks(filtered);
964
+ return true;
965
+ }
966
+ /**
967
+ * Get the branch linked to an issue.
968
+ */
969
+ async getBranchForIssue(repo, issueNumber) {
970
+ const links = await this.loadLinks();
971
+ const link = links.find((l) => l.repo === repo && l.issueNumber === issueNumber);
972
+ return link?.branch || null;
973
+ }
974
+ /**
975
+ * Get the full link info for a branch.
976
+ */
977
+ async getLinkForBranch(repo, branch) {
978
+ const links = await this.loadLinks();
979
+ return links.find((l) => l.repo === repo && l.branch === branch) || null;
980
+ }
981
+ /**
982
+ * Get the full link info for an issue.
983
+ */
984
+ async getLinkForIssue(repo, issueNumber) {
985
+ const links = await this.loadLinks();
986
+ return links.find((l) => l.repo === repo && l.issueNumber === issueNumber) || null;
987
+ }
988
+ /**
989
+ * Get all links for a repository.
990
+ */
991
+ async getLinksForRepo(repo) {
992
+ const links = await this.loadLinks();
993
+ return links.filter((l) => l.repo === repo);
994
+ }
995
+ /**
996
+ * Get all links.
997
+ */
998
+ async getAllLinks() {
999
+ return this.loadLinks();
1000
+ }
1001
+ /**
1002
+ * Check if a branch has a link.
1003
+ */
1004
+ async hasLinkForBranch(repo, branch) {
1005
+ const link = await this.getLinkForBranch(repo, branch);
1006
+ return link !== null;
1007
+ }
1008
+ /**
1009
+ * Check if an issue has a link.
1010
+ */
1011
+ async hasLinkForIssue(repo, issueNumber) {
1012
+ const link = await this.getLinkForIssue(repo, issueNumber);
1013
+ return link !== null;
1014
+ }
1015
+ };
1016
+ function createInMemoryAdapter() {
1017
+ const adapter = {
1018
+ links: [],
1019
+ load() {
1020
+ return [...this.links];
1021
+ },
1022
+ save(links) {
1023
+ this.links = [...links];
1024
+ }
1025
+ };
1026
+ return adapter;
1027
+ }
1028
+
1029
+ // src/git-utils.ts
1030
+ import { exec } from "child_process";
1031
+ import { promisify } from "util";
1032
+
1033
+ // src/url-parser.ts
1034
+ function parseGitHubUrl(url) {
1035
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
1036
+ if (sshMatch) {
1037
+ return {
1038
+ owner: sshMatch[1],
1039
+ name: sshMatch[2],
1040
+ fullName: `${sshMatch[1]}/${sshMatch[2]}`
1041
+ };
1042
+ }
1043
+ const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
1044
+ if (httpsMatch) {
1045
+ return {
1046
+ owner: httpsMatch[1],
1047
+ name: httpsMatch[2],
1048
+ fullName: `${httpsMatch[1]}/${httpsMatch[2]}`
1049
+ };
1050
+ }
1051
+ return null;
1052
+ }
1053
+ function parseIssueUrl(url) {
1054
+ const match = url.match(
1055
+ /https:\/\/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)/
1056
+ );
1057
+ if (match) {
1058
+ return {
1059
+ owner: match[1],
1060
+ repo: match[2],
1061
+ number: parseInt(match[4], 10),
1062
+ type: match[3] === "pull" ? "pull" : "issue"
1063
+ };
1064
+ }
1065
+ return null;
1066
+ }
1067
+ function buildIssueUrl(owner, repo, number) {
1068
+ return `https://github.com/${owner}/${repo}/issues/${number}`;
1069
+ }
1070
+ function buildPullRequestUrl(owner, repo, number) {
1071
+ return `https://github.com/${owner}/${repo}/pull/${number}`;
1072
+ }
1073
+ function buildRepoUrl(owner, repo) {
1074
+ return `https://github.com/${owner}/${repo}`;
1075
+ }
1076
+ function buildProjectUrl(owner, projectNumber) {
1077
+ return `https://github.com/users/${owner}/projects/${projectNumber}`;
1078
+ }
1079
+ function buildOrgProjectUrl(org, projectNumber) {
1080
+ return `https://github.com/orgs/${org}/projects/${projectNumber}`;
1081
+ }
1082
+
1083
+ // src/git-utils.ts
1084
+ var execAsync = promisify(exec);
1085
+ async function execGit(command, options = {}) {
1086
+ const cwd = options.cwd || process.cwd();
1087
+ return execAsync(command, { cwd });
1088
+ }
1089
+ async function detectRepository(options = {}) {
1090
+ try {
1091
+ const { stdout } = await execGit("git remote get-url origin", options);
1092
+ const url = stdout.trim();
1093
+ return parseGitHubUrl(url);
1094
+ } catch {
1095
+ return null;
1096
+ }
1097
+ }
1098
+ async function getCurrentBranch(options = {}) {
1099
+ try {
1100
+ const { stdout } = await execGit("git branch --show-current", options);
1101
+ return stdout.trim() || null;
1102
+ } catch {
1103
+ return null;
1104
+ }
1105
+ }
1106
+ async function hasUncommittedChanges(options = {}) {
1107
+ try {
1108
+ const { stdout } = await execGit("git status --porcelain", options);
1109
+ return stdout.trim().length > 0;
1110
+ } catch {
1111
+ return false;
1112
+ }
1113
+ }
1114
+ async function branchExists(branchName, options = {}) {
1115
+ try {
1116
+ await execGit(`git show-ref --verify --quiet refs/heads/${branchName}`, options);
1117
+ return true;
1118
+ } catch {
1119
+ return false;
1120
+ }
1121
+ }
1122
+ async function createBranch(branchName, options = {}) {
1123
+ await execGit(`git checkout -b "${branchName}"`, options);
1124
+ }
1125
+ async function checkoutBranch(branchName, options = {}) {
1126
+ await execGit(`git checkout "${branchName}"`, options);
1127
+ }
1128
+ async function pullLatest(options = {}) {
1129
+ await execGit("git pull", options);
1130
+ }
1131
+ async function fetchOrigin(options = {}) {
1132
+ await execGit("git fetch origin", options);
1133
+ }
1134
+ async function getCommitsBehind(branch, options = {}) {
1135
+ try {
1136
+ await fetchOrigin(options);
1137
+ const { stdout } = await execGit(
1138
+ `git rev-list --count ${branch}..origin/${branch}`,
1139
+ options
1140
+ );
1141
+ return parseInt(stdout.trim(), 10) || 0;
1142
+ } catch {
1143
+ return 0;
1144
+ }
1145
+ }
1146
+ async function getCommitsAhead(branch, options = {}) {
1147
+ try {
1148
+ await fetchOrigin(options);
1149
+ const { stdout } = await execGit(
1150
+ `git rev-list --count origin/${branch}..${branch}`,
1151
+ options
1152
+ );
1153
+ return parseInt(stdout.trim(), 10) || 0;
1154
+ } catch {
1155
+ return 0;
1156
+ }
1157
+ }
1158
+ async function isGitRepository(options = {}) {
1159
+ try {
1160
+ await execGit("git rev-parse --git-dir", options);
1161
+ return true;
1162
+ } catch {
1163
+ return false;
1164
+ }
1165
+ }
1166
+ async function getRepositoryRoot(options = {}) {
1167
+ try {
1168
+ const { stdout } = await execGit("git rev-parse --show-toplevel", options);
1169
+ return stdout.trim();
1170
+ } catch {
1171
+ return null;
1172
+ }
1173
+ }
1174
+ function sanitizeForBranchName(str) {
1175
+ return str.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 50);
1176
+ }
1177
+ function generateBranchName(pattern, vars, maxLength = 60) {
1178
+ const sanitizedTitle = sanitizeForBranchName(vars.title);
1179
+ let branch = pattern.replace("{user}", vars.user).replace("{number}", vars.number?.toString() || "draft").replace("{title}", sanitizedTitle).replace("{repo}", vars.repo);
1180
+ if (branch.length > maxLength) {
1181
+ branch = branch.substring(0, maxLength).replace(/-$/, "");
1182
+ }
1183
+ return branch;
1184
+ }
1185
+ async function getDefaultBranch(options = {}) {
1186
+ try {
1187
+ const { stdout } = await execGit(
1188
+ "git symbolic-ref refs/remotes/origin/HEAD",
1189
+ options
1190
+ );
1191
+ const ref = stdout.trim();
1192
+ const match = ref.match(/refs\/remotes\/origin\/(.+)/);
1193
+ if (match) {
1194
+ return match[1];
1195
+ }
1196
+ } catch {
1197
+ }
1198
+ if (await branchExists("main", options)) {
1199
+ return "main";
1200
+ }
1201
+ return "master";
1202
+ }
1203
+ export {
1204
+ BranchLinker,
1205
+ GitHubAPI,
1206
+ branchExists,
1207
+ buildIssueUrl,
1208
+ buildOrgProjectUrl,
1209
+ buildProjectUrl,
1210
+ buildPullRequestUrl,
1211
+ buildRepoUrl,
1212
+ checkoutBranch,
1213
+ createBranch,
1214
+ createInMemoryAdapter,
1215
+ detectRepository,
1216
+ fetchOrigin,
1217
+ generateBranchName,
1218
+ getCommitsAhead,
1219
+ getCommitsBehind,
1220
+ getCurrentBranch,
1221
+ getDefaultBranch,
1222
+ getRepositoryRoot,
1223
+ hasUncommittedChanges,
1224
+ isGitRepository,
1225
+ parseGitHubUrl,
1226
+ parseIssueUrl,
1227
+ pullLatest,
1228
+ queries_exports as queries,
1229
+ sanitizeForBranchName
1230
+ };