@reviewflow/sdk 0.0.0-stage → 6.22.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.
Files changed (50) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +86 -2
  3. package/build/files-node.mjs +70 -0
  4. package/build/git-node.mjs +1792 -0
  5. package/build/index-browser.es.js +3328 -0
  6. package/build/index-node.mjs +3328 -0
  7. package/package.json +135 -3
  8. package/src/api/pullRequest.ts +617 -0
  9. package/src/config/insertCheckoutEntry.ts +54 -0
  10. package/src/diff/diffPaths.ts +78 -0
  11. package/src/diff/impacts.ts +47 -0
  12. package/src/diff/readingOrder.ts +428 -0
  13. package/src/diff/themes.ts +105 -0
  14. package/src/diff/types.ts +141 -0
  15. package/src/files/editableFile.ts +163 -0
  16. package/src/files.ts +13 -0
  17. package/src/git/groupDiffPaths.ts +134 -0
  18. package/src/git/manifests.ts +96 -0
  19. package/src/git/moduleBindings.ts +571 -0
  20. package/src/git/pullRequestDiff.ts +159 -0
  21. package/src/git/readBranchDiff.ts +359 -0
  22. package/src/git/readImpacts.ts +797 -0
  23. package/src/git/readingOrder.ts +190 -0
  24. package/src/git/run.ts +213 -0
  25. package/src/git.ts +38 -0
  26. package/src/github/applySuggestions.ts +399 -0
  27. package/src/github/editPullRequest.ts +790 -0
  28. package/src/github/fetchCompareDiff.ts +21 -0
  29. package/src/github/imageProxies.ts +43 -0
  30. package/src/github/lineDiff.ts +145 -0
  31. package/src/github/prQuery.ts +298 -0
  32. package/src/github/pullRequestDetail.ts +1347 -0
  33. package/src/github/pullRequestHead.ts +65 -0
  34. package/src/github/pullRequestRevisions.ts +124 -0
  35. package/src/github/transport.ts +171 -0
  36. package/src/index.ts +35 -0
  37. package/src/reviewflow/board.ts +333 -0
  38. package/src/reviewflow/changedParts.ts +80 -0
  39. package/src/reviewflow/checksFromReviewflow.ts +75 -0
  40. package/src/reviewflow/client.ts +589 -0
  41. package/src/reviewflow/follow.ts +345 -0
  42. package/src/reviewflow/status.ts +17 -0
  43. package/src/reviewflow/stream.ts +199 -0
  44. package/src/types/entities.ts +8 -0
  45. package/src/types/ids.ts +1 -0
  46. package/src/types/reviewflow.ts +32 -0
  47. package/src/utils/pullRequestEdit.ts +108 -0
  48. package/src/utils/pullRequestPart.ts +12 -0
  49. package/src/utils/pullRequestReaction.ts +19 -0
  50. package/src/utils/pullRequestSuggestion.ts +148 -0
@@ -0,0 +1,617 @@
1
+ import type { PrReviewDecision, PrState } from "../types/entities.ts";
2
+ import type { IsoDateTime } from "../types/ids.ts";
3
+ import type { ReviewflowReviewState } from "../types/reviewflow.ts";
4
+
5
+ /**
6
+ * One pull request, named the way GitHub names it rather than by a local
7
+ * record: the screen opens colleagues' pull requests too.
8
+ */
9
+ export interface PullRequestRef {
10
+ /** `owner/repo` */
11
+ ownerRepo: string;
12
+ number: number;
13
+ }
14
+
15
+ export interface PullRequestPerson {
16
+ login: string;
17
+ avatarUrl: string | null;
18
+ }
19
+
20
+ export interface PullRequestLabel {
21
+ name: string;
22
+ /** six hex digits, no `#`, as GitHub stores it */
23
+ color: string;
24
+ description: string | null;
25
+ }
26
+
27
+ export interface PullRequestTeam {
28
+ slug: string;
29
+ name: string;
30
+ }
31
+
32
+ export type PullRequestRequestedReviewer =
33
+ | { kind: "user"; login: string; avatarUrl: string | null }
34
+ | (PullRequestTeam & { kind: "team" });
35
+
36
+ export type PullRequestReviewState =
37
+ | "approved"
38
+ | "changesRequested"
39
+ | "commented"
40
+ | "dismissed"
41
+ | "pending";
42
+
43
+ /** The latest review of each reviewer, as GitHub folds them. */
44
+ export interface PullRequestReview {
45
+ author: string;
46
+ state: PullRequestReviewState;
47
+ submittedAt: IsoDateTime | null;
48
+ }
49
+
50
+ export type PullRequestCheckOutcome = "failed" | "passed" | "pending";
51
+
52
+ export interface PullRequestCheck {
53
+ name: string;
54
+ outcome: PullRequestCheckOutcome;
55
+ /**
56
+ * A check run GitHub reports in progress. Always false for a status context:
57
+ * its `PENDING` is the same whether the job runs or waits on an approval.
58
+ */
59
+ running: boolean;
60
+ /** what the check says about itself: a status's description, a check run's title */
61
+ description: string | null;
62
+ url: string | null;
63
+ }
64
+
65
+ export type PullRequestFileChange =
66
+ | "added"
67
+ | "changed"
68
+ | "copied"
69
+ | "deleted"
70
+ | "modified"
71
+ | "renamed";
72
+
73
+ /** `dismissed`: marked viewed, then changed by a later push. */
74
+ export type PullRequestFileViewed = "dismissed" | "unviewed" | "viewed";
75
+
76
+ export interface PullRequestFile {
77
+ path: string;
78
+ additions: number;
79
+ deletions: number;
80
+ change: PullRequestFileChange;
81
+ viewed: PullRequestFileViewed;
82
+ }
83
+
84
+ export type PullRequestMergeable = "conflicting" | "mergeable" | "unknown";
85
+
86
+ /** GitHub's `ReactionContent`, camel-cased. */
87
+ export type PullRequestReactionContent =
88
+ | "confused"
89
+ | "eyes"
90
+ | "heart"
91
+ | "hooray"
92
+ | "laugh"
93
+ | "rocket"
94
+ | "thumbsDown"
95
+ | "thumbsUp";
96
+
97
+ export interface PullRequestReaction {
98
+ content: PullRequestReactionContent;
99
+ /** at least 1: a content nobody reacted with is left out */
100
+ count: number;
101
+ viewerHasReacted: boolean;
102
+ }
103
+
104
+ /** What can be reacted to: the description, a comment, a review. */
105
+ export interface PullRequestReactable {
106
+ /** GraphQL node id, what a reaction names */
107
+ id: string;
108
+ reactions: PullRequestReaction[];
109
+ viewerCanReact: boolean;
110
+ }
111
+
112
+ export interface PullRequestComment extends PullRequestReactable {
113
+ /** REST id, what an edit or a reply names */
114
+ databaseId: number;
115
+ author: PullRequestPerson;
116
+ /** GitHub-flavoured markdown, verbatim */
117
+ body: string;
118
+ createdAt: IsoDateTime;
119
+ /** null when never edited; the edits are `prRevisions`, read on a press */
120
+ lastEditedAt: IsoDateTime | null;
121
+ url: string;
122
+ viewerCanUpdate: boolean;
123
+ }
124
+
125
+ export type PullRequestDiffSide = "LEFT" | "RIGHT";
126
+
127
+ /** A thread on the code; `line`/`startLine` are in `side`'s file, as GitHub numbers them. */
128
+ export interface PullRequestThread {
129
+ kind: "thread";
130
+ /** GraphQL node id, what resolving names */
131
+ id: string;
132
+ path: string;
133
+ /** null when the line is gone from the diff (an outdated thread) */
134
+ line: number | null;
135
+ /** the first line of a multi-line comment; null for a single line */
136
+ startLine: number | null;
137
+ side: PullRequestDiffSide;
138
+ /** the side of `startLine`; null for a single line */
139
+ startSide: PullRequestDiffSide | null;
140
+ /** the line when the thread was opened, kept for an outdated thread */
141
+ originalLine: number | null;
142
+ originalStartLine: number | null;
143
+ resolved: boolean;
144
+ outdated: boolean;
145
+ viewerCanResolve: boolean;
146
+ viewerCanUnresolve: boolean;
147
+ viewerCanReply: boolean;
148
+ /** the diff lines above the comment, as GitHub kept them */
149
+ diffHunk: string;
150
+ comments: PullRequestComment[];
151
+ /** every comment of the thread, `comments` included */
152
+ commentsTotal: number;
153
+ createdAt: IsoDateTime;
154
+ }
155
+
156
+ /** What happened to the pull request, as GitHub's page lists it between comments. */
157
+ export type PullRequestEventAction =
158
+ | "assigned"
159
+ | "autoMergeDisabled"
160
+ | "autoMergeEnabled"
161
+ | "baseRefChanged"
162
+ | "closed"
163
+ | "committed"
164
+ | "convertedToDraft"
165
+ | "headRefForcePushed"
166
+ | "labeled"
167
+ | "merged"
168
+ | "readyForReview"
169
+ | "renamed"
170
+ | "reopened"
171
+ | "reviewDismissed"
172
+ | "reviewRequested"
173
+ | "reviewRequestRemoved"
174
+ | "unassigned"
175
+ | "unlabeled";
176
+
177
+ export interface PullRequestEvent {
178
+ kind: "event";
179
+ action: PullRequestEventAction;
180
+ /** null for a deleted account, or a push with no GitHub user */
181
+ actor: PullRequestPerson | null;
182
+ /**
183
+ * What the action names, ready to read: the reviewer or team asked, the
184
+ * assignee, the label, the commit's headline, the new title, the new base.
185
+ */
186
+ subject: string | null;
187
+ /** `labeled` and `unlabeled`: the label, for its colour */
188
+ label: PullRequestLabel | null;
189
+ /**
190
+ * The second thing some actions name: a commit's or a force-push's short
191
+ * sha, the previous title of `renamed`, the previous base of `baseRefChanged`.
192
+ */
193
+ detail: string | null;
194
+ createdAt: IsoDateTime;
195
+ }
196
+
197
+ export interface PullRequestReviewItem extends PullRequestReactable {
198
+ kind: "review";
199
+ databaseId: number;
200
+ author: PullRequestPerson;
201
+ state: PullRequestReviewState;
202
+ body: string;
203
+ createdAt: IsoDateTime;
204
+ lastEditedAt: IsoDateTime | null;
205
+ url: string;
206
+ viewerCanUpdate: boolean;
207
+ /** the head the review was made on */
208
+ commitOid: string | null;
209
+ }
210
+
211
+ /**
212
+ * The conversation as GitHub's page shows it, oldest first: comments, reviews
213
+ * that said something (a verdict or a body), threads on the code, and events.
214
+ */
215
+ export type PullRequestTimelineItem =
216
+ | PullRequestEvent
217
+ | PullRequestReviewItem
218
+ | PullRequestThread
219
+ | (PullRequestComment & { kind: "comment" });
220
+
221
+ export interface PullRequestCommit {
222
+ oid: string;
223
+ /** the first parent: with `oid`, the commit's own diff; null for a root commit */
224
+ parentOid: string | null;
225
+ /** more than one parent: its diff against `parentOid` carries what was merged in */
226
+ merge: boolean;
227
+ /** the message's first line */
228
+ headline: string;
229
+ /** the rest of the message, empty when there is none */
230
+ body: string;
231
+ /** the GitHub user, else the git author's name with no avatar */
232
+ author: PullRequestPerson | null;
233
+ committedAt: IsoDateTime;
234
+ }
235
+
236
+ /** GitHub's `mergeStateStatus`, lowercased: why a merge is or is not offered. */
237
+ export type PullRequestMergeState =
238
+ | "behind"
239
+ | "blocked"
240
+ | "clean"
241
+ | "dirty"
242
+ | "draft"
243
+ | "hasHooks"
244
+ | "unknown"
245
+ | "unstable";
246
+
247
+ export type PullRequestMergeMethod = "merge" | "rebase" | "squash";
248
+
249
+ /** A rebase replays the commits as they are: it writes no commit of its own. */
250
+ export type PullRequestCommittingMergeMethod = Exclude<
251
+ PullRequestMergeMethod,
252
+ "rebase"
253
+ >;
254
+
255
+ export interface PullRequestMergeCommitMessage {
256
+ title: string;
257
+ body: string;
258
+ }
259
+
260
+ /** `id` is also what the draft and viewed mutations name; the reactions are the description's. */
261
+ export interface PullRequestDetail
262
+ extends PullRequestRef, PullRequestReactable {
263
+ url: string;
264
+ title: string;
265
+ /** GitHub-flavoured markdown, verbatim */
266
+ body: string;
267
+ state: PrState;
268
+ author: PullRequestPerson | null;
269
+ headRef: string;
270
+ baseRef: string;
271
+ headSha: string;
272
+ /** the base branch's tip as GitHub holds it; with `headSha`, what the diff is */
273
+ baseSha: string;
274
+ createdAt: IsoDateTime;
275
+ updatedAt: IsoDateTime;
276
+ /** the description's last edit, null when never edited */
277
+ lastEditedAt: IsoDateTime | null;
278
+ additions: number;
279
+ deletions: number;
280
+ changedFiles: number;
281
+ mergeable: PullRequestMergeable;
282
+ mergeState: PullRequestMergeState;
283
+ /** what the repository allows, GitHub's default first */
284
+ mergeMethods: PullRequestMergeMethod[];
285
+ /** what GitHub prefills in its own merge box for the viewer, per method */
286
+ mergeCommitMessages: Record<
287
+ PullRequestCommittingMergeMethod,
288
+ PullRequestMergeCommitMessage
289
+ >;
290
+ /** title, body, labels, assignees, state: GitHub's own answer for the viewer */
291
+ viewerCanUpdate: boolean;
292
+ /** "Update branch" is offered: the base moved and the head can take it */
293
+ viewerCanUpdateBranch: boolean;
294
+ /** the viewer may commit a suggestion to the head branch */
295
+ viewerCanApplySuggestion: boolean;
296
+ reviewDecision: PrReviewDecision;
297
+ labels: PullRequestLabel[];
298
+ assignees: PullRequestPerson[];
299
+ requestedReviewers: PullRequestRequestedReviewer[];
300
+ reviews: PullRequestReview[];
301
+ commitsTotal: number;
302
+ }
303
+
304
+ /** One read of one part of a pull request. */
305
+ export interface PullRequestRead {
306
+ /** when the source answered: the age of what is shown */
307
+ fetchedAt: IsoDateTime;
308
+ /**
309
+ * when the app last knew it current: the answer itself, or later, reviewflow
310
+ * reporting nothing this part shows changed since
311
+ */
312
+ checkedAt: IsoDateTime;
313
+ /** `reviewflow`: read from reviewflow's copy (the checks), not from GitHub */
314
+ source: "github" | "reviewflow";
315
+ /** what GitHub refused next to a usable answer, verbatim */
316
+ errors: string[];
317
+ }
318
+
319
+ /** The header, the sidebar and the merge box: what every tab shows. */
320
+ /**
321
+ * Each image url a body wrote, to the camo url GitHub serves it from: see
322
+ * `imageProxiesOf`. Absent from a source that does not render GitHub's html.
323
+ */
324
+ export type PullRequestImageProxies = Record<string, string>;
325
+
326
+ export interface PullRequestDetailResponse extends PullRequestRead {
327
+ pullRequest: PullRequestDetail;
328
+ /** the `gh` login, whose turn the screen reads gestures for */
329
+ viewer: string;
330
+ /** the images of the description */
331
+ images?: PullRequestImageProxies;
332
+ }
333
+
334
+ export interface PullRequestChecksResponse extends PullRequestRead {
335
+ /** the head commit's checks, up to the first page */
336
+ checks: PullRequestCheck[];
337
+ /** every check of the head commit, `checks` included */
338
+ checksTotal: number;
339
+ }
340
+
341
+ export interface PullRequestTimelineResponse extends PullRequestRead {
342
+ timeline: PullRequestTimelineItem[];
343
+ /** the images of every comment and review of `timeline` */
344
+ images?: PullRequestImageProxies;
345
+ /** older items past the page read, left out of `timeline` (the newest are kept) */
346
+ timelineTruncated: boolean;
347
+ }
348
+
349
+ export interface PullRequestCommitsResponse extends PullRequestRead {
350
+ /** oldest first, up to the first page; `commitsTotal` is the total */
351
+ commits: PullRequestCommit[];
352
+ }
353
+
354
+ /** The viewer's newest submitted review, whatever its verdict. */
355
+ export interface PullRequestViewerLastReview {
356
+ /** the head it was submitted on: "changes since my last review" start there */
357
+ oid: string;
358
+ submittedAt: string | null;
359
+ }
360
+
361
+ export interface PullRequestFilesResponse extends PullRequestRead {
362
+ /** up to the first page; `changedFiles` is the total */
363
+ files: PullRequestFile[];
364
+ viewerLastReview: PullRequestViewerLastReview | null;
365
+ }
366
+
367
+ export interface PullRequestPartResponses {
368
+ detail: PullRequestDetailResponse;
369
+ checks: PullRequestChecksResponse;
370
+ timeline: PullRequestTimelineResponse;
371
+ commits: PullRequestCommitsResponse;
372
+ files: PullRequestFilesResponse;
373
+ }
374
+
375
+ export type PullRequestPart = keyof PullRequestPartResponses;
376
+
377
+ /**
378
+ * `GET /api/prs/detail?repo=<owner/repo>&number=<n>&part=<part>` — one part of
379
+ * a pull request, read from GitHub when the screen asks for it and never
380
+ * polled. Each is its own query so the screen waits for none it
381
+ * does not show: GitHub resolves a query's fields one after the other.
382
+ *
383
+ * The answer is kept as the pull request's last read, served by `prCached`.
384
+ */
385
+ export type PullRequestPartResponse<Part extends PullRequestPart> =
386
+ PullRequestPartResponses[Part];
387
+
388
+ /**
389
+ * `GET /api/prs/cached?repo=<owner/repo>&number=<n>` — the last read of each
390
+ * part, from the daemon's database, shown while GitHub is read again. Never
391
+ * served as current: `fetchedAt` says how old each part is.
392
+ */
393
+ export interface PullRequestCachedResponse {
394
+ parts: Partial<PullRequestPartResponses>;
395
+ }
396
+
397
+ /**
398
+ * A `pullRequestChanged` frame on `/api/events`: reviewflow reported a change
399
+ * of this pull request, and these parts of its screen are stale.
400
+ */
401
+ export interface PullRequestChange extends PullRequestRef {
402
+ parts: PullRequestPart[];
403
+ }
404
+
405
+ /**
406
+ * `POST /api/prs/cache/clear` — drops the last read of every pull request:
407
+ * the next opening of each waits for GitHub, as a first one does.
408
+ */
409
+ export interface ClearPullRequestCacheResponse {
410
+ /** pull requests whose last read was dropped */
411
+ cleared: number;
412
+ }
413
+
414
+ /**
415
+ * What reviewflow holds of a pull request, enough to head the screen while
416
+ * GitHub is read. Never a substitute for the detail: no body, no timeline.
417
+ */
418
+ export interface PullRequestSummary extends PullRequestRef {
419
+ title: string;
420
+ url: string;
421
+ state: PrState | null;
422
+ reviewState: ReviewflowReviewState | null;
423
+ author: string | null;
424
+ headRef: string | null;
425
+ baseRef: string | null;
426
+ /** null until reviewflow knows it; with `baseSha`, enough to read the diff early */
427
+ headSha: string | null;
428
+ baseSha: string | null;
429
+ }
430
+
431
+ /**
432
+ * `GET /api/prs/summary?repo=<owner/repo>&number=<n>` — reviewflow's copy, null
433
+ * without a token, for a pull request reviewflow never handled, or when it
434
+ * cannot be reached: the screen then waits for the detail, as it always did.
435
+ */
436
+ export interface PullRequestSummaryResponse {
437
+ summary: PullRequestSummary | null;
438
+ }
439
+
440
+ /**
441
+ * `GET /api/prs/candidates?repo=<owner/repo>&number=<n>` — what the editors
442
+ * offer, read apart from the detail so the screen does not wait for them: the
443
+ * repository's assignable users alone cost GitHub about half a second.
444
+ */
445
+ export interface PullRequestCandidatesResponse {
446
+ /** what the label editor offers */
447
+ repoLabels: PullRequestLabel[];
448
+ /**
449
+ * GitHub's suggested reviewers, in GitHub's order. Neither list holds the
450
+ * author — GitHub refuses a review request to the author.
451
+ */
452
+ suggestedReviewers: PullRequestPerson[];
453
+ /** the repository's assignable users not suggested, by login */
454
+ reviewerCandidates: PullRequestPerson[];
455
+ /**
456
+ * The organisation's teams the viewer can see, by name; empty for a
457
+ * repository a user owns.
458
+ */
459
+ teamCandidates: PullRequestTeam[];
460
+ /** the repository's assignable users, the author included */
461
+ assigneeCandidates: PullRequestPerson[];
462
+ /** what GitHub refused next to a usable answer, verbatim */
463
+ errors: string[];
464
+ }
465
+
466
+ export interface PullRequestTextLine {
467
+ kind: "added" | "context" | "removed";
468
+ text: string;
469
+ }
470
+
471
+ /** One version of a description, a comment or a review, as GitHub keeps it. */
472
+ export interface PullRequestRevision {
473
+ editedAt: IsoDateTime;
474
+ /** null for a deleted account */
475
+ editor: PullRequestPerson | null;
476
+ /** the text as first posted */
477
+ original: boolean;
478
+ /** the whole text of this version; null once its author deleted it from the history */
479
+ body: string | null;
480
+ deletedBy: string | null;
481
+ /**
482
+ * Against the next older revision listed with a text, every line kept; null
483
+ * when there is none (the original, or the oldest listed).
484
+ */
485
+ changes: PullRequestTextLine[] | null;
486
+ }
487
+
488
+ /**
489
+ * `GET /api/prs/revisions?repo=<owner/repo>&number=<n>&subject=<id>` — the
490
+ * edit history of a `PullRequestReactable`'s `id`, read from GitHub on a press
491
+ * and never kept: the timeline carries only `lastEditedAt`, since every
492
+ * version of every comment would weigh the query far more than it is opened.
493
+ */
494
+ export interface PullRequestRevisionsResponse {
495
+ /** newest first, up to the first page */
496
+ revisions: PullRequestRevision[];
497
+ /** every revision, the original included when GitHub kept it */
498
+ revisionsTotal: number;
499
+ }
500
+
501
+ /**
502
+ * One change per request, so a refusal names the change it refused. Labels and
503
+ * reviewers carry both directions because the editor submits a selection, and
504
+ * the difference is what GitHub is asked for.
505
+ */
506
+ export type PullRequestEdit =
507
+ | {
508
+ kind: "applySuggestions";
509
+ /** refused when the head moved since the screen read it */
510
+ expectedHeadSha: string;
511
+ /** one commit for all of them, as GitHub's batch */
512
+ suggestions: PullRequestSuggestionRef[];
513
+ headline: string;
514
+ /** the daemon appends a `Co-authored-by` trailer per suggester */
515
+ body: string;
516
+ }
517
+ | {
518
+ kind: "editComment";
519
+ target: "issueComment" | "review" | "reviewComment";
520
+ databaseId: number;
521
+ body: string;
522
+ }
523
+ | {
524
+ kind: "inlineComment";
525
+ commitId: string;
526
+ comment: PullRequestDraftComment;
527
+ }
528
+ | {
529
+ kind: "merge";
530
+ method: PullRequestMergeMethod;
531
+ /** refused by GitHub when the head moved since the screen read it */
532
+ expectedHeadSha: string;
533
+ /** absent: GitHub writes its own; never with a rebase */
534
+ commitMessage?: PullRequestMergeCommitMessage;
535
+ }
536
+ | {
537
+ kind: "reaction";
538
+ /** a `PullRequestReactable`'s `id` */
539
+ subjectId: string;
540
+ content: PullRequestReactionContent;
541
+ /** false removes the viewer's own reaction */
542
+ reacted: boolean;
543
+ }
544
+ | {
545
+ kind: "reply";
546
+ /** the `databaseId` of a comment of the thread */
547
+ inReplyTo: number;
548
+ body: string;
549
+ }
550
+ | {
551
+ kind: "review";
552
+ event: PullRequestReviewEvent;
553
+ body: string;
554
+ /** the head the review is about; GitHub refuses one that is not the PR's */
555
+ commitId: string;
556
+ comments: PullRequestDraftComment[];
557
+ }
558
+ | {
559
+ kind: "reviewers";
560
+ /** a login that already reviewed is asked again: GitHub's re-request */
561
+ add: string[];
562
+ remove: string[];
563
+ /** team slugs */
564
+ addTeams: string[];
565
+ removeTeams: string[];
566
+ }
567
+ | { kind: "assignees"; add: string[]; remove: string[] }
568
+ | { kind: "body"; body: string }
569
+ | { kind: "comment"; body: string }
570
+ | { kind: "draft"; draft: boolean }
571
+ | { kind: "labels"; add: string[]; remove: string[] }
572
+ | { kind: "resolveThread"; threadId: string; resolved: boolean }
573
+ | { kind: "state"; state: "closed" | "open" }
574
+ | { kind: "title"; title: string }
575
+ | { kind: "updateBranch"; expectedHeadSha: string }
576
+ | { kind: "viewed"; path: string; viewed: boolean };
577
+
578
+ export type PullRequestReviewEvent = "approve" | "comment" | "requestChanges";
579
+
580
+ /**
581
+ * A comment on lines of the diff, before it is sent: alone (`inlineComment`)
582
+ * or with a review. A suggestion is a ```` ```suggestion ```` block in `body`.
583
+ */
584
+ export interface PullRequestDraftComment {
585
+ path: string;
586
+ side: PullRequestDiffSide;
587
+ line: number;
588
+ /** the first line of a range, on `startSide`; null for one line */
589
+ startLine: number | null;
590
+ startSide: PullRequestDiffSide | null;
591
+ body: string;
592
+ }
593
+
594
+ /**
595
+ * `POST /api/prs/edit` — writes to GitHub under the operator's `gh` login, the
596
+ * one route that does. The answer carries no pull request: the screen reads it
597
+ * again, so what it shows is GitHub's state rather than the request echoed.
598
+ */
599
+ export interface EditPullRequestRequest extends PullRequestRef {
600
+ edit: PullRequestEdit;
601
+ }
602
+
603
+ /** A suggestion is committed from its comment, on its thread's lines. */
604
+ export interface PullRequestSuggestionRef {
605
+ threadId: string;
606
+ /** the comment's GraphQL node id */
607
+ commentId: string;
608
+ }
609
+
610
+ export interface EditPullRequestResponse {
611
+ edited: PullRequestEdit["kind"];
612
+ /**
613
+ * What failed past GitHub's write, which landed: resolving the threads of
614
+ * committed suggestions. Null otherwise.
615
+ */
616
+ followUpError: string | null;
617
+ }
@@ -0,0 +1,54 @@
1
+ export interface InsertedCheckoutEntry {
2
+ /** the config text with the entry in `repos` */
3
+ source: string;
4
+ /** where the path goes: between the quotes of the entry's value */
5
+ cursor: number;
6
+ }
7
+
8
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
9
+ typeof value === "object" && value !== null && !Array.isArray(value);
10
+
11
+ /**
12
+ * `"owner/repo": ""` added to `repos` of the app's config text, so an editor
13
+ * can open on it with the cursor where the checkout path is typed. Nothing is
14
+ * saved here. An entry already present (names are compared case-insensitively,
15
+ * as GitHub's are) is left as it is, with the cursor in its value. The text is
16
+ * rewritten as two-space json, other keys kept; a blank text becomes a new
17
+ * config. `null` when the text is not a json object whose `repos` is an object:
18
+ * the user's own text is not guessed at.
19
+ */
20
+ export const insertCheckoutEntry = (
21
+ source: string,
22
+ ownerRepo: string,
23
+ ): InsertedCheckoutEntry | null => {
24
+ let parsed: unknown;
25
+ if (source.trim() === "") {
26
+ parsed = {};
27
+ } else {
28
+ try {
29
+ parsed = JSON.parse(source);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ if (!isRecord(parsed)) return null;
35
+ const repos = parsed.repos ?? {};
36
+ if (!isRecord(repos)) return null;
37
+
38
+ const wanted = ownerRepo.toLowerCase();
39
+ const existing = Object.keys(repos).find(
40
+ (key) => key.toLowerCase() === wanted,
41
+ );
42
+ const key = existing ?? ownerRepo;
43
+ const next = existing === undefined ? { ...repos, [key]: "" } : repos;
44
+ const text = `${JSON.stringify({ ...parsed, repos: next }, null, 2)}\n`;
45
+
46
+ // the value follows the key on the same line; the same key may appear
47
+ // earlier under another top-level key
48
+ const marker = `${JSON.stringify(key)}: "`;
49
+ const reposAt = text.indexOf('\n "repos": ');
50
+ const at = text.indexOf(marker, reposAt);
51
+ if (at === -1) return null;
52
+ const value = at + marker.length;
53
+ return { source: text, cursor: value };
54
+ };