@happyvertical/repos 0.79.0 → 0.80.1
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/AGENT.md +1 -1
- package/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/index.js +700 -671
- package/dist/index.js.map +1 -1
- package/metadata.json +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,462 +1,417 @@
|
|
|
1
1
|
import { GraphQLClient } from "@happyvertical/graphql";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/errors.ts
|
|
17
|
+
/**
|
|
18
|
+
* Repository error handling
|
|
19
|
+
*/
|
|
20
|
+
var RepositoryErrorCode = /* @__PURE__ */ function(RepositoryErrorCode) {
|
|
21
|
+
RepositoryErrorCode["NOT_FOUND"] = "NOT_FOUND";
|
|
22
|
+
RepositoryErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
|
|
23
|
+
RepositoryErrorCode["FORBIDDEN"] = "FORBIDDEN";
|
|
24
|
+
RepositoryErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
|
|
25
|
+
RepositoryErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
26
|
+
RepositoryErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
27
|
+
RepositoryErrorCode["UNKNOWN"] = "UNKNOWN";
|
|
28
|
+
return RepositoryErrorCode;
|
|
29
|
+
}({});
|
|
30
|
+
var RepositoryError = class RepositoryError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
statusCode;
|
|
33
|
+
response;
|
|
34
|
+
constructor(message, code, statusCode, response) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.statusCode = statusCode;
|
|
38
|
+
this.response = response;
|
|
39
|
+
this.name = "RepositoryError";
|
|
40
|
+
Error.captureStackTrace(this, this.constructor);
|
|
41
|
+
}
|
|
42
|
+
static fromHTTPStatus(statusCode, message, response) {
|
|
43
|
+
let code;
|
|
44
|
+
switch (statusCode) {
|
|
45
|
+
case 404:
|
|
46
|
+
code = "NOT_FOUND";
|
|
47
|
+
break;
|
|
48
|
+
case 401:
|
|
49
|
+
code = "UNAUTHORIZED";
|
|
50
|
+
break;
|
|
51
|
+
case 403:
|
|
52
|
+
code = "FORBIDDEN";
|
|
53
|
+
break;
|
|
54
|
+
case 429:
|
|
55
|
+
code = "RATE_LIMITED";
|
|
56
|
+
break;
|
|
57
|
+
case 422:
|
|
58
|
+
code = "VALIDATION_ERROR";
|
|
59
|
+
break;
|
|
60
|
+
default: code = "UNKNOWN";
|
|
61
|
+
}
|
|
62
|
+
return new RepositoryError(message, code, statusCode, response);
|
|
63
|
+
}
|
|
64
|
+
static networkError(message, cause) {
|
|
65
|
+
return new RepositoryError(message, "NETWORK_ERROR", void 0, cause);
|
|
66
|
+
}
|
|
67
|
+
isRetryable() {
|
|
68
|
+
return this.code === "RATE_LIMITED" || this.code === "NETWORK_ERROR";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/factory.ts
|
|
73
|
+
/**
|
|
74
|
+
* Repository factory function
|
|
75
|
+
*
|
|
76
|
+
* Creates repository instances following the pattern from @happyvertical/files and @happyvertical/sql
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* Check if value is already a repository instance
|
|
80
|
+
*/
|
|
58
81
|
function isRepositoryInstance(value) {
|
|
59
|
-
|
|
82
|
+
return value !== null && typeof value === "object" && "getIssue" in value && "createIssue" in value && "addLabels" in value && typeof value.getIssue === "function";
|
|
60
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Get a repository client instance
|
|
86
|
+
*
|
|
87
|
+
* This is the main entry point following the pattern from @happyvertical/files and @happyvertical/sql.
|
|
88
|
+
* It can accept either a configuration object or an existing repository instance.
|
|
89
|
+
*
|
|
90
|
+
* @param options - Repository configuration or existing repository instance
|
|
91
|
+
* @returns Promise resolving to repository interface
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* import { getRepository } from '@happyvertical/repos';
|
|
96
|
+
*
|
|
97
|
+
* // Create a GitHub repository client
|
|
98
|
+
* const repo = await getRepository({
|
|
99
|
+
* type: 'github',
|
|
100
|
+
* owner: 'happyvertical',
|
|
101
|
+
* repo: 'sdk',
|
|
102
|
+
* token: process.env.GITHUB_TOKEN
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Use the client
|
|
106
|
+
* const issue = await repo.getIssue(352);
|
|
107
|
+
* await repo.addLabels(352, ['type: feature', 'priority: high']);
|
|
108
|
+
*
|
|
109
|
+
* // Pass existing instance (returns it unchanged)
|
|
110
|
+
* const sameRepo = await getRepository(repo);
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
61
113
|
async function getRepository(options) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (!options.token) {
|
|
78
|
-
throw new RepositoryError(
|
|
79
|
-
"Authentication token is required",
|
|
80
|
-
RepositoryErrorCode.UNAUTHORIZED
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
switch (options.type) {
|
|
84
|
-
case "github": {
|
|
85
|
-
const { GitHubRepository: GitHubRepository2 } = await Promise.resolve().then(() => index);
|
|
86
|
-
return new GitHubRepository2(options);
|
|
87
|
-
}
|
|
88
|
-
case "gitlab":
|
|
89
|
-
throw new RepositoryError(
|
|
90
|
-
"GitLab support not yet implemented",
|
|
91
|
-
RepositoryErrorCode.UNKNOWN
|
|
92
|
-
);
|
|
93
|
-
case "bitbucket":
|
|
94
|
-
throw new RepositoryError(
|
|
95
|
-
"Bitbucket support not yet implemented",
|
|
96
|
-
RepositoryErrorCode.UNKNOWN
|
|
97
|
-
);
|
|
98
|
-
case "azure":
|
|
99
|
-
throw new RepositoryError(
|
|
100
|
-
"Azure DevOps support not yet implemented",
|
|
101
|
-
RepositoryErrorCode.UNKNOWN
|
|
102
|
-
);
|
|
103
|
-
default:
|
|
104
|
-
throw new RepositoryError(
|
|
105
|
-
`Unsupported repository type: ${options.type}`,
|
|
106
|
-
RepositoryErrorCode.VALIDATION_ERROR
|
|
107
|
-
);
|
|
108
|
-
}
|
|
114
|
+
if (isRepositoryInstance(options)) return options;
|
|
115
|
+
if (!options.type) throw new RepositoryError("Repository type is required", RepositoryErrorCode.VALIDATION_ERROR);
|
|
116
|
+
if (!options.owner || !options.repo) throw new RepositoryError("Repository owner and repo are required", RepositoryErrorCode.VALIDATION_ERROR);
|
|
117
|
+
if (!options.token) throw new RepositoryError("Authentication token is required", RepositoryErrorCode.UNAUTHORIZED);
|
|
118
|
+
switch (options.type) {
|
|
119
|
+
case "github": {
|
|
120
|
+
const { GitHubRepository } = await Promise.resolve().then(() => github_exports);
|
|
121
|
+
return new GitHubRepository(options);
|
|
122
|
+
}
|
|
123
|
+
case "gitlab": throw new RepositoryError("GitLab support not yet implemented", RepositoryErrorCode.UNKNOWN);
|
|
124
|
+
case "bitbucket": throw new RepositoryError("Bitbucket support not yet implemented", RepositoryErrorCode.UNKNOWN);
|
|
125
|
+
case "azure": throw new RepositoryError("Azure DevOps support not yet implemented", RepositoryErrorCode.UNKNOWN);
|
|
126
|
+
default: throw new RepositoryError(`Unsupported repository type: ${options.type}`, RepositoryErrorCode.VALIDATION_ERROR);
|
|
127
|
+
}
|
|
109
128
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
searchQuery += ` author:${filters.author}`;
|
|
397
|
-
}
|
|
398
|
-
if (filters?.assignee) {
|
|
399
|
-
searchQuery += ` assignee:${filters.assignee}`;
|
|
400
|
-
}
|
|
401
|
-
const params = new URLSearchParams({
|
|
402
|
-
q: searchQuery,
|
|
403
|
-
sort: filters?.sort || "created",
|
|
404
|
-
order: filters?.order || "desc",
|
|
405
|
-
per_page: String(filters?.limit || 30)
|
|
406
|
-
});
|
|
407
|
-
const data = await this.rest.get(`/search/issues?${params}`);
|
|
408
|
-
return Promise.all(data.items.map((item) => this.getIssue(item.number)));
|
|
409
|
-
}
|
|
410
|
-
// Node ID resolution
|
|
411
|
-
async getIssueNodeId(issueNumber) {
|
|
412
|
-
const issue = await this.getIssue(issueNumber);
|
|
413
|
-
return issue.id;
|
|
414
|
-
}
|
|
415
|
-
async getPRNodeId(prNumber) {
|
|
416
|
-
const pr = await this.getPullRequest(prNumber);
|
|
417
|
-
return pr.id;
|
|
418
|
-
}
|
|
419
|
-
// Branch Management
|
|
420
|
-
async createBranch(name, fromRef) {
|
|
421
|
-
const refData = await this.rest.get(
|
|
422
|
-
`/repos/${this.owner}/${this.repo}/git/ref/heads/${fromRef}`
|
|
423
|
-
);
|
|
424
|
-
await this.rest.post(`/repos/${this.owner}/${this.repo}/git/refs`, {
|
|
425
|
-
ref: `refs/heads/${name}`,
|
|
426
|
-
sha: refData.object.sha
|
|
427
|
-
});
|
|
428
|
-
return {
|
|
429
|
-
name,
|
|
430
|
-
sha: refData.object.sha,
|
|
431
|
-
protected: false
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
async deleteBranch(name) {
|
|
435
|
-
await this.rest.delete(
|
|
436
|
-
`/repos/${this.owner}/${this.repo}/git/refs/heads/${name}`
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
async getBranch(name) {
|
|
440
|
-
try {
|
|
441
|
-
const data = await this.rest.get(
|
|
442
|
-
`/repos/${this.owner}/${this.repo}/branches/${encodeURIComponent(name)}`
|
|
443
|
-
);
|
|
444
|
-
return {
|
|
445
|
-
name: data.name,
|
|
446
|
-
sha: data.commit.sha,
|
|
447
|
-
protected: data.protected
|
|
448
|
-
};
|
|
449
|
-
} catch (error) {
|
|
450
|
-
if (error instanceof Error && "code" in error && error.code === "NOT_FOUND") {
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
throw error;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// PR Draft/Review
|
|
457
|
-
async markPRReady(prNumber) {
|
|
458
|
-
const pr = await this.getPullRequest(prNumber);
|
|
459
|
-
const mutation = `
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/github/rest.ts
|
|
131
|
+
/**
|
|
132
|
+
* GitHub REST API client
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* GitHub REST API client
|
|
136
|
+
*/
|
|
137
|
+
var GitHubRest = class {
|
|
138
|
+
token;
|
|
139
|
+
baseUrl;
|
|
140
|
+
constructor(config) {
|
|
141
|
+
this.token = config.token;
|
|
142
|
+
this.baseUrl = config.baseUrl || "https://api.github.com";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Make a REST API request
|
|
146
|
+
*/
|
|
147
|
+
async request(method, path, body) {
|
|
148
|
+
const url = `${this.baseUrl}${path}`;
|
|
149
|
+
const headers = {
|
|
150
|
+
Authorization: `Bearer ${this.token}`,
|
|
151
|
+
Accept: "application/vnd.github+json",
|
|
152
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
153
|
+
"Content-Type": "application/json"
|
|
154
|
+
};
|
|
155
|
+
const response = await fetch(url, {
|
|
156
|
+
method,
|
|
157
|
+
headers,
|
|
158
|
+
body: body ? JSON.stringify(body) : void 0
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const error = await response.text();
|
|
162
|
+
throw RepositoryError.fromHTTPStatus(response.status, `GitHub API error: ${response.statusText}\n${error}`, error);
|
|
163
|
+
}
|
|
164
|
+
return response.json();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* GET request
|
|
168
|
+
*/
|
|
169
|
+
async get(path) {
|
|
170
|
+
return this.request("GET", path);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* POST request
|
|
174
|
+
*/
|
|
175
|
+
async post(path, body) {
|
|
176
|
+
return this.request("POST", path, body);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* PATCH request
|
|
180
|
+
*/
|
|
181
|
+
async patch(path, body) {
|
|
182
|
+
return this.request("PATCH", path, body);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* PUT request
|
|
186
|
+
*/
|
|
187
|
+
async put(path, body) {
|
|
188
|
+
return this.request("PUT", path, body);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* DELETE request
|
|
192
|
+
*/
|
|
193
|
+
async delete(path) {
|
|
194
|
+
await this.request("DELETE", path);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/github/index.ts
|
|
199
|
+
/**
|
|
200
|
+
* GitHub repository implementation
|
|
201
|
+
*/
|
|
202
|
+
var github_exports = /* @__PURE__ */ __exportAll({ GitHubRepository: () => GitHubRepository });
|
|
203
|
+
/**
|
|
204
|
+
* GitHub repository implementation
|
|
205
|
+
*/
|
|
206
|
+
var GitHubRepository = class {
|
|
207
|
+
rest;
|
|
208
|
+
graphql;
|
|
209
|
+
owner;
|
|
210
|
+
repo;
|
|
211
|
+
constructor(config) {
|
|
212
|
+
if (config.type !== "github") throw new Error("Invalid config type for GitHubRepository");
|
|
213
|
+
this.owner = config.owner;
|
|
214
|
+
this.repo = config.repo;
|
|
215
|
+
this.rest = new GitHubRest({
|
|
216
|
+
token: config.token,
|
|
217
|
+
baseUrl: config.baseUrl
|
|
218
|
+
});
|
|
219
|
+
this.graphql = new GraphQLClient({
|
|
220
|
+
endpoint: config.baseUrl ? `${config.baseUrl}/graphql` : "https://api.github.com/graphql",
|
|
221
|
+
token: config.token
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async getRepository() {
|
|
225
|
+
const data = await this.rest.get(`/repos/${this.owner}/${this.repo}`);
|
|
226
|
+
return {
|
|
227
|
+
owner: data.owner.login,
|
|
228
|
+
name: data.name,
|
|
229
|
+
description: data.description,
|
|
230
|
+
defaultBranch: data.default_branch,
|
|
231
|
+
url: data.html_url,
|
|
232
|
+
isPrivate: data.private
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async getIssue(number) {
|
|
236
|
+
const data = await this.rest.get(`/repos/${this.owner}/${this.repo}/issues/${number}`);
|
|
237
|
+
return {
|
|
238
|
+
number: data.number,
|
|
239
|
+
id: data.node_id,
|
|
240
|
+
title: data.title,
|
|
241
|
+
body: data.body || "",
|
|
242
|
+
state: data.state === "open" ? "open" : "closed",
|
|
243
|
+
labels: data.labels.map((l) => ({
|
|
244
|
+
name: l.name,
|
|
245
|
+
color: l.color,
|
|
246
|
+
description: l.description
|
|
247
|
+
})),
|
|
248
|
+
assignees: data.assignees.map((a) => ({
|
|
249
|
+
login: a.login,
|
|
250
|
+
id: String(a.id),
|
|
251
|
+
type: a.type === "Bot" ? "Bot" : "User"
|
|
252
|
+
})),
|
|
253
|
+
author: {
|
|
254
|
+
login: data.user.login,
|
|
255
|
+
id: String(data.user.id),
|
|
256
|
+
type: data.user.type === "Bot" ? "Bot" : "User"
|
|
257
|
+
},
|
|
258
|
+
createdAt: new Date(data.created_at),
|
|
259
|
+
updatedAt: new Date(data.updated_at),
|
|
260
|
+
closedAt: data.closed_at ? new Date(data.closed_at) : void 0,
|
|
261
|
+
url: data.html_url,
|
|
262
|
+
commentsCount: data.comments
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async createIssue(data) {
|
|
266
|
+
const result = await this.rest.post(`/repos/${this.owner}/${this.repo}/issues`, data);
|
|
267
|
+
return this.getIssue(result.number);
|
|
268
|
+
}
|
|
269
|
+
async updateIssue(number, data) {
|
|
270
|
+
await this.rest.patch(`/repos/${this.owner}/${this.repo}/issues/${number}`, data);
|
|
271
|
+
return this.getIssue(number);
|
|
272
|
+
}
|
|
273
|
+
async closeIssue(number) {
|
|
274
|
+
await this.updateIssue(number, { state: "closed" });
|
|
275
|
+
}
|
|
276
|
+
async addLabels(issueNumber, labels) {
|
|
277
|
+
await this.rest.post(`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels`, { labels });
|
|
278
|
+
}
|
|
279
|
+
async removeLabel(issueNumber, label) {
|
|
280
|
+
const encodedLabel = encodeURIComponent(label);
|
|
281
|
+
await this.rest.delete(`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels/${encodedLabel}`);
|
|
282
|
+
}
|
|
283
|
+
async createLabel(label) {
|
|
284
|
+
await this.rest.post(`/repos/${this.owner}/${this.repo}/labels`, label);
|
|
285
|
+
}
|
|
286
|
+
async updateLabel(name, label) {
|
|
287
|
+
await this.rest.patch(`/repos/${this.owner}/${this.repo}/labels/${encodeURIComponent(name)}`, label);
|
|
288
|
+
}
|
|
289
|
+
async listLabels() {
|
|
290
|
+
return (await this.rest.get(`/repos/${this.owner}/${this.repo}/labels`)).map((l) => ({
|
|
291
|
+
name: l.name,
|
|
292
|
+
color: l.color,
|
|
293
|
+
description: l.description
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
async addComment(issueNumber, body) {
|
|
297
|
+
const data = await this.rest.post(`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`, { body });
|
|
298
|
+
return {
|
|
299
|
+
id: String(data.id),
|
|
300
|
+
body: data.body,
|
|
301
|
+
author: {
|
|
302
|
+
login: data.user.login,
|
|
303
|
+
id: String(data.user.id),
|
|
304
|
+
type: data.user.type === "Bot" ? "Bot" : "User"
|
|
305
|
+
},
|
|
306
|
+
createdAt: new Date(data.created_at),
|
|
307
|
+
updatedAt: new Date(data.updated_at),
|
|
308
|
+
url: data.html_url
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async updateComment(commentId, body) {
|
|
312
|
+
return await this.rest.patch(`/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`, { body });
|
|
313
|
+
}
|
|
314
|
+
async deleteComment(commentId) {
|
|
315
|
+
await this.rest.delete(`/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`);
|
|
316
|
+
}
|
|
317
|
+
async listComments(issueNumber) {
|
|
318
|
+
return (await this.rest.get(`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`)).map((c) => ({
|
|
319
|
+
id: String(c.id),
|
|
320
|
+
body: c.body,
|
|
321
|
+
author: {
|
|
322
|
+
login: c.user.login,
|
|
323
|
+
id: String(c.user.id),
|
|
324
|
+
type: c.user.type === "Bot" ? "Bot" : "User"
|
|
325
|
+
},
|
|
326
|
+
createdAt: new Date(c.created_at),
|
|
327
|
+
updatedAt: new Date(c.updated_at),
|
|
328
|
+
url: c.html_url
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
async assignIssue(issueNumber, assignees) {
|
|
332
|
+
await this.rest.post(`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/assignees`, { assignees });
|
|
333
|
+
}
|
|
334
|
+
async unassignIssue(issueNumber, assignees) {
|
|
335
|
+
await this.rest.delete(`/repos/${this.owner}/${this.repo}/issues/${issueNumber}/assignees`);
|
|
336
|
+
}
|
|
337
|
+
async getPullRequest(number) {
|
|
338
|
+
const issue = await this.getIssue(number);
|
|
339
|
+
const data = await this.rest.get(`/repos/${this.owner}/${this.repo}/pulls/${number}`);
|
|
340
|
+
return {
|
|
341
|
+
...issue,
|
|
342
|
+
headRef: data.head.ref,
|
|
343
|
+
baseRef: data.base.ref,
|
|
344
|
+
merged: data.merged,
|
|
345
|
+
mergedAt: data.merged_at ? new Date(data.merged_at) : void 0,
|
|
346
|
+
mergeable: data.mergeable,
|
|
347
|
+
draft: data.draft
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async createPullRequest(data) {
|
|
351
|
+
const result = await this.rest.post(`/repos/${this.owner}/${this.repo}/pulls`, {
|
|
352
|
+
title: data.title,
|
|
353
|
+
body: data.body,
|
|
354
|
+
head: data.headRef,
|
|
355
|
+
base: data.baseRef,
|
|
356
|
+
draft: data.draft
|
|
357
|
+
});
|
|
358
|
+
return this.getPullRequest(result.number);
|
|
359
|
+
}
|
|
360
|
+
async mergePullRequest(number, method) {
|
|
361
|
+
await this.rest.put(`/repos/${this.owner}/${this.repo}/pulls/${number}/merge`, { merge_method: method || "merge" });
|
|
362
|
+
}
|
|
363
|
+
async searchIssues(query, filters) {
|
|
364
|
+
let searchQuery = `${query} repo:${this.owner}/${this.repo}`;
|
|
365
|
+
if (filters?.state) searchQuery += ` state:${filters.state}`;
|
|
366
|
+
if (filters?.labels) searchQuery += ` ${filters.labels.map((l) => `label:"${l}"`).join(" ")}`;
|
|
367
|
+
if (filters?.author) searchQuery += ` author:${filters.author}`;
|
|
368
|
+
if (filters?.assignee) searchQuery += ` assignee:${filters.assignee}`;
|
|
369
|
+
const params = new URLSearchParams({
|
|
370
|
+
q: searchQuery,
|
|
371
|
+
sort: filters?.sort || "created",
|
|
372
|
+
order: filters?.order || "desc",
|
|
373
|
+
per_page: String(filters?.limit || 30)
|
|
374
|
+
});
|
|
375
|
+
const data = await this.rest.get(`/search/issues?${params}`);
|
|
376
|
+
return Promise.all(data.items.map((item) => this.getIssue(item.number)));
|
|
377
|
+
}
|
|
378
|
+
async getIssueNodeId(issueNumber) {
|
|
379
|
+
return (await this.getIssue(issueNumber)).id;
|
|
380
|
+
}
|
|
381
|
+
async getPRNodeId(prNumber) {
|
|
382
|
+
return (await this.getPullRequest(prNumber)).id;
|
|
383
|
+
}
|
|
384
|
+
async createBranch(name, fromRef) {
|
|
385
|
+
const refData = await this.rest.get(`/repos/${this.owner}/${this.repo}/git/ref/heads/${fromRef}`);
|
|
386
|
+
await this.rest.post(`/repos/${this.owner}/${this.repo}/git/refs`, {
|
|
387
|
+
ref: `refs/heads/${name}`,
|
|
388
|
+
sha: refData.object.sha
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
name,
|
|
392
|
+
sha: refData.object.sha,
|
|
393
|
+
protected: false
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
async deleteBranch(name) {
|
|
397
|
+
await this.rest.delete(`/repos/${this.owner}/${this.repo}/git/refs/heads/${name}`);
|
|
398
|
+
}
|
|
399
|
+
async getBranch(name) {
|
|
400
|
+
try {
|
|
401
|
+
const data = await this.rest.get(`/repos/${this.owner}/${this.repo}/branches/${encodeURIComponent(name)}`);
|
|
402
|
+
return {
|
|
403
|
+
name: data.name,
|
|
404
|
+
sha: data.commit.sha,
|
|
405
|
+
protected: data.protected
|
|
406
|
+
};
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error instanceof Error && "code" in error && error.code === "NOT_FOUND") return null;
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async markPRReady(prNumber) {
|
|
413
|
+
const pr = await this.getPullRequest(prNumber);
|
|
414
|
+
await this.graphql.mutate(`
|
|
460
415
|
mutation($pullRequestId: ID!) {
|
|
461
416
|
markPullRequestReadyForReview(input: {
|
|
462
417
|
pullRequestId: $pullRequestId
|
|
@@ -466,12 +421,11 @@ class GitHubRepository {
|
|
|
466
421
|
}
|
|
467
422
|
}
|
|
468
423
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const mutation = `
|
|
424
|
+
`, { pullRequestId: pr.id });
|
|
425
|
+
}
|
|
426
|
+
async convertPRToDraft(prNumber) {
|
|
427
|
+
const pr = await this.getPullRequest(prNumber);
|
|
428
|
+
await this.graphql.mutate(`
|
|
475
429
|
mutation($pullRequestId: ID!) {
|
|
476
430
|
convertPullRequestToDraft(input: {
|
|
477
431
|
pullRequestId: $pullRequestId
|
|
@@ -481,234 +435,309 @@ class GitHubRepository {
|
|
|
481
435
|
}
|
|
482
436
|
}
|
|
483
437
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
583
|
-
__proto__: null,
|
|
584
|
-
GitHubRepository
|
|
585
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
438
|
+
`, { pullRequestId: pr.id });
|
|
439
|
+
}
|
|
440
|
+
async requestReview(prNumber, reviewers) {
|
|
441
|
+
await this.rest.post(`/repos/${this.owner}/${this.repo}/pulls/${prNumber}/requested_reviewers`, { reviewers });
|
|
442
|
+
}
|
|
443
|
+
async triggerWorkflow(workflowId, ref, inputs) {
|
|
444
|
+
await this.rest.post(`/repos/${this.owner}/${this.repo}/actions/workflows/${workflowId}/dispatches`, {
|
|
445
|
+
ref,
|
|
446
|
+
inputs: inputs || {}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
async findPRsForIssue(issueNumber) {
|
|
450
|
+
const searchTerms = [
|
|
451
|
+
"closes",
|
|
452
|
+
"fixes",
|
|
453
|
+
"resolves"
|
|
454
|
+
].map((k) => `${k} #${issueNumber}`).join(" OR ");
|
|
455
|
+
const query = `is:pr repo:${this.owner}/${this.repo} ${searchTerms}`;
|
|
456
|
+
const data = await this.rest.get(`/search/issues?q=${encodeURIComponent(query)}`);
|
|
457
|
+
return Promise.all(data.items.map((item) => this.getPullRequest(item.number)));
|
|
458
|
+
}
|
|
459
|
+
async findIssueForPR(prNumber) {
|
|
460
|
+
const matches = [...(await this.getPullRequest(prNumber)).body.matchAll(/(?:closes?|fixes?|resolves?)\s+#(\d+)/gi)];
|
|
461
|
+
if (matches.length === 0) return null;
|
|
462
|
+
const issueNumber = Number.parseInt(matches[0][1], 10);
|
|
463
|
+
try {
|
|
464
|
+
return await this.getIssue(issueNumber);
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async getFileContent(path, ref) {
|
|
470
|
+
try {
|
|
471
|
+
const url = `/repos/${this.owner}/${this.repo}/contents/${path}${ref ? `?ref=${ref}` : ""}`;
|
|
472
|
+
const data = await this.rest.get(url);
|
|
473
|
+
if (data.type !== "file" || !data.content) return null;
|
|
474
|
+
if (data.encoding === "base64") return Buffer.from(data.content, "base64").toString("utf-8");
|
|
475
|
+
return data.content;
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async listDirectoryFiles(path, ref) {
|
|
481
|
+
try {
|
|
482
|
+
const url = `/repos/${this.owner}/${this.repo}/contents/${path}${ref ? `?ref=${ref}` : ""}`;
|
|
483
|
+
const data = await this.rest.get(url);
|
|
484
|
+
if (!Array.isArray(data)) return [];
|
|
485
|
+
return data.filter((item) => item.type === "file").map((item) => item.name);
|
|
486
|
+
} catch {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create a new repository from this repository as a template.
|
|
492
|
+
*
|
|
493
|
+
* Uses the GitHub "Generate" API: POST /repos/{template_owner}/{template_repo}/generate
|
|
494
|
+
* The current repository (this.owner/this.repo) is used as the template.
|
|
495
|
+
*/
|
|
496
|
+
async createRepositoryFromTemplate(options) {
|
|
497
|
+
const data = await this.rest.post(`/repos/${this.owner}/${this.repo}/generate`, {
|
|
498
|
+
owner: options.owner,
|
|
499
|
+
name: options.name,
|
|
500
|
+
description: options.description || "",
|
|
501
|
+
private: options.isPrivate ?? true,
|
|
502
|
+
include_all_branches: options.includeAllBranches ?? false
|
|
503
|
+
});
|
|
504
|
+
return {
|
|
505
|
+
owner: data.owner.login,
|
|
506
|
+
name: data.name,
|
|
507
|
+
description: data.description || "",
|
|
508
|
+
defaultBranch: data.default_branch,
|
|
509
|
+
url: data.html_url,
|
|
510
|
+
isPrivate: data.private
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/parsing.ts
|
|
516
|
+
/**
|
|
517
|
+
* Issue body parsing and rendering utilities
|
|
518
|
+
*
|
|
519
|
+
* Parses GitHub issue bodies created from YAML form templates.
|
|
520
|
+
* GitHub renders form fields as markdown sections with ### headings.
|
|
521
|
+
*/
|
|
522
|
+
/**
|
|
523
|
+
* Load and parse an issue template from a YAML file
|
|
524
|
+
*
|
|
525
|
+
* @param yamlPath - Path to the YAML template file
|
|
526
|
+
* @returns Parsed issue template
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* const template = await loadIssueTemplate('.github/ISSUE_TEMPLATE/bug_report.yml');
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
586
533
|
async function loadIssueTemplate(yamlPath) {
|
|
587
|
-
|
|
588
|
-
return parseIssueTemplate(content);
|
|
534
|
+
return parseIssueTemplate(await readFile(yamlPath, "utf-8"));
|
|
589
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Parse an issue template from YAML content
|
|
538
|
+
*
|
|
539
|
+
* @param yamlContent - YAML content string
|
|
540
|
+
* @returns Parsed issue template
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```typescript
|
|
544
|
+
* const template = parseIssueTemplate(`
|
|
545
|
+
* name: Bug Report
|
|
546
|
+
* body:
|
|
547
|
+
* - type: textarea
|
|
548
|
+
* id: description
|
|
549
|
+
* attributes:
|
|
550
|
+
* label: Description
|
|
551
|
+
* `);
|
|
552
|
+
* ```
|
|
553
|
+
*/
|
|
590
554
|
function parseIssueTemplate(yamlContent) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (!parsed.body || !Array.isArray(parsed.body)) {
|
|
596
|
-
throw new Error("Issue template must have a body array");
|
|
597
|
-
}
|
|
598
|
-
return parsed;
|
|
555
|
+
const parsed = yaml.load(yamlContent);
|
|
556
|
+
if (!parsed.name) throw new Error("Issue template must have a name");
|
|
557
|
+
if (!parsed.body || !Array.isArray(parsed.body)) throw new Error("Issue template must have a body array");
|
|
558
|
+
return parsed;
|
|
599
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* Parse an issue body into field values
|
|
562
|
+
*
|
|
563
|
+
* GitHub renders form fields as markdown sections:
|
|
564
|
+
* ```
|
|
565
|
+
* ### Label
|
|
566
|
+
*
|
|
567
|
+
* Content here
|
|
568
|
+
*
|
|
569
|
+
* ### Next Label
|
|
570
|
+
* ```
|
|
571
|
+
*
|
|
572
|
+
* Without a template, returns a Record<string, string> keyed by label.
|
|
573
|
+
* With a template, returns a Record<string, string> keyed by field ID.
|
|
574
|
+
*
|
|
575
|
+
* @param body - The issue body markdown
|
|
576
|
+
* @param template - Optional template for mapping labels to field IDs
|
|
577
|
+
* @returns Parsed fields as key-value pairs
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```typescript
|
|
581
|
+
* // Without template - keyed by label
|
|
582
|
+
* const fields = parseIssueBody(issue.body);
|
|
583
|
+
* // { 'Description': '...', 'Steps to Reproduce': '...' }
|
|
584
|
+
*
|
|
585
|
+
* // With template - keyed by field ID
|
|
586
|
+
* const template = await loadIssueTemplate('.github/ISSUE_TEMPLATE/bug_report.yml');
|
|
587
|
+
* const fields = parseIssueBody(issue.body, template);
|
|
588
|
+
* // { description: '...', reproduction: '...' }
|
|
589
|
+
* ```
|
|
590
|
+
*/
|
|
600
591
|
function parseIssueBody(body, template) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
for (const [label, content] of Object.entries(sections)) {
|
|
613
|
-
const id = labelToId.get(label);
|
|
614
|
-
if (id) {
|
|
615
|
-
result[id] = content;
|
|
616
|
-
} else {
|
|
617
|
-
result[label] = content;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return result;
|
|
592
|
+
const sections = parseSections(body);
|
|
593
|
+
if (!template) return sections;
|
|
594
|
+
const labelToId = /* @__PURE__ */ new Map();
|
|
595
|
+
for (const field of template.body) if (field.id && field.attributes?.label) labelToId.set(field.attributes.label, field.id);
|
|
596
|
+
const result = {};
|
|
597
|
+
for (const [label, content] of Object.entries(sections)) {
|
|
598
|
+
const id = labelToId.get(label);
|
|
599
|
+
if (id) result[id] = content;
|
|
600
|
+
else result[label] = content;
|
|
601
|
+
}
|
|
602
|
+
return result;
|
|
621
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Parse markdown sections (### headers) into key-value pairs
|
|
606
|
+
*/
|
|
622
607
|
function parseSections(body) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
return result;
|
|
608
|
+
const result = {};
|
|
609
|
+
for (const match of body.matchAll(/^### (.+?)\r?\n\r?\n([\s\S]*?)(?=\r?\n### |\r?\n---|\s*$)/gm)) {
|
|
610
|
+
const label = match[1].trim();
|
|
611
|
+
result[label] = match[2].trim();
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
631
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Render fields into an issue body markdown string
|
|
617
|
+
*
|
|
618
|
+
* Without a template, uses field keys as labels.
|
|
619
|
+
* With a template, uses field IDs to look up labels and renders in template order.
|
|
620
|
+
*
|
|
621
|
+
* @param fields - Field values as key-value pairs
|
|
622
|
+
* @param template - Optional template for field ordering and labels
|
|
623
|
+
* @returns Markdown string for issue body
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```typescript
|
|
627
|
+
* // Without template
|
|
628
|
+
* const body = renderIssueBody({
|
|
629
|
+
* 'Description': 'Bug description here',
|
|
630
|
+
* 'Steps to Reproduce': '1. Do X\n2. See error'
|
|
631
|
+
* });
|
|
632
|
+
*
|
|
633
|
+
* // With template
|
|
634
|
+
* const template = await loadIssueTemplate('.github/ISSUE_TEMPLATE/bug_report.yml');
|
|
635
|
+
* const body = renderIssueBody({
|
|
636
|
+
* description: 'Bug description here',
|
|
637
|
+
* reproduction: '1. Do X\n2. See error'
|
|
638
|
+
* }, template);
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
632
641
|
function renderIssueBody(fields, template) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
const label = field.attributes?.label || field.id;
|
|
647
|
-
const content = idToContent.get(field.id);
|
|
648
|
-
if (content !== void 0) {
|
|
649
|
-
sections.push(`### ${label}
|
|
650
|
-
|
|
651
|
-
${content}`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return sections.join("\n\n");
|
|
642
|
+
const sections = [];
|
|
643
|
+
if (!template) for (const [label, content] of Object.entries(fields)) sections.push(`### ${label}\n\n${content}`);
|
|
644
|
+
else {
|
|
645
|
+
const idToContent = new Map(Object.entries(fields));
|
|
646
|
+
for (const field of template.body) {
|
|
647
|
+
if (!field.id || field.type === "markdown") continue;
|
|
648
|
+
const label = field.attributes?.label || field.id;
|
|
649
|
+
const content = idToContent.get(field.id);
|
|
650
|
+
if (content !== void 0) sections.push(`### ${label}\n\n${content}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return sections.join("\n\n");
|
|
656
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* Get a specific field value from an issue body
|
|
657
|
+
*
|
|
658
|
+
* @param body - The issue body markdown
|
|
659
|
+
* @param fieldIdOrLabel - Field ID (with template) or label (without template)
|
|
660
|
+
* @param template - Optional template for field ID lookup
|
|
661
|
+
* @returns Field value or undefined if not found
|
|
662
|
+
*/
|
|
657
663
|
function getIssueField(body, fieldIdOrLabel, template) {
|
|
658
|
-
|
|
659
|
-
return fields[fieldIdOrLabel];
|
|
664
|
+
return parseIssueBody(body, template)[fieldIdOrLabel];
|
|
660
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Update a specific field in an issue body
|
|
668
|
+
*
|
|
669
|
+
* @param body - The issue body markdown
|
|
670
|
+
* @param fieldIdOrLabel - Field ID (with template) or label (without template)
|
|
671
|
+
* @param value - New value for the field
|
|
672
|
+
* @param template - Optional template for field ID lookup
|
|
673
|
+
* @returns Updated issue body markdown
|
|
674
|
+
*/
|
|
661
675
|
function updateIssueField(body, fieldIdOrLabel, value, template) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
676
|
+
const fields = parseIssueBody(body, template);
|
|
677
|
+
fields[fieldIdOrLabel] = value;
|
|
678
|
+
return renderIssueBody(fields, template);
|
|
665
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Fetch issue templates from a repository via GitHub API
|
|
682
|
+
*
|
|
683
|
+
* @param repo - Repository client implementing IRepository
|
|
684
|
+
* @returns Array of parsed issue templates
|
|
685
|
+
*
|
|
686
|
+
* @example
|
|
687
|
+
* ```typescript
|
|
688
|
+
* const repo = await getRepository({ type: 'github', owner: 'org', repo: 'name', token });
|
|
689
|
+
* const templates = await fetchIssueTemplates(repo);
|
|
690
|
+
* // [{ name: 'Bug Report', labels: ['bug'], body: [...] }, ...]
|
|
691
|
+
* ```
|
|
692
|
+
*/
|
|
666
693
|
async function fetchIssueTemplates(repo) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
return templates;
|
|
694
|
+
const templatePath = ".github/ISSUE_TEMPLATE";
|
|
695
|
+
const files = await repo.listDirectoryFiles(templatePath);
|
|
696
|
+
const templates = [];
|
|
697
|
+
for (const file of files) if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
698
|
+
const content = await repo.getFileContent(`${templatePath}/${file}`);
|
|
699
|
+
if (content) try {
|
|
700
|
+
templates.push(parseIssueTemplate(content));
|
|
701
|
+
} catch (e) {
|
|
702
|
+
console.warn(`[repos] Failed to parse template ${file}:`, e);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return templates;
|
|
683
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Detect which template an issue was created from based on labels
|
|
709
|
+
*
|
|
710
|
+
* Matches issue labels against template labels to find the best match.
|
|
711
|
+
* Returns the template with the most matching labels.
|
|
712
|
+
*
|
|
713
|
+
* @param labels - Issue labels
|
|
714
|
+
* @param templates - Available templates
|
|
715
|
+
* @returns Matching template or undefined if no match
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```typescript
|
|
719
|
+
* const templates = await fetchIssueTemplates(repo);
|
|
720
|
+
* const template = detectTemplateFromLabels(['bug', 'critical'], templates);
|
|
721
|
+
* if (template) {
|
|
722
|
+
* const fields = parseIssueBody(issue.body, template);
|
|
723
|
+
* }
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
684
726
|
function detectTemplateFromLabels(labels, templates) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
return bestMatch;
|
|
727
|
+
const labelSet = new Set(labels.map((l) => l.toLowerCase()));
|
|
728
|
+
let bestMatch;
|
|
729
|
+
let bestScore = 0;
|
|
730
|
+
for (const template of templates) {
|
|
731
|
+
if (!template.labels || template.labels.length === 0) continue;
|
|
732
|
+
const matchCount = template.labels.filter((l) => labelSet.has(l.toLowerCase())).length;
|
|
733
|
+
if (matchCount > 0 && matchCount > bestScore) {
|
|
734
|
+
bestScore = matchCount;
|
|
735
|
+
bestMatch = template;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return bestMatch;
|
|
699
739
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
detectTemplateFromLabels,
|
|
705
|
-
fetchIssueTemplates,
|
|
706
|
-
getIssueField,
|
|
707
|
-
getRepository,
|
|
708
|
-
loadIssueTemplate,
|
|
709
|
-
parseIssueBody,
|
|
710
|
-
parseIssueTemplate,
|
|
711
|
-
renderIssueBody,
|
|
712
|
-
updateIssueField
|
|
713
|
-
};
|
|
714
|
-
//# sourceMappingURL=index.js.map
|
|
740
|
+
//#endregion
|
|
741
|
+
export { GitHubRepository, RepositoryError, RepositoryErrorCode, detectTemplateFromLabels, fetchIssueTemplates, getIssueField, getRepository, loadIssueTemplate, parseIssueBody, parseIssueTemplate, renderIssueBody, updateIssueField };
|
|
742
|
+
|
|
743
|
+
//# sourceMappingURL=index.js.map
|