@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 +1276 -0
- package/dist/index.d.cts +885 -0
- package/dist/index.d.ts +885 -0
- package/dist/index.js +1230 -0
- package/package.json +48 -0
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
|
+
});
|