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