@forge-glance/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,722 @@
1
+ // src/logger.ts
2
+ var noopLogger = {
3
+ debug() {},
4
+ info() {},
5
+ warn() {},
6
+ error() {}
7
+ };
8
+
9
+ // src/MRDetailFetcher.ts
10
+ class MRDetailFetcher {
11
+ baseURL;
12
+ token;
13
+ log;
14
+ constructor(baseURL, token, options = {}) {
15
+ this.baseURL = baseURL.replace(/\/$/, "");
16
+ this.token = token;
17
+ this.log = options.logger ?? noopLogger;
18
+ }
19
+ async fetchDetail(projectId, mrIid) {
20
+ const url = `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/discussions?per_page=100`;
21
+ const res = await fetch(url, {
22
+ headers: { "PRIVATE-TOKEN": this.token }
23
+ });
24
+ if (!res.ok) {
25
+ throw new Error(`MR discussions fetch failed: ${res.status} ${res.statusText}`);
26
+ }
27
+ const raw = await res.json();
28
+ const discussions = raw.map((d) => ({
29
+ id: d.id,
30
+ resolvable: null,
31
+ resolved: null,
32
+ notes: d.notes.map(toNote)
33
+ }));
34
+ this.log.debug("MRDetailFetcher.fetchDetail", {
35
+ projectId,
36
+ mrIid,
37
+ discussionCount: discussions.length
38
+ });
39
+ return { mrIid, repositoryId: `gitlab:${projectId}`, discussions };
40
+ }
41
+ }
42
+ function toNote(n) {
43
+ return {
44
+ id: n.id,
45
+ body: n.body,
46
+ author: toAuthor(n.author),
47
+ createdAt: n.created_at,
48
+ system: n.system,
49
+ type: n.type,
50
+ resolvable: n.resolvable ?? null,
51
+ resolved: n.resolved ?? null,
52
+ position: n.position ? toPosition(n.position) : null
53
+ };
54
+ }
55
+ function toAuthor(a) {
56
+ return {
57
+ id: `gitlab:user:${a.id}`,
58
+ username: a.username,
59
+ name: a.name,
60
+ avatarUrl: a.avatar_url
61
+ };
62
+ }
63
+ function toPosition(p) {
64
+ return {
65
+ newPath: p.new_path ?? null,
66
+ oldPath: p.old_path ?? null,
67
+ newLine: p.new_line ?? null,
68
+ oldLine: p.old_line ?? null,
69
+ positionType: p.position_type ?? null
70
+ };
71
+ }
72
+
73
+ // src/GitLabProvider.ts
74
+ function parseGitLabRepoId(repositoryId) {
75
+ const parts = repositoryId.split(":");
76
+ return parseInt(parts.at(-1) ?? "0", 10);
77
+ }
78
+ var MR_DASHBOARD_FRAGMENT = `
79
+ fragment MRDashboardFields on MergeRequest {
80
+ id iid projectId title description state draft
81
+ sourceBranch targetBranch webUrl
82
+ diffHeadSha
83
+ updatedAt createdAt
84
+ conflicts
85
+ detailedMergeStatus
86
+ approved
87
+ diffStatsSummary { additions deletions fileCount }
88
+ author { id username name avatarUrl }
89
+ assignees(first: 20) { nodes { id username name avatarUrl } }
90
+ reviewers(first: 20) { nodes { id username name avatarUrl } }
91
+ approvedBy(first: 20) { nodes { id username name avatarUrl } }
92
+ approvalsLeft
93
+ resolvableDiscussionsCount
94
+ resolvedDiscussionsCount
95
+ headPipeline {
96
+ id iid status
97
+ createdAt
98
+ path
99
+ stages(first: 20) { nodes {
100
+ name
101
+ jobs(first: 50) { nodes {
102
+ id name status
103
+ allowFailure
104
+ webPath
105
+ stage { name }
106
+ }}
107
+ }}
108
+ }
109
+ }
110
+ `;
111
+ var AUTHORED_QUERY = `
112
+ query GlanceDashboardAuthored {
113
+ currentUser {
114
+ authoredMergeRequests(state: opened, first: 100) {
115
+ nodes { ...MRDashboardFields }
116
+ }
117
+ }
118
+ }
119
+ ${MR_DASHBOARD_FRAGMENT}
120
+ `;
121
+ var REVIEWING_QUERY = `
122
+ query GlanceDashboardReviewing {
123
+ currentUser {
124
+ reviewRequestedMergeRequests(state: opened, first: 100) {
125
+ nodes { ...MRDashboardFields }
126
+ }
127
+ }
128
+ }
129
+ ${MR_DASHBOARD_FRAGMENT}
130
+ `;
131
+ var ASSIGNED_QUERY = `
132
+ query GlanceDashboardAssigned {
133
+ currentUser {
134
+ assignedMergeRequests(state: opened, first: 100) {
135
+ nodes { ...MRDashboardFields }
136
+ }
137
+ }
138
+ }
139
+ ${MR_DASHBOARD_FRAGMENT}
140
+ `;
141
+ function numericId(gid) {
142
+ const parts = gid.split("/");
143
+ return parseInt(parts[parts.length - 1] ?? "0", 10);
144
+ }
145
+ function domainId(type, id) {
146
+ return `gitlab:${type}:${id}`;
147
+ }
148
+ function toUserRef(u) {
149
+ return {
150
+ id: `gitlab:user:${numericId(u.id)}`,
151
+ username: u.username,
152
+ name: u.name,
153
+ avatarUrl: u.avatarUrl
154
+ };
155
+ }
156
+ function toPipeline(p, baseURL) {
157
+ const allJobs = p.stages.nodes.flatMap((stage) => stage.jobs.nodes.map((job) => ({
158
+ id: `gitlab:job:${numericId(job.id)}`,
159
+ name: job.name,
160
+ stage: job.stage.name,
161
+ status: job.status,
162
+ allowFailure: job.allowFailure,
163
+ webUrl: job.webPath ? `${baseURL}${job.webPath}` : null
164
+ })));
165
+ return {
166
+ id: domainId("pipeline", numericId(p.id)),
167
+ status: normalizePipelineStatus(p),
168
+ createdAt: p.createdAt,
169
+ webUrl: p.path ? `${baseURL}${p.path}` : null,
170
+ jobs: allJobs
171
+ };
172
+ }
173
+ function normalizePipelineStatus(p) {
174
+ const allJobs = p.stages.nodes.flatMap((s) => s.jobs.nodes);
175
+ const hasAllowFailFailed = allJobs.some((j) => j.allowFailure && j.status === "failed");
176
+ if (p.status === "success" && hasAllowFailFailed) {
177
+ return "success_with_warnings";
178
+ }
179
+ return p.status;
180
+ }
181
+ function toMR(gql, role, baseURL) {
182
+ const resolvable = gql.resolvableDiscussionsCount ?? 0;
183
+ const resolved = gql.resolvedDiscussionsCount ?? 0;
184
+ const unresolvedThreadCount = Math.max(0, resolvable - resolved);
185
+ const diffStats = gql.diffStatsSummary ? {
186
+ additions: gql.diffStatsSummary.additions,
187
+ deletions: gql.diffStatsSummary.deletions,
188
+ filesChanged: gql.diffStatsSummary.fileCount
189
+ } : null;
190
+ return {
191
+ id: `gitlab:mr:${numericId(gql.id)}`,
192
+ iid: parseInt(gql.iid, 10),
193
+ repositoryId: `gitlab:${gql.projectId}`,
194
+ title: gql.title,
195
+ description: gql.description ?? null,
196
+ state: gql.state,
197
+ draft: gql.draft,
198
+ conflicts: gql.conflicts || gql.detailedMergeStatus === "conflict",
199
+ webUrl: gql.webUrl,
200
+ sourceBranch: gql.sourceBranch,
201
+ targetBranch: gql.targetBranch,
202
+ createdAt: gql.createdAt,
203
+ updatedAt: gql.updatedAt,
204
+ sha: gql.diffHeadSha,
205
+ author: toUserRef(gql.author),
206
+ assignees: gql.assignees.nodes.map(toUserRef),
207
+ reviewers: gql.reviewers.nodes.map(toUserRef),
208
+ roles: [role],
209
+ pipeline: gql.headPipeline ? toPipeline(gql.headPipeline, baseURL) : null,
210
+ unresolvedThreadCount,
211
+ approvalsLeft: gql.approvalsLeft ?? 0,
212
+ approved: gql.approved ?? false,
213
+ approvedBy: gql.approvedBy.nodes.map(toUserRef),
214
+ diffStats,
215
+ detailedMergeStatus: gql.detailedMergeStatus ?? null
216
+ };
217
+ }
218
+ var MR_DETAIL_QUERY = `
219
+ query GlanceMRDetail($projectPath: ID!, $iid: String!) {
220
+ project(fullPath: $projectPath) {
221
+ mergeRequest(iid: $iid) {
222
+ ...MRDashboardFields
223
+ }
224
+ }
225
+ }
226
+ ${MR_DASHBOARD_FRAGMENT}
227
+ `;
228
+
229
+ class GitLabProvider {
230
+ providerName = "gitlab";
231
+ baseURL;
232
+ token;
233
+ log;
234
+ mrDetailFetcher;
235
+ constructor(baseURL, token, options = {}) {
236
+ this.baseURL = baseURL.replace(/\/$/, "");
237
+ this.token = token;
238
+ this.log = options.logger ?? noopLogger;
239
+ this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, { logger: this.log });
240
+ }
241
+ async validateToken() {
242
+ const url = `${this.baseURL}/api/v4/user`;
243
+ const res = await fetch(url, {
244
+ headers: { "PRIVATE-TOKEN": this.token }
245
+ });
246
+ if (!res.ok) {
247
+ throw new Error(`Token validation failed: ${res.status} ${res.statusText}`);
248
+ }
249
+ const user = await res.json();
250
+ return {
251
+ id: `gitlab:user:${user.id}`,
252
+ username: user.username,
253
+ name: user.name,
254
+ avatarUrl: user.avatar_url
255
+ };
256
+ }
257
+ async fetchSingleMR(projectPath, mrIid, currentUserNumericId) {
258
+ let resp;
259
+ try {
260
+ resp = await this.runQuery(MR_DETAIL_QUERY, {
261
+ projectPath,
262
+ iid: String(mrIid)
263
+ });
264
+ } catch (err) {
265
+ const message = err instanceof Error ? err.message : String(err);
266
+ this.log.warn("fetchSingleMR failed", { projectPath, mrIid, message });
267
+ return null;
268
+ }
269
+ const gql = resp.project?.mergeRequest;
270
+ if (!gql)
271
+ return null;
272
+ const roles = [];
273
+ if (currentUserNumericId !== null) {
274
+ const userGqlId = `gid://gitlab/User/${currentUserNumericId}`;
275
+ if (gql.author.id === userGqlId)
276
+ roles.push("author");
277
+ if (gql.assignees.nodes.some((u) => u.id === userGqlId))
278
+ roles.push("assignee");
279
+ if (gql.reviewers.nodes.some((u) => u.id === userGqlId))
280
+ roles.push("reviewer");
281
+ }
282
+ const pr = toMR(gql, roles[0] ?? "author", this.baseURL);
283
+ pr.roles = roles.length > 0 ? roles : pr.roles;
284
+ return pr;
285
+ }
286
+ async fetchPullRequests() {
287
+ const [authored, reviewing, assigned] = await Promise.all([
288
+ this.runQuery(AUTHORED_QUERY),
289
+ this.runQuery(REVIEWING_QUERY),
290
+ this.runQuery(ASSIGNED_QUERY)
291
+ ]);
292
+ const byId = new Map;
293
+ const addAll = (mrs, role) => {
294
+ for (const gql of mrs) {
295
+ const existing = byId.get(gql.id);
296
+ if (existing) {
297
+ if (!existing.roles.includes(role)) {
298
+ existing.roles.push(role);
299
+ }
300
+ } else {
301
+ byId.set(gql.id, toMR(gql, role, this.baseURL));
302
+ }
303
+ }
304
+ };
305
+ addAll(authored.currentUser.authoredMergeRequests.nodes, "author");
306
+ addAll(reviewing.currentUser.reviewRequestedMergeRequests.nodes, "reviewer");
307
+ addAll(assigned.currentUser.assignedMergeRequests.nodes, "assignee");
308
+ const prs = [...byId.values()];
309
+ this.log.debug("fetchPullRequests", { count: prs.length });
310
+ return prs;
311
+ }
312
+ async fetchMRDiscussions(repositoryId, mrIid) {
313
+ const projectId = parseGitLabRepoId(repositoryId);
314
+ return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
315
+ }
316
+ async restRequest(method, path, body) {
317
+ const url = `${this.baseURL}${path}`;
318
+ const headers = {
319
+ "PRIVATE-TOKEN": this.token
320
+ };
321
+ if (body !== undefined) {
322
+ headers["Content-Type"] = "application/json";
323
+ }
324
+ return fetch(url, {
325
+ method,
326
+ headers,
327
+ body: body !== undefined ? JSON.stringify(body) : undefined
328
+ });
329
+ }
330
+ async runQuery(query, variables) {
331
+ const url = `${this.baseURL}/api/graphql`;
332
+ const body = JSON.stringify({ query, variables: variables ?? {} });
333
+ const res = await fetch(url, {
334
+ method: "POST",
335
+ headers: {
336
+ "Content-Type": "application/json",
337
+ Authorization: `Bearer ${this.token}`
338
+ },
339
+ body
340
+ });
341
+ if (!res.ok) {
342
+ throw new Error(`GraphQL request failed: ${res.status} ${res.statusText}`);
343
+ }
344
+ const envelope = await res.json();
345
+ if (envelope.errors?.length) {
346
+ const msg = envelope.errors.map((e) => e.message).join("; ");
347
+ throw new Error(`GraphQL errors: ${msg}`);
348
+ }
349
+ if (!envelope.data) {
350
+ throw new Error("GraphQL response missing data");
351
+ }
352
+ return envelope.data;
353
+ }
354
+ }
355
+
356
+ // src/GitHubProvider.ts
357
+ function toUserRef2(u) {
358
+ return {
359
+ id: `github:user:${u.id}`,
360
+ username: u.login,
361
+ name: u.name ?? u.login,
362
+ avatarUrl: u.avatar_url
363
+ };
364
+ }
365
+ function normalizePRState(pr) {
366
+ if (pr.merged_at)
367
+ return "merged";
368
+ if (pr.state === "open")
369
+ return "opened";
370
+ return "closed";
371
+ }
372
+ function toPipeline2(checkRuns, prHtmlUrl) {
373
+ if (checkRuns.length === 0)
374
+ return null;
375
+ const jobs = checkRuns.map((cr) => ({
376
+ id: `github:check:${cr.id}`,
377
+ name: cr.name,
378
+ stage: "checks",
379
+ status: normalizeCheckStatus(cr),
380
+ allowFailure: false,
381
+ webUrl: cr.html_url
382
+ }));
383
+ const statuses = jobs.map((j) => j.status);
384
+ let overallStatus;
385
+ if (statuses.some((s) => s === "failed")) {
386
+ overallStatus = "failed";
387
+ } else if (statuses.some((s) => s === "running")) {
388
+ overallStatus = "running";
389
+ } else if (statuses.some((s) => s === "pending")) {
390
+ overallStatus = "pending";
391
+ } else if (statuses.every((s) => s === "success" || s === "skipped")) {
392
+ overallStatus = "success";
393
+ } else {
394
+ overallStatus = "pending";
395
+ }
396
+ return {
397
+ id: `github:checks:${prHtmlUrl}`,
398
+ status: overallStatus,
399
+ createdAt: null,
400
+ webUrl: `${prHtmlUrl}/checks`,
401
+ jobs
402
+ };
403
+ }
404
+ function normalizeCheckStatus(cr) {
405
+ if (cr.status === "completed") {
406
+ switch (cr.conclusion) {
407
+ case "success":
408
+ return "success";
409
+ case "failure":
410
+ case "timed_out":
411
+ return "failed";
412
+ case "cancelled":
413
+ return "canceled";
414
+ case "skipped":
415
+ return "skipped";
416
+ case "neutral":
417
+ return "success";
418
+ case "action_required":
419
+ return "manual";
420
+ default:
421
+ return "pending";
422
+ }
423
+ }
424
+ if (cr.status === "in_progress")
425
+ return "running";
426
+ return "pending";
427
+ }
428
+
429
+ class GitHubProvider {
430
+ providerName = "github";
431
+ baseURL;
432
+ apiBase;
433
+ token;
434
+ log;
435
+ constructor(baseURL, token, options = {}) {
436
+ this.baseURL = baseURL.replace(/\/$/, "");
437
+ this.token = token;
438
+ this.log = options.logger ?? noopLogger;
439
+ if (this.baseURL === "https://github.com" || this.baseURL === "https://www.github.com") {
440
+ this.apiBase = "https://api.github.com";
441
+ } else {
442
+ this.apiBase = `${this.baseURL}/api/v3`;
443
+ }
444
+ }
445
+ async validateToken() {
446
+ const res = await this.api("GET", "/user");
447
+ if (!res.ok) {
448
+ throw new Error(`GitHub token validation failed: ${res.status} ${res.statusText}`);
449
+ }
450
+ const user = await res.json();
451
+ return toUserRef2(user);
452
+ }
453
+ async fetchPullRequests() {
454
+ const [authored, reviewRequested, assigned] = await Promise.all([
455
+ this.searchPRs("is:open is:pr author:@me"),
456
+ this.searchPRs("is:open is:pr review-requested:@me"),
457
+ this.searchPRs("is:open is:pr assignee:@me")
458
+ ]);
459
+ const byKey = new Map;
460
+ const roles = new Map;
461
+ const addAll = (prs, role) => {
462
+ for (const pr of prs) {
463
+ const key = `${pr.base.repo.id}:${pr.number}`;
464
+ if (!byKey.has(key)) {
465
+ byKey.set(key, pr);
466
+ roles.set(key, [role]);
467
+ } else {
468
+ const existing = roles.get(key);
469
+ if (!existing.includes(role))
470
+ existing.push(role);
471
+ }
472
+ }
473
+ };
474
+ addAll(authored, "author");
475
+ addAll(reviewRequested, "reviewer");
476
+ addAll(assigned, "assignee");
477
+ const entries = [...byKey.entries()];
478
+ const results = await Promise.all(entries.map(async ([key, pr]) => {
479
+ const prRoles = roles.get(key) ?? ["author"];
480
+ const [reviews, checkRuns] = await Promise.all([
481
+ this.fetchReviews(pr.base.repo.full_name, pr.number),
482
+ this.fetchCheckRuns(pr.base.repo.full_name, pr.head.sha)
483
+ ]);
484
+ return this.toPullRequest(pr, prRoles, reviews, checkRuns);
485
+ }));
486
+ this.log.debug("GitHubProvider.fetchPullRequests", { count: results.length });
487
+ return results;
488
+ }
489
+ async fetchSingleMR(projectPath, mrIid, _currentUserNumericId) {
490
+ try {
491
+ const res = await this.api("GET", `/repos/${projectPath}/pulls/${mrIid}`);
492
+ if (!res.ok)
493
+ return null;
494
+ const pr = await res.json();
495
+ const [reviews, checkRuns] = await Promise.all([
496
+ this.fetchReviews(projectPath, mrIid),
497
+ this.fetchCheckRuns(projectPath, pr.head.sha)
498
+ ]);
499
+ const currentUser = await this.api("GET", "/user");
500
+ const currentUserData = await currentUser.json();
501
+ const prRoles = [];
502
+ if (pr.user.id === currentUserData.id)
503
+ prRoles.push("author");
504
+ if (pr.assignees.some((a) => a.id === currentUserData.id))
505
+ prRoles.push("assignee");
506
+ if (pr.requested_reviewers.some((r) => r.id === currentUserData.id))
507
+ prRoles.push("reviewer");
508
+ return this.toPullRequest(pr, prRoles.length > 0 ? prRoles : ["author"], reviews, checkRuns);
509
+ } catch (err) {
510
+ const message = err instanceof Error ? err.message : String(err);
511
+ this.log.warn("GitHubProvider.fetchSingleMR failed", {
512
+ projectPath,
513
+ mrIid,
514
+ message
515
+ });
516
+ return null;
517
+ }
518
+ }
519
+ async fetchMRDiscussions(repositoryId, mrIid) {
520
+ const repoId = parseInt(repositoryId.split(":").pop() ?? "0", 10);
521
+ const repoRes = await this.api("GET", `/repositories/${repoId}`);
522
+ if (!repoRes.ok) {
523
+ throw new Error(`Failed to fetch repo: ${repoRes.status}`);
524
+ }
525
+ const repo = await repoRes.json();
526
+ const [reviewComments, issueComments] = await Promise.all([
527
+ this.fetchAllPages(`/repos/${repo.full_name}/pulls/${mrIid}/comments?per_page=100`),
528
+ this.fetchAllPages(`/repos/${repo.full_name}/issues/${mrIid}/comments?per_page=100`)
529
+ ]);
530
+ const discussions = [];
531
+ for (const c of issueComments) {
532
+ discussions.push({
533
+ id: `gh-issue-comment-${c.id}`,
534
+ resolvable: null,
535
+ resolved: null,
536
+ notes: [toNote2(c)]
537
+ });
538
+ }
539
+ const threadMap = new Map;
540
+ for (const c of reviewComments) {
541
+ const rootId = c.in_reply_to_id ?? c.id;
542
+ const thread = threadMap.get(rootId) ?? [];
543
+ thread.push(c);
544
+ threadMap.set(rootId, thread);
545
+ }
546
+ for (const [rootId, comments] of threadMap) {
547
+ comments.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
548
+ discussions.push({
549
+ id: `gh-review-thread-${rootId}`,
550
+ resolvable: true,
551
+ resolved: null,
552
+ notes: comments.map(toNote2)
553
+ });
554
+ }
555
+ return { mrIid, repositoryId, discussions };
556
+ }
557
+ async restRequest(method, path, body) {
558
+ return this.api(method, path, body);
559
+ }
560
+ async api(method, path, body) {
561
+ const url = `${this.apiBase}${path}`;
562
+ const headers = {
563
+ Authorization: `Bearer ${this.token}`,
564
+ Accept: "application/vnd.github+json",
565
+ "X-GitHub-Api-Version": "2022-11-28"
566
+ };
567
+ if (body !== undefined) {
568
+ headers["Content-Type"] = "application/json";
569
+ }
570
+ return fetch(url, {
571
+ method,
572
+ headers,
573
+ body: body !== undefined ? JSON.stringify(body) : undefined
574
+ });
575
+ }
576
+ async searchPRs(qualifiers) {
577
+ const q = encodeURIComponent(qualifiers);
578
+ const res = await this.api("GET", `/search/issues?q=${q}&per_page=100&sort=updated`);
579
+ if (!res.ok) {
580
+ this.log.warn("GitHub search failed", { status: res.status, qualifiers });
581
+ return [];
582
+ }
583
+ const data = await res.json();
584
+ const prPromises = data.items.filter((item) => item.pull_request).map(async (item) => {
585
+ const repoPath = item.repository_url.replace(`${this.apiBase}/repos/`, "");
586
+ const res2 = await this.api("GET", `/repos/${repoPath}/pulls/${item.number}`);
587
+ if (!res2.ok)
588
+ return null;
589
+ return await res2.json();
590
+ });
591
+ const results = await Promise.all(prPromises);
592
+ return results.filter((pr) => pr !== null);
593
+ }
594
+ async fetchReviews(repoPath, prNumber) {
595
+ return this.fetchAllPages(`/repos/${repoPath}/pulls/${prNumber}/reviews?per_page=100`);
596
+ }
597
+ async fetchCheckRuns(repoPath, sha) {
598
+ try {
599
+ const res = await this.api("GET", `/repos/${repoPath}/commits/${sha}/check-runs?per_page=100`);
600
+ if (!res.ok)
601
+ return [];
602
+ const data = await res.json();
603
+ return data.check_runs;
604
+ } catch {
605
+ return [];
606
+ }
607
+ }
608
+ async fetchAllPages(path) {
609
+ const results = [];
610
+ let url = path;
611
+ while (url) {
612
+ const res = await this.api("GET", url);
613
+ if (!res.ok)
614
+ break;
615
+ const items = await res.json();
616
+ results.push(...items);
617
+ const linkHeader = res.headers.get("Link");
618
+ const nextMatch = linkHeader?.match(/<([^>]+)>;\s*rel="next"/);
619
+ if (nextMatch) {
620
+ url = nextMatch[1].replace(this.apiBase, "");
621
+ } else {
622
+ url = null;
623
+ }
624
+ }
625
+ return results;
626
+ }
627
+ toPullRequest(pr, roles, reviews, checkRuns) {
628
+ const latestReviewByUser = new Map;
629
+ for (const r of reviews.sort((a, b) => new Date(a.submitted_at).getTime() - new Date(b.submitted_at).getTime())) {
630
+ latestReviewByUser.set(r.user.id, r);
631
+ }
632
+ const approvedBy = [];
633
+ let changesRequested = 0;
634
+ for (const r of latestReviewByUser.values()) {
635
+ if (r.state === "APPROVED") {
636
+ approvedBy.push(toUserRef2(r.user));
637
+ } else if (r.state === "CHANGES_REQUESTED") {
638
+ changesRequested++;
639
+ }
640
+ }
641
+ const approvalsLeft = changesRequested;
642
+ const diffStats = pr.additions !== undefined ? {
643
+ additions: pr.additions,
644
+ deletions: pr.deletions ?? 0,
645
+ filesChanged: pr.changed_files ?? 0
646
+ } : null;
647
+ const conflicts = pr.mergeable === false || pr.mergeable_state === "dirty";
648
+ const pipeline = toPipeline2(checkRuns, pr.html_url);
649
+ return {
650
+ id: `github:pr:${pr.id}`,
651
+ iid: pr.number,
652
+ repositoryId: `github:${pr.base.repo.id}`,
653
+ title: pr.title,
654
+ state: normalizePRState(pr),
655
+ draft: pr.draft,
656
+ conflicts,
657
+ webUrl: pr.html_url,
658
+ sourceBranch: pr.head.ref,
659
+ targetBranch: pr.base.ref,
660
+ createdAt: pr.created_at,
661
+ updatedAt: pr.updated_at,
662
+ sha: pr.head.sha,
663
+ author: toUserRef2(pr.user),
664
+ assignees: pr.assignees.map(toUserRef2),
665
+ reviewers: pr.requested_reviewers.map(toUserRef2),
666
+ roles,
667
+ pipeline,
668
+ description: pr.body ?? null,
669
+ unresolvedThreadCount: 0,
670
+ approvalsLeft,
671
+ approved: approvedBy.length > 0 && changesRequested === 0,
672
+ approvedBy,
673
+ diffStats,
674
+ detailedMergeStatus: null
675
+ };
676
+ }
677
+ }
678
+ function toNote2(c) {
679
+ const position = c.path ? {
680
+ newPath: c.path,
681
+ oldPath: c.path,
682
+ newLine: c.line ?? null,
683
+ oldLine: c.original_line ?? null,
684
+ positionType: c.path ? "text" : null
685
+ } : null;
686
+ return {
687
+ id: c.id,
688
+ body: c.body,
689
+ author: toNoteAuthor(c.user),
690
+ createdAt: c.created_at,
691
+ system: false,
692
+ type: c.path ? "DiffNote" : "DiscussionNote",
693
+ resolvable: c.path ? true : null,
694
+ resolved: null,
695
+ position
696
+ };
697
+ }
698
+ function toNoteAuthor(u) {
699
+ return {
700
+ id: `github:user:${u.id}`,
701
+ username: u.login,
702
+ name: u.name ?? u.login,
703
+ avatarUrl: u.avatar_url
704
+ };
705
+ }
706
+
707
+ // src/providers.ts
708
+ function createProvider(provider, baseURL, token, options = {}) {
709
+ switch (provider) {
710
+ case "gitlab":
711
+ return new GitLabProvider(baseURL, token, options);
712
+ case "github":
713
+ return new GitHubProvider(baseURL, token, options);
714
+ default:
715
+ throw new Error(`Unknown provider: ${provider}`);
716
+ }
717
+ }
718
+ var SUPPORTED_PROVIDERS = ["gitlab", "github"];
719
+ export {
720
+ createProvider,
721
+ SUPPORTED_PROVIDERS
722
+ };