@happyvertical/repos 0.74.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,714 @@
1
+ import { GraphQLClient } from "@happyvertical/graphql";
2
+ import { readFile } from "node:fs/promises";
3
+ import yaml from "js-yaml";
4
+ var RepositoryErrorCode = /* @__PURE__ */ ((RepositoryErrorCode2) => {
5
+ RepositoryErrorCode2["NOT_FOUND"] = "NOT_FOUND";
6
+ RepositoryErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
7
+ RepositoryErrorCode2["FORBIDDEN"] = "FORBIDDEN";
8
+ RepositoryErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
9
+ RepositoryErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
10
+ RepositoryErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
11
+ RepositoryErrorCode2["UNKNOWN"] = "UNKNOWN";
12
+ return RepositoryErrorCode2;
13
+ })(RepositoryErrorCode || {});
14
+ class RepositoryError extends Error {
15
+ constructor(message, code, statusCode, response) {
16
+ super(message);
17
+ this.code = code;
18
+ this.statusCode = statusCode;
19
+ this.response = response;
20
+ this.name = "RepositoryError";
21
+ Error.captureStackTrace(this, this.constructor);
22
+ }
23
+ static fromHTTPStatus(statusCode, message, response) {
24
+ let code;
25
+ switch (statusCode) {
26
+ case 404:
27
+ code = "NOT_FOUND";
28
+ break;
29
+ case 401:
30
+ code = "UNAUTHORIZED";
31
+ break;
32
+ case 403:
33
+ code = "FORBIDDEN";
34
+ break;
35
+ case 429:
36
+ code = "RATE_LIMITED";
37
+ break;
38
+ case 422:
39
+ code = "VALIDATION_ERROR";
40
+ break;
41
+ default:
42
+ code = "UNKNOWN";
43
+ }
44
+ return new RepositoryError(message, code, statusCode, response);
45
+ }
46
+ static networkError(message, cause) {
47
+ return new RepositoryError(
48
+ message,
49
+ "NETWORK_ERROR",
50
+ void 0,
51
+ cause
52
+ );
53
+ }
54
+ isRetryable() {
55
+ return this.code === "RATE_LIMITED" || this.code === "NETWORK_ERROR";
56
+ }
57
+ }
58
+ function isRepositoryInstance(value) {
59
+ return value !== null && typeof value === "object" && "getIssue" in value && "createIssue" in value && "addLabels" in value && typeof value.getIssue === "function";
60
+ }
61
+ async function getRepository(options) {
62
+ if (isRepositoryInstance(options)) {
63
+ return options;
64
+ }
65
+ if (!options.type) {
66
+ throw new RepositoryError(
67
+ "Repository type is required",
68
+ RepositoryErrorCode.VALIDATION_ERROR
69
+ );
70
+ }
71
+ if (!options.owner || !options.repo) {
72
+ throw new RepositoryError(
73
+ "Repository owner and repo are required",
74
+ RepositoryErrorCode.VALIDATION_ERROR
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
+ }
109
+ }
110
+ class GitHubRest {
111
+ token;
112
+ baseUrl;
113
+ constructor(config) {
114
+ this.token = config.token;
115
+ this.baseUrl = config.baseUrl || "https://api.github.com";
116
+ }
117
+ /**
118
+ * Make a REST API request
119
+ */
120
+ async request(method, path, body) {
121
+ const url = `${this.baseUrl}${path}`;
122
+ const headers = {
123
+ Authorization: `Bearer ${this.token}`,
124
+ Accept: "application/vnd.github+json",
125
+ "X-GitHub-Api-Version": "2022-11-28",
126
+ "Content-Type": "application/json"
127
+ };
128
+ const response = await fetch(url, {
129
+ method,
130
+ headers,
131
+ body: body ? JSON.stringify(body) : void 0
132
+ });
133
+ if (!response.ok) {
134
+ const error = await response.text();
135
+ throw RepositoryError.fromHTTPStatus(
136
+ response.status,
137
+ `GitHub API error: ${response.statusText}
138
+ ${error}`,
139
+ error
140
+ );
141
+ }
142
+ return response.json();
143
+ }
144
+ /**
145
+ * GET request
146
+ */
147
+ async get(path) {
148
+ return this.request("GET", path);
149
+ }
150
+ /**
151
+ * POST request
152
+ */
153
+ async post(path, body) {
154
+ return this.request("POST", path, body);
155
+ }
156
+ /**
157
+ * PATCH request
158
+ */
159
+ async patch(path, body) {
160
+ return this.request("PATCH", path, body);
161
+ }
162
+ /**
163
+ * PUT request
164
+ */
165
+ async put(path, body) {
166
+ return this.request("PUT", path, body);
167
+ }
168
+ /**
169
+ * DELETE request
170
+ */
171
+ async delete(path) {
172
+ await this.request("DELETE", path);
173
+ }
174
+ }
175
+ class GitHubRepository {
176
+ rest;
177
+ graphql;
178
+ owner;
179
+ repo;
180
+ constructor(config) {
181
+ if (config.type !== "github") {
182
+ throw new Error("Invalid config type for GitHubRepository");
183
+ }
184
+ this.owner = config.owner;
185
+ this.repo = config.repo;
186
+ this.rest = new GitHubRest({
187
+ token: config.token,
188
+ baseUrl: config.baseUrl
189
+ });
190
+ this.graphql = new GraphQLClient({
191
+ endpoint: config.baseUrl ? `${config.baseUrl}/graphql` : "https://api.github.com/graphql",
192
+ token: config.token
193
+ });
194
+ }
195
+ // Repository Info
196
+ async getRepository() {
197
+ const data = await this.rest.get(`/repos/${this.owner}/${this.repo}`);
198
+ return {
199
+ owner: data.owner.login,
200
+ name: data.name,
201
+ description: data.description,
202
+ defaultBranch: data.default_branch,
203
+ url: data.html_url,
204
+ isPrivate: data.private
205
+ };
206
+ }
207
+ // Issues
208
+ async getIssue(number) {
209
+ const data = await this.rest.get(
210
+ `/repos/${this.owner}/${this.repo}/issues/${number}`
211
+ );
212
+ return {
213
+ number: data.number,
214
+ id: data.node_id,
215
+ title: data.title,
216
+ body: data.body || "",
217
+ state: data.state === "open" ? "open" : "closed",
218
+ labels: data.labels.map((l) => ({
219
+ name: l.name,
220
+ color: l.color,
221
+ description: l.description
222
+ })),
223
+ assignees: data.assignees.map((a) => ({
224
+ login: a.login,
225
+ id: String(a.id),
226
+ type: a.type === "Bot" ? "Bot" : "User"
227
+ })),
228
+ author: {
229
+ login: data.user.login,
230
+ id: String(data.user.id),
231
+ type: data.user.type === "Bot" ? "Bot" : "User"
232
+ },
233
+ createdAt: new Date(data.created_at),
234
+ updatedAt: new Date(data.updated_at),
235
+ closedAt: data.closed_at ? new Date(data.closed_at) : void 0,
236
+ url: data.html_url,
237
+ commentsCount: data.comments
238
+ };
239
+ }
240
+ async createIssue(data) {
241
+ const result = await this.rest.post(
242
+ `/repos/${this.owner}/${this.repo}/issues`,
243
+ data
244
+ );
245
+ return this.getIssue(result.number);
246
+ }
247
+ async updateIssue(number, data) {
248
+ await this.rest.patch(
249
+ `/repos/${this.owner}/${this.repo}/issues/${number}`,
250
+ data
251
+ );
252
+ return this.getIssue(number);
253
+ }
254
+ async closeIssue(number) {
255
+ await this.updateIssue(number, { state: "closed" });
256
+ }
257
+ // Labels
258
+ async addLabels(issueNumber, labels) {
259
+ await this.rest.post(
260
+ `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels`,
261
+ { labels }
262
+ );
263
+ }
264
+ async removeLabel(issueNumber, label) {
265
+ const encodedLabel = encodeURIComponent(label);
266
+ await this.rest.delete(
267
+ `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/labels/${encodedLabel}`
268
+ );
269
+ }
270
+ async createLabel(label) {
271
+ await this.rest.post(`/repos/${this.owner}/${this.repo}/labels`, label);
272
+ }
273
+ async updateLabel(name, label) {
274
+ await this.rest.patch(
275
+ `/repos/${this.owner}/${this.repo}/labels/${encodeURIComponent(name)}`,
276
+ label
277
+ );
278
+ }
279
+ async listLabels() {
280
+ const data = await this.rest.get(
281
+ `/repos/${this.owner}/${this.repo}/labels`
282
+ );
283
+ return data.map((l) => ({
284
+ name: l.name,
285
+ color: l.color,
286
+ description: l.description
287
+ }));
288
+ }
289
+ // Comments
290
+ async addComment(issueNumber, body) {
291
+ const data = await this.rest.post(
292
+ `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`,
293
+ { body }
294
+ );
295
+ return {
296
+ id: String(data.id),
297
+ body: data.body,
298
+ author: {
299
+ login: data.user.login,
300
+ id: String(data.user.id),
301
+ type: data.user.type === "Bot" ? "Bot" : "User"
302
+ },
303
+ createdAt: new Date(data.created_at),
304
+ updatedAt: new Date(data.updated_at),
305
+ url: data.html_url
306
+ };
307
+ }
308
+ async updateComment(commentId, body) {
309
+ const data = await this.rest.patch(
310
+ `/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`,
311
+ { body }
312
+ );
313
+ return data;
314
+ }
315
+ async deleteComment(commentId) {
316
+ await this.rest.delete(
317
+ `/repos/${this.owner}/${this.repo}/issues/comments/${commentId}`
318
+ );
319
+ }
320
+ async listComments(issueNumber) {
321
+ const data = await this.rest.get(
322
+ `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`
323
+ );
324
+ return data.map((c) => ({
325
+ id: String(c.id),
326
+ body: c.body,
327
+ author: {
328
+ login: c.user.login,
329
+ id: String(c.user.id),
330
+ type: c.user.type === "Bot" ? "Bot" : "User"
331
+ },
332
+ createdAt: new Date(c.created_at),
333
+ updatedAt: new Date(c.updated_at),
334
+ url: c.html_url
335
+ }));
336
+ }
337
+ // Assignments
338
+ async assignIssue(issueNumber, assignees) {
339
+ await this.rest.post(
340
+ `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/assignees`,
341
+ { assignees }
342
+ );
343
+ }
344
+ async unassignIssue(issueNumber, assignees) {
345
+ await this.rest.delete(
346
+ `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/assignees`
347
+ );
348
+ }
349
+ // Pull Requests
350
+ async getPullRequest(number) {
351
+ const issue = await this.getIssue(number);
352
+ const data = await this.rest.get(
353
+ `/repos/${this.owner}/${this.repo}/pulls/${number}`
354
+ );
355
+ return {
356
+ ...issue,
357
+ headRef: data.head.ref,
358
+ baseRef: data.base.ref,
359
+ merged: data.merged,
360
+ mergedAt: data.merged_at ? new Date(data.merged_at) : void 0,
361
+ mergeable: data.mergeable,
362
+ draft: data.draft
363
+ };
364
+ }
365
+ async createPullRequest(data) {
366
+ const result = await this.rest.post(
367
+ `/repos/${this.owner}/${this.repo}/pulls`,
368
+ {
369
+ title: data.title,
370
+ body: data.body,
371
+ head: data.headRef,
372
+ base: data.baseRef,
373
+ draft: data.draft
374
+ }
375
+ );
376
+ return this.getPullRequest(result.number);
377
+ }
378
+ async mergePullRequest(number, method) {
379
+ await this.rest.put(
380
+ `/repos/${this.owner}/${this.repo}/pulls/${number}/merge`,
381
+ {
382
+ merge_method: method || "merge"
383
+ }
384
+ );
385
+ }
386
+ // Search
387
+ async searchIssues(query, filters) {
388
+ let searchQuery = `${query} repo:${this.owner}/${this.repo}`;
389
+ if (filters?.state) {
390
+ searchQuery += ` state:${filters.state}`;
391
+ }
392
+ if (filters?.labels) {
393
+ searchQuery += ` ${filters.labels.map((l) => `label:"${l}"`).join(" ")}`;
394
+ }
395
+ if (filters?.author) {
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 = `
460
+ mutation($pullRequestId: ID!) {
461
+ markPullRequestReadyForReview(input: {
462
+ pullRequestId: $pullRequestId
463
+ }) {
464
+ pullRequest {
465
+ id
466
+ }
467
+ }
468
+ }
469
+ `;
470
+ await this.graphql.mutate(mutation, { pullRequestId: pr.id });
471
+ }
472
+ async convertPRToDraft(prNumber) {
473
+ const pr = await this.getPullRequest(prNumber);
474
+ const mutation = `
475
+ mutation($pullRequestId: ID!) {
476
+ convertPullRequestToDraft(input: {
477
+ pullRequestId: $pullRequestId
478
+ }) {
479
+ pullRequest {
480
+ id
481
+ }
482
+ }
483
+ }
484
+ `;
485
+ await this.graphql.mutate(mutation, { pullRequestId: pr.id });
486
+ }
487
+ async requestReview(prNumber, reviewers) {
488
+ await this.rest.post(
489
+ `/repos/${this.owner}/${this.repo}/pulls/${prNumber}/requested_reviewers`,
490
+ { reviewers }
491
+ );
492
+ }
493
+ // Workflow
494
+ async triggerWorkflow(workflowId, ref, inputs) {
495
+ await this.rest.post(
496
+ `/repos/${this.owner}/${this.repo}/actions/workflows/${workflowId}/dispatches`,
497
+ { ref, inputs: inputs || {} }
498
+ );
499
+ }
500
+ // Linking
501
+ async findPRsForIssue(issueNumber) {
502
+ const keywords = ["closes", "fixes", "resolves"];
503
+ const searchTerms = keywords.map((k) => `${k} #${issueNumber}`).join(" OR ");
504
+ const query = `is:pr repo:${this.owner}/${this.repo} ${searchTerms}`;
505
+ const data = await this.rest.get(
506
+ `/search/issues?q=${encodeURIComponent(query)}`
507
+ );
508
+ return Promise.all(
509
+ data.items.map((item) => this.getPullRequest(item.number))
510
+ );
511
+ }
512
+ async findIssueForPR(prNumber) {
513
+ const pr = await this.getPullRequest(prNumber);
514
+ const closingPattern = /(?:closes?|fixes?|resolves?)\s+#(\d+)/gi;
515
+ const matches = [...pr.body.matchAll(closingPattern)];
516
+ if (matches.length === 0) {
517
+ return null;
518
+ }
519
+ const issueNumber = Number.parseInt(matches[0][1], 10);
520
+ try {
521
+ return await this.getIssue(issueNumber);
522
+ } catch {
523
+ return null;
524
+ }
525
+ }
526
+ // File Content
527
+ async getFileContent(path, ref) {
528
+ try {
529
+ const url = `/repos/${this.owner}/${this.repo}/contents/${path}${ref ? `?ref=${ref}` : ""}`;
530
+ const data = await this.rest.get(url);
531
+ if (data.type !== "file" || !data.content) {
532
+ return null;
533
+ }
534
+ if (data.encoding === "base64") {
535
+ return Buffer.from(data.content, "base64").toString("utf-8");
536
+ }
537
+ return data.content;
538
+ } catch {
539
+ return null;
540
+ }
541
+ }
542
+ async listDirectoryFiles(path, ref) {
543
+ try {
544
+ const url = `/repos/${this.owner}/${this.repo}/contents/${path}${ref ? `?ref=${ref}` : ""}`;
545
+ const data = await this.rest.get(url);
546
+ if (!Array.isArray(data)) {
547
+ return [];
548
+ }
549
+ return data.filter((item) => item.type === "file").map((item) => item.name);
550
+ } catch {
551
+ return [];
552
+ }
553
+ }
554
+ // Repository Creation from Template
555
+ /**
556
+ * Create a new repository from this repository as a template.
557
+ *
558
+ * Uses the GitHub "Generate" API: POST /repos/{template_owner}/{template_repo}/generate
559
+ * The current repository (this.owner/this.repo) is used as the template.
560
+ */
561
+ async createRepositoryFromTemplate(options) {
562
+ const data = await this.rest.post(
563
+ `/repos/${this.owner}/${this.repo}/generate`,
564
+ {
565
+ owner: options.owner,
566
+ name: options.name,
567
+ description: options.description || "",
568
+ private: options.isPrivate ?? true,
569
+ include_all_branches: options.includeAllBranches ?? false
570
+ }
571
+ );
572
+ return {
573
+ owner: data.owner.login,
574
+ name: data.name,
575
+ description: data.description || "",
576
+ defaultBranch: data.default_branch,
577
+ url: data.html_url,
578
+ isPrivate: data.private
579
+ };
580
+ }
581
+ }
582
+ const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
583
+ __proto__: null,
584
+ GitHubRepository
585
+ }, Symbol.toStringTag, { value: "Module" }));
586
+ async function loadIssueTemplate(yamlPath) {
587
+ const content = await readFile(yamlPath, "utf-8");
588
+ return parseIssueTemplate(content);
589
+ }
590
+ function parseIssueTemplate(yamlContent) {
591
+ const parsed = yaml.load(yamlContent);
592
+ if (!parsed.name) {
593
+ throw new Error("Issue template must have a name");
594
+ }
595
+ if (!parsed.body || !Array.isArray(parsed.body)) {
596
+ throw new Error("Issue template must have a body array");
597
+ }
598
+ return parsed;
599
+ }
600
+ function parseIssueBody(body, template) {
601
+ const sections = parseSections(body);
602
+ if (!template) {
603
+ return sections;
604
+ }
605
+ const labelToId = /* @__PURE__ */ new Map();
606
+ for (const field of template.body) {
607
+ if (field.id && field.attributes?.label) {
608
+ labelToId.set(field.attributes.label, field.id);
609
+ }
610
+ }
611
+ const result = {};
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;
621
+ }
622
+ function parseSections(body) {
623
+ const result = {};
624
+ const sectionRegex = /^### (.+?)\r?\n\r?\n([\s\S]*?)(?=\r?\n### |\r?\n---|\s*$)/gm;
625
+ for (const match of body.matchAll(sectionRegex)) {
626
+ const label = match[1].trim();
627
+ const content = match[2].trim();
628
+ result[label] = content;
629
+ }
630
+ return result;
631
+ }
632
+ function renderIssueBody(fields, template) {
633
+ const sections = [];
634
+ if (!template) {
635
+ for (const [label, content] of Object.entries(fields)) {
636
+ sections.push(`### ${label}
637
+
638
+ ${content}`);
639
+ }
640
+ } else {
641
+ const idToContent = new Map(Object.entries(fields));
642
+ for (const field of template.body) {
643
+ if (!field.id || field.type === "markdown") {
644
+ continue;
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");
656
+ }
657
+ function getIssueField(body, fieldIdOrLabel, template) {
658
+ const fields = parseIssueBody(body, template);
659
+ return fields[fieldIdOrLabel];
660
+ }
661
+ function updateIssueField(body, fieldIdOrLabel, value, template) {
662
+ const fields = parseIssueBody(body, template);
663
+ fields[fieldIdOrLabel] = value;
664
+ return renderIssueBody(fields, template);
665
+ }
666
+ async function fetchIssueTemplates(repo) {
667
+ const templatePath = ".github/ISSUE_TEMPLATE";
668
+ const files = await repo.listDirectoryFiles(templatePath);
669
+ const templates = [];
670
+ for (const file of files) {
671
+ if (file.endsWith(".yml") || file.endsWith(".yaml")) {
672
+ const content = await repo.getFileContent(`${templatePath}/${file}`);
673
+ if (content) {
674
+ try {
675
+ templates.push(parseIssueTemplate(content));
676
+ } catch (e) {
677
+ console.warn(`[repos] Failed to parse template ${file}:`, e);
678
+ }
679
+ }
680
+ }
681
+ }
682
+ return templates;
683
+ }
684
+ function detectTemplateFromLabels(labels, templates) {
685
+ const labelSet = new Set(labels.map((l) => l.toLowerCase()));
686
+ let bestMatch;
687
+ let bestScore = 0;
688
+ for (const template of templates) {
689
+ if (!template.labels || template.labels.length === 0) continue;
690
+ const matchCount = template.labels.filter(
691
+ (l) => labelSet.has(l.toLowerCase())
692
+ ).length;
693
+ if (matchCount > 0 && matchCount > bestScore) {
694
+ bestScore = matchCount;
695
+ bestMatch = template;
696
+ }
697
+ }
698
+ return bestMatch;
699
+ }
700
+ export {
701
+ GitHubRepository,
702
+ RepositoryError,
703
+ RepositoryErrorCode,
704
+ detectTemplateFromLabels,
705
+ fetchIssueTemplates,
706
+ getIssueField,
707
+ getRepository,
708
+ loadIssueTemplate,
709
+ parseIssueBody,
710
+ parseIssueTemplate,
711
+ renderIssueBody,
712
+ updateIssueField
713
+ };
714
+ //# sourceMappingURL=index.js.map