@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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +111 -0
- package/dist/claude-context.d.ts +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +714 -0
- package/dist/index.js.map +1 -0
- package/metadata.json +33 -0
- package/package.json +58 -0
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
|