@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/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
- 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
- }
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
- return value !== null && typeof value === "object" && "getIssue" in value && "createIssue" in value && "addLabels" in value && typeof value.getIssue === "function";
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
- 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
- }
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
- 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 = `
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
- await this.graphql.mutate(mutation, { pullRequestId: pr.id });
471
- }
472
- async convertPRToDraft(prNumber) {
473
- const pr = await this.getPullRequest(prNumber);
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
- 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" }));
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
- const content = await readFile(yamlPath, "utf-8");
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
- 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;
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
- 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;
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
- 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;
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
- 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");
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
- const fields = parseIssueBody(body, template);
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
- const fields = parseIssueBody(body, template);
663
- fields[fieldIdOrLabel] = value;
664
- return renderIssueBody(fields, template);
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
- 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;
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
- 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;
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
- 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
740
+ //#endregion
741
+ export { GitHubRepository, RepositoryError, RepositoryErrorCode, detectTemplateFromLabels, fetchIssueTemplates, getIssueField, getRepository, loadIssueTemplate, parseIssueBody, parseIssueTemplate, renderIssueBody, updateIssueField };
742
+
743
+ //# sourceMappingURL=index.js.map