@forge-glance/sdk 0.1.0 → 0.2.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.
package/dist/providers.js CHANGED
@@ -236,8 +236,20 @@ class GitLabProvider {
236
236
  this.baseURL = baseURL.replace(/\/$/, "");
237
237
  this.token = token;
238
238
  this.log = options.logger ?? noopLogger;
239
- this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, { logger: this.log });
239
+ this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, {
240
+ logger: this.log
241
+ });
240
242
  }
243
+ capabilities = {
244
+ canMerge: true,
245
+ canApprove: true,
246
+ canUnapprove: true,
247
+ canRebase: true,
248
+ canAutoMerge: true,
249
+ canResolveDiscussions: true,
250
+ canRetryPipeline: true,
251
+ canRequestReReview: true
252
+ };
241
253
  async validateToken() {
242
254
  const url = `${this.baseURL}/api/v4/user`;
243
255
  const res = await fetch(url, {
@@ -313,6 +325,113 @@ class GitLabProvider {
313
325
  const projectId = parseGitLabRepoId(repositoryId);
314
326
  return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
315
327
  }
328
+ async fetchBranchProtectionRules(projectPath) {
329
+ const encoded = encodeURIComponent(projectPath);
330
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/protected_branches?per_page=100`, { headers: { "PRIVATE-TOKEN": this.token } });
331
+ if (!res.ok) {
332
+ throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
333
+ }
334
+ const branches = await res.json();
335
+ return branches.map((b) => ({
336
+ pattern: b.name,
337
+ allowForcePush: b.allow_force_push,
338
+ allowDeletion: false,
339
+ requiredApprovals: 0,
340
+ requireStatusChecks: false,
341
+ raw: b
342
+ }));
343
+ }
344
+ async deleteBranch(projectPath, branch) {
345
+ const encoded = encodeURIComponent(projectPath);
346
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/repository/branches/${encodeURIComponent(branch)}`, { method: "DELETE", headers: { "PRIVATE-TOKEN": this.token } });
347
+ if (!res.ok) {
348
+ throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
349
+ }
350
+ }
351
+ async fetchPullRequestByBranch(projectPath, sourceBranch) {
352
+ const encoded = encodeURIComponent(projectPath);
353
+ const url = `${this.baseURL}/api/v4/projects/${encoded}/merge_requests?source_branch=${encodeURIComponent(sourceBranch)}&state=opened&per_page=1`;
354
+ const res = await fetch(url, {
355
+ headers: { "PRIVATE-TOKEN": this.token }
356
+ });
357
+ if (!res.ok) {
358
+ this.log.warn("fetchPullRequestByBranch failed", {
359
+ projectPath,
360
+ sourceBranch,
361
+ status: res.status
362
+ });
363
+ return null;
364
+ }
365
+ const mrs = await res.json();
366
+ if (!mrs[0])
367
+ return null;
368
+ return this.fetchSingleMR(projectPath, mrs[0].iid, null);
369
+ }
370
+ async createPullRequest(input) {
371
+ const encoded = encodeURIComponent(input.projectPath);
372
+ const body = {
373
+ source_branch: input.sourceBranch,
374
+ target_branch: input.targetBranch,
375
+ title: input.title
376
+ };
377
+ if (input.description != null)
378
+ body.description = input.description;
379
+ if (input.draft != null)
380
+ body.draft = input.draft;
381
+ if (input.labels?.length)
382
+ body.labels = input.labels.join(",");
383
+ if (input.assignees?.length)
384
+ body.assignee_ids = input.assignees;
385
+ if (input.reviewers?.length)
386
+ body.reviewer_ids = input.reviewers;
387
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests`, {
388
+ method: "POST",
389
+ headers: {
390
+ "PRIVATE-TOKEN": this.token,
391
+ "Content-Type": "application/json"
392
+ },
393
+ body: JSON.stringify(body)
394
+ });
395
+ if (!res.ok) {
396
+ const text = await res.text();
397
+ throw new Error(`createPullRequest failed: ${res.status} ${text}`);
398
+ }
399
+ const created = await res.json();
400
+ return this.fetchSingleMRWithRetry(input.projectPath, created.iid, "Created MR but failed to fetch it back");
401
+ }
402
+ async updatePullRequest(projectPath, mrIid, input) {
403
+ const encoded = encodeURIComponent(projectPath);
404
+ const body = {};
405
+ if (input.title != null)
406
+ body.title = input.title;
407
+ if (input.description != null)
408
+ body.description = input.description;
409
+ if (input.draft != null)
410
+ body.draft = input.draft;
411
+ if (input.targetBranch != null)
412
+ body.target_branch = input.targetBranch;
413
+ if (input.labels)
414
+ body.labels = input.labels.join(",");
415
+ if (input.assignees)
416
+ body.assignee_ids = input.assignees;
417
+ if (input.reviewers)
418
+ body.reviewer_ids = input.reviewers;
419
+ if (input.stateEvent)
420
+ body.state_event = input.stateEvent;
421
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
422
+ method: "PUT",
423
+ headers: {
424
+ "PRIVATE-TOKEN": this.token,
425
+ "Content-Type": "application/json"
426
+ },
427
+ body: JSON.stringify(body)
428
+ });
429
+ if (!res.ok) {
430
+ const text = await res.text();
431
+ throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
432
+ }
433
+ return this.fetchSingleMRWithRetry(projectPath, mrIid, "Updated MR but failed to fetch it back");
434
+ }
316
435
  async restRequest(method, path, body) {
317
436
  const url = `${this.baseURL}${path}`;
318
437
  const headers = {
@@ -327,6 +446,171 @@ class GitLabProvider {
327
446
  body: body !== undefined ? JSON.stringify(body) : undefined
328
447
  });
329
448
  }
449
+ async mergePullRequest(projectPath, mrIid, input) {
450
+ const encoded = encodeURIComponent(projectPath);
451
+ const body = {};
452
+ if (input?.commitMessage != null)
453
+ body.merge_commit_message = input.commitMessage;
454
+ if (input?.squashCommitMessage != null)
455
+ body.squash_commit_message = input.squashCommitMessage;
456
+ if (input?.squash != null)
457
+ body.squash = input.squash;
458
+ if (input?.shouldRemoveSourceBranch != null)
459
+ body.should_remove_source_branch = input.shouldRemoveSourceBranch;
460
+ if (input?.sha != null)
461
+ body.sha = input.sha;
462
+ if (input?.mergeMethod === "squash" && input?.squash == null)
463
+ body.squash = true;
464
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
465
+ method: "PUT",
466
+ headers: {
467
+ "PRIVATE-TOKEN": this.token,
468
+ "Content-Type": "application/json"
469
+ },
470
+ body: JSON.stringify(body)
471
+ });
472
+ if (!res.ok) {
473
+ const text = await res.text();
474
+ throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
475
+ }
476
+ return this.fetchSingleMRWithRetry(projectPath, mrIid, "Merged MR but failed to fetch it back");
477
+ }
478
+ async approvePullRequest(projectPath, mrIid) {
479
+ const encoded = encodeURIComponent(projectPath);
480
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/approve`, {
481
+ method: "POST",
482
+ headers: { "PRIVATE-TOKEN": this.token }
483
+ });
484
+ if (!res.ok) {
485
+ const text = await res.text().catch(() => "");
486
+ throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
487
+ }
488
+ }
489
+ async unapprovePullRequest(projectPath, mrIid) {
490
+ const encoded = encodeURIComponent(projectPath);
491
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/unapprove`, {
492
+ method: "POST",
493
+ headers: { "PRIVATE-TOKEN": this.token }
494
+ });
495
+ if (!res.ok) {
496
+ const text = await res.text().catch(() => "");
497
+ throw new Error(`unapprovePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
498
+ }
499
+ }
500
+ async rebasePullRequest(projectPath, mrIid) {
501
+ const encoded = encodeURIComponent(projectPath);
502
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/rebase`, {
503
+ method: "PUT",
504
+ headers: { "PRIVATE-TOKEN": this.token }
505
+ });
506
+ if (!res.ok) {
507
+ const text = await res.text().catch(() => "");
508
+ throw new Error(`rebasePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
509
+ }
510
+ }
511
+ async setAutoMerge(projectPath, mrIid) {
512
+ const encoded = encodeURIComponent(projectPath);
513
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
514
+ method: "PUT",
515
+ headers: {
516
+ "PRIVATE-TOKEN": this.token,
517
+ "Content-Type": "application/json"
518
+ },
519
+ body: JSON.stringify({ merge_when_pipeline_succeeds: true })
520
+ });
521
+ if (!res.ok) {
522
+ const text = await res.text().catch(() => "");
523
+ throw new Error(`setAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
524
+ }
525
+ }
526
+ async cancelAutoMerge(projectPath, mrIid) {
527
+ const encoded = encodeURIComponent(projectPath);
528
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/cancel_merge_when_pipeline_succeeds`, {
529
+ method: "POST",
530
+ headers: { "PRIVATE-TOKEN": this.token }
531
+ });
532
+ if (!res.ok) {
533
+ const text = await res.text().catch(() => "");
534
+ throw new Error(`cancelAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
535
+ }
536
+ }
537
+ async resolveDiscussion(projectPath, mrIid, discussionId) {
538
+ const encoded = encodeURIComponent(projectPath);
539
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
540
+ method: "PUT",
541
+ headers: {
542
+ "PRIVATE-TOKEN": this.token,
543
+ "Content-Type": "application/json"
544
+ },
545
+ body: JSON.stringify({ resolved: true })
546
+ });
547
+ if (!res.ok) {
548
+ const text = await res.text().catch(() => "");
549
+ throw new Error(`resolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
550
+ }
551
+ }
552
+ async unresolveDiscussion(projectPath, mrIid, discussionId) {
553
+ const encoded = encodeURIComponent(projectPath);
554
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
555
+ method: "PUT",
556
+ headers: {
557
+ "PRIVATE-TOKEN": this.token,
558
+ "Content-Type": "application/json"
559
+ },
560
+ body: JSON.stringify({ resolved: false })
561
+ });
562
+ if (!res.ok) {
563
+ const text = await res.text().catch(() => "");
564
+ throw new Error(`unresolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
565
+ }
566
+ }
567
+ async retryPipeline(projectPath, pipelineId) {
568
+ const encoded = encodeURIComponent(projectPath);
569
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/pipelines/${pipelineId}/retry`, {
570
+ method: "POST",
571
+ headers: { "PRIVATE-TOKEN": this.token }
572
+ });
573
+ if (!res.ok) {
574
+ const text = await res.text().catch(() => "");
575
+ throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
576
+ }
577
+ }
578
+ async requestReReview(projectPath, mrIid, _reviewerUsernames) {
579
+ const encoded = encodeURIComponent(projectPath);
580
+ const mrRes = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, { headers: { "PRIVATE-TOKEN": this.token } });
581
+ if (!mrRes.ok) {
582
+ const text = await mrRes.text().catch(() => "");
583
+ throw new Error(`requestReReview: failed to fetch MR: ${mrRes.status}${text ? ` — ${text}` : ""}`);
584
+ }
585
+ const mr = await mrRes.json();
586
+ const reviewerIds = mr.reviewers?.map((r) => r.id) ?? [];
587
+ if (reviewerIds.length === 0) {
588
+ return;
589
+ }
590
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
591
+ method: "PUT",
592
+ headers: {
593
+ "PRIVATE-TOKEN": this.token,
594
+ "Content-Type": "application/json"
595
+ },
596
+ body: JSON.stringify({ reviewer_ids: reviewerIds })
597
+ });
598
+ if (!res.ok) {
599
+ const text = await res.text().catch(() => "");
600
+ throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
601
+ }
602
+ }
603
+ async fetchSingleMRWithRetry(projectPath, mrIid, errorMessage) {
604
+ for (let attempt = 0;attempt < 3; attempt++) {
605
+ if (attempt > 0) {
606
+ await new Promise((r) => setTimeout(r, attempt * 300));
607
+ }
608
+ const pr = await this.fetchSingleMR(projectPath, mrIid, null);
609
+ if (pr)
610
+ return pr;
611
+ }
612
+ throw new Error(errorMessage);
613
+ }
330
614
  async runQuery(query, variables) {
331
615
  const url = `${this.baseURL}/api/graphql`;
332
616
  const body = JSON.stringify({ query, variables: variables ?? {} });
@@ -442,6 +726,16 @@ class GitHubProvider {
442
726
  this.apiBase = `${this.baseURL}/api/v3`;
443
727
  }
444
728
  }
729
+ capabilities = {
730
+ canMerge: true,
731
+ canApprove: true,
732
+ canUnapprove: false,
733
+ canRebase: false,
734
+ canAutoMerge: false,
735
+ canResolveDiscussions: false,
736
+ canRetryPipeline: true,
737
+ canRequestReReview: true
738
+ };
445
739
  async validateToken() {
446
740
  const res = await this.api("GET", "/user");
447
741
  if (!res.ok) {
@@ -483,7 +777,9 @@ class GitHubProvider {
483
777
  ]);
484
778
  return this.toPullRequest(pr, prRoles, reviews, checkRuns);
485
779
  }));
486
- this.log.debug("GitHubProvider.fetchPullRequests", { count: results.length });
780
+ this.log.debug("GitHubProvider.fetchPullRequests", {
781
+ count: results.length
782
+ });
487
783
  return results;
488
784
  }
489
785
  async fetchSingleMR(projectPath, mrIid, _currentUserNumericId) {
@@ -554,9 +850,213 @@ class GitHubProvider {
554
850
  }
555
851
  return { mrIid, repositoryId, discussions };
556
852
  }
853
+ async fetchBranchProtectionRules(projectPath) {
854
+ const res = await this.api("GET", `/repos/${projectPath}/branches?protected=true&per_page=100`);
855
+ if (!res.ok) {
856
+ throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
857
+ }
858
+ const branches = await res.json();
859
+ const rules = [];
860
+ for (const b of branches) {
861
+ if (!b.protected)
862
+ continue;
863
+ const detailRes = await this.api("GET", `/repos/${projectPath}/branches/${encodeURIComponent(b.name)}/protection`);
864
+ if (!detailRes.ok) {
865
+ rules.push({
866
+ pattern: b.name,
867
+ allowForcePush: false,
868
+ allowDeletion: false,
869
+ requiredApprovals: 0,
870
+ requireStatusChecks: false
871
+ });
872
+ continue;
873
+ }
874
+ const detail = await detailRes.json();
875
+ rules.push({
876
+ pattern: b.name,
877
+ allowForcePush: detail.allow_force_pushes?.enabled ?? false,
878
+ allowDeletion: detail.allow_deletions?.enabled ?? false,
879
+ requiredApprovals: detail.required_pull_request_reviews?.required_approving_review_count ?? 0,
880
+ requireStatusChecks: detail.required_status_checks !== null && detail.required_status_checks !== undefined,
881
+ raw: detail
882
+ });
883
+ }
884
+ return rules;
885
+ }
886
+ async deleteBranch(projectPath, branch) {
887
+ const res = await this.api("DELETE", `/repos/${projectPath}/git/refs/heads/${encodeURIComponent(branch)}`);
888
+ if (!res.ok) {
889
+ throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
890
+ }
891
+ }
892
+ async fetchPullRequestByBranch(projectPath, sourceBranch) {
893
+ const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
894
+ if (!res.ok) {
895
+ this.log.warn("fetchPullRequestByBranch failed", {
896
+ projectPath,
897
+ sourceBranch,
898
+ status: res.status
899
+ });
900
+ return null;
901
+ }
902
+ const prs = await res.json();
903
+ if (!prs[0])
904
+ return null;
905
+ return this.fetchSingleMR(projectPath, prs[0].number, null);
906
+ }
907
+ async createPullRequest(input) {
908
+ const body = {
909
+ head: input.sourceBranch,
910
+ base: input.targetBranch,
911
+ title: input.title
912
+ };
913
+ if (input.description != null)
914
+ body.body = input.description;
915
+ if (input.draft != null)
916
+ body.draft = input.draft;
917
+ const res = await this.api("POST", `/repos/${input.projectPath}/pulls`, body);
918
+ if (!res.ok) {
919
+ const text = await res.text();
920
+ throw new Error(`createPullRequest failed: ${res.status} ${text}`);
921
+ }
922
+ const created = await res.json();
923
+ if (input.reviewers?.length) {
924
+ await this.api("POST", `/repos/${input.projectPath}/pulls/${created.number}/requested_reviewers`, {
925
+ reviewers: input.reviewers
926
+ });
927
+ }
928
+ if (input.assignees?.length) {
929
+ await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/assignees`, {
930
+ assignees: input.assignees
931
+ });
932
+ }
933
+ if (input.labels?.length) {
934
+ await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/labels`, {
935
+ labels: input.labels
936
+ });
937
+ }
938
+ const pr = await this.fetchSingleMR(input.projectPath, created.number, null);
939
+ if (!pr)
940
+ throw new Error("Created PR but failed to fetch it back");
941
+ return pr;
942
+ }
943
+ async updatePullRequest(projectPath, mrIid, input) {
944
+ const body = {};
945
+ if (input.title != null)
946
+ body.title = input.title;
947
+ if (input.description != null)
948
+ body.body = input.description;
949
+ if (input.draft != null)
950
+ body.draft = input.draft;
951
+ if (input.targetBranch != null)
952
+ body.base = input.targetBranch;
953
+ if (input.stateEvent)
954
+ body.state = input.stateEvent === "close" ? "closed" : "open";
955
+ const res = await this.api("PATCH", `/repos/${projectPath}/pulls/${mrIid}`, body);
956
+ if (!res.ok) {
957
+ const text = await res.text();
958
+ throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
959
+ }
960
+ if (input.reviewers) {
961
+ await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, {
962
+ reviewers: input.reviewers
963
+ });
964
+ }
965
+ if (input.assignees) {
966
+ await this.api("POST", `/repos/${projectPath}/issues/${mrIid}/assignees`, {
967
+ assignees: input.assignees
968
+ });
969
+ }
970
+ if (input.labels) {
971
+ await this.api("PUT", `/repos/${projectPath}/issues/${mrIid}/labels`, {
972
+ labels: input.labels
973
+ });
974
+ }
975
+ const pr = await this.fetchSingleMR(projectPath, mrIid, null);
976
+ if (!pr)
977
+ throw new Error("Updated PR but failed to fetch it back");
978
+ return pr;
979
+ }
557
980
  async restRequest(method, path, body) {
558
981
  return this.api(method, path, body);
559
982
  }
983
+ async mergePullRequest(projectPath, mrIid, input) {
984
+ const body = {};
985
+ if (input?.commitMessage != null)
986
+ body.commit_title = input.commitMessage;
987
+ if (input?.squashCommitMessage != null)
988
+ body.commit_title = input.squashCommitMessage;
989
+ if (input?.shouldRemoveSourceBranch != null)
990
+ body.delete_branch = input.shouldRemoveSourceBranch;
991
+ if (input?.sha != null)
992
+ body.sha = input.sha;
993
+ if (input?.mergeMethod) {
994
+ body.merge_method = input.mergeMethod;
995
+ } else if (input?.squash) {
996
+ body.merge_method = "squash";
997
+ }
998
+ const res = await this.api("PUT", `/repos/${projectPath}/pulls/${mrIid}/merge`, body);
999
+ if (!res.ok) {
1000
+ const text = await res.text();
1001
+ throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
1002
+ }
1003
+ const pr = await this.fetchSingleMR(projectPath, mrIid, null);
1004
+ if (!pr)
1005
+ throw new Error("Merged PR but failed to fetch it back");
1006
+ return pr;
1007
+ }
1008
+ async approvePullRequest(projectPath, mrIid) {
1009
+ const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/reviews`, {
1010
+ event: "APPROVE"
1011
+ });
1012
+ if (!res.ok) {
1013
+ const text = await res.text().catch(() => "");
1014
+ throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
1015
+ }
1016
+ }
1017
+ async unapprovePullRequest(_projectPath, _mrIid) {
1018
+ throw new Error("unapprovePullRequest is not supported by GitHub. " + "Check provider.capabilities.canUnapprove before calling.");
1019
+ }
1020
+ async rebasePullRequest(_projectPath, _mrIid) {
1021
+ throw new Error("rebasePullRequest is not supported by GitHub. " + "Check provider.capabilities.canRebase before calling.");
1022
+ }
1023
+ async setAutoMerge(_projectPath, _mrIid) {
1024
+ throw new Error("setAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
1025
+ }
1026
+ async cancelAutoMerge(_projectPath, _mrIid) {
1027
+ throw new Error("cancelAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
1028
+ }
1029
+ async resolveDiscussion(_projectPath, _mrIid, _discussionId) {
1030
+ throw new Error("resolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
1031
+ }
1032
+ async unresolveDiscussion(_projectPath, _mrIid, _discussionId) {
1033
+ throw new Error("unresolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
1034
+ }
1035
+ async retryPipeline(projectPath, pipelineId) {
1036
+ const res = await this.api("POST", `/repos/${projectPath}/actions/runs/${pipelineId}/rerun`);
1037
+ if (!res.ok) {
1038
+ const text = await res.text().catch(() => "");
1039
+ throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
1040
+ }
1041
+ }
1042
+ async requestReReview(projectPath, mrIid, reviewerUsernames) {
1043
+ if (!reviewerUsernames?.length) {
1044
+ const prRes = await this.api("GET", `/repos/${projectPath}/pulls/${mrIid}`);
1045
+ if (!prRes.ok) {
1046
+ throw new Error(`requestReReview: failed to fetch PR: ${prRes.status}`);
1047
+ }
1048
+ const pr = await prRes.json();
1049
+ reviewerUsernames = pr.requested_reviewers.map((r) => r.login);
1050
+ if (!reviewerUsernames.length) {
1051
+ return;
1052
+ }
1053
+ }
1054
+ const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, { reviewers: reviewerUsernames });
1055
+ if (!res.ok) {
1056
+ const text = await res.text().catch(() => "");
1057
+ throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
1058
+ }
1059
+ }
560
1060
  async api(method, path, body) {
561
1061
  const url = `${this.apiBase}${path}`;
562
1062
  const headers = {
package/dist/types.d.ts CHANGED
@@ -76,6 +76,109 @@ export interface PullRequest {
76
76
  */
77
77
  detailedMergeStatus: string | null;
78
78
  }
79
+ /** Input for creating a new merge request / pull request. */
80
+ export interface CreatePullRequestInput {
81
+ /** Project path (GitLab: "group/project") or owner/repo (GitHub: "owner/repo"). */
82
+ projectPath: string;
83
+ title: string;
84
+ description?: string;
85
+ sourceBranch: string;
86
+ targetBranch: string;
87
+ draft?: boolean;
88
+ /** Usernames to assign as reviewers. */
89
+ reviewers?: string[];
90
+ /** Usernames to assign. */
91
+ assignees?: string[];
92
+ /** Labels to apply (string names). */
93
+ labels?: string[];
94
+ }
95
+ /** Input for updating an existing merge request / pull request. */
96
+ export interface UpdatePullRequestInput {
97
+ title?: string;
98
+ description?: string;
99
+ /** Set draft status. */
100
+ draft?: boolean;
101
+ /** Change the target branch. */
102
+ targetBranch?: string;
103
+ /** Usernames to assign as reviewers (replaces current set). */
104
+ reviewers?: string[];
105
+ /** Usernames to assign (replaces current set). */
106
+ assignees?: string[];
107
+ /** Labels (replaces current set). */
108
+ labels?: string[];
109
+ /** Set MR state: "close" or "reopen". */
110
+ stateEvent?: 'close' | 'reopen';
111
+ }
112
+ /**
113
+ * Merge strategy override.
114
+ * - "merge" — Standard merge commit.
115
+ * - "squash" — Squash all commits into one before merging.
116
+ * - "rebase" — Rebase the source branch onto the target (fast-forward).
117
+ *
118
+ * When omitted, the provider uses the project's configured default merge method.
119
+ */
120
+ export type MergeMethod = 'merge' | 'squash' | 'rebase';
121
+ /**
122
+ * Input for merging (accepting) a pull request / merge request.
123
+ *
124
+ * All fields are **optional**. Omitting them defers to the project-level
125
+ * settings configured in the forge UI (merge method, squash policy,
126
+ * delete-source-branch, etc.). This matches how the web UI works.
127
+ */
128
+ export interface MergePullRequestInput {
129
+ /** Merge commit message. Omit to use the provider/project default. */
130
+ commitMessage?: string;
131
+ /** Squash commit message (when merge method is "squash"). */
132
+ squashCommitMessage?: string;
133
+ /** Whether to squash commits. Omit to use the MR / project default. */
134
+ squash?: boolean;
135
+ /** Merge strategy override. Omit to use the project's default merge method. */
136
+ mergeMethod?: MergeMethod;
137
+ /** Delete source branch after merge. Omit to use project default. */
138
+ shouldRemoveSourceBranch?: boolean;
139
+ /** SHA that HEAD must match for the merge to proceed (optimistic locking). */
140
+ sha?: string;
141
+ }
142
+ /**
143
+ * Reports which mutation operations a provider supports.
144
+ *
145
+ * Callers should check these flags before invoking vendor-specific methods
146
+ * so they can conditionally show/hide UI affordances without
147
+ * knowing which provider they're talking to.
148
+ */
149
+ export interface ProviderCapabilities {
150
+ /** Can merge / accept a pull request. */
151
+ canMerge: boolean;
152
+ /** Can approve a pull request. */
153
+ canApprove: boolean;
154
+ /** Can revoke an existing approval. */
155
+ canUnapprove: boolean;
156
+ /** Can rebase the source branch onto the target. */
157
+ canRebase: boolean;
158
+ /** Can enable automatic merge when pipeline succeeds. */
159
+ canAutoMerge: boolean;
160
+ /** Can resolve / unresolve discussion threads. */
161
+ canResolveDiscussions: boolean;
162
+ /** Can retry a pipeline. */
163
+ canRetryPipeline: boolean;
164
+ /** Can re-request review attention from reviewers. */
165
+ canRequestReReview: boolean;
166
+ }
167
+ /** Branch protection rule (provider-agnostic). */
168
+ export interface BranchProtectionRule {
169
+ /** Branch name or pattern, e.g. "main" or "release/*". */
170
+ pattern: string;
171
+ /** Whether force-pushes are allowed. */
172
+ allowForcePush: boolean;
173
+ /** Whether branch deletions are allowed. */
174
+ allowDeletion: boolean;
175
+ /** Number of required approving reviews (0 = none required). */
176
+ requiredApprovals: number;
177
+ /** Whether the branch requires status checks to pass before merge. */
178
+ requireStatusChecks: boolean;
179
+ /** Raw provider-specific data for fields not covered above. */
180
+ raw?: Record<string, unknown>;
181
+ }
79
182
  /** Snapshot payload sent when a client first connects. */
80
183
  export interface PullRequestsSnapshot {
81
184
  items: PullRequest[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge-glance/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "GitHub & GitLab API client — REST, GraphQL, and real-time ActionCable subscriptions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",