@osdlc/typed-gh 0.9.0 → 0.9.2

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/verbs.js CHANGED
@@ -1,28 +1,39 @@
1
1
  /**
2
- * The verb table — every `gh` invocation this library knows, as data.
2
+ * The verb table: every `gh` call this library knows, as data.
3
3
  *
4
- * Same shape and same reasoning as `@osdlc/typed-git`'s: a {@link GhVerb}
5
- * pairs the argv to run with the function that reads the result, and nothing
6
- * here spawns anything. That is what lets a synchronous client and an
7
- * asynchronous fan-out drive the same definitions without either duplicating
8
- * the other's parsing.
4
+ * It has the same shape as `@osdlc/typed-git`'s. A {@link GhVerb} pairs the
5
+ * argv to run with the function that reads the result, and nothing here
6
+ * starts a process. So the sync client, the async client and callers with
7
+ * their own runner all use the same definitions and the same parsing.
9
8
  *
10
9
  * `checked` marks a verb the runner throws for on a non-zero exit, with
11
- * stderr: every mutation, and `prDiff`, which is a read. Other queries are run
12
- * unchecked and `parse` folds a failure into `null`, which is the verb's
13
- * *soft* reading. The clients' plain door reads them through `settle` (see
14
- * `doors.ts`) instead: a failure throws, except where the verb declares
15
- * `absent` or is a `probe`.
10
+ * stderr. That is every mutation, and `prDiff`, which is a read. Other
11
+ * queries run unchecked, and `parse` turns a failure into `null`, which is
12
+ * what `soft` returns. The clients' methods that throw read them through
13
+ * `settle` (see `doors.ts`) instead. There a failure throws, except where the
14
+ * verb declares `absent` or is a `probe`.
16
15
  */
17
16
  import { stderrOrError } from '@osdlc/command-seam';
18
17
  import { parseLabels, parsePrSearchHit, parsePullRequest, parseRepository } from "./parse.js";
19
18
  import { isAbsent } from "./failure.js";
20
19
  import { isRepoSlug, parsePrUrl } from "./repo.js";
21
- /** Whether a failed run says GitHub has no such `thing`: read from the same text the classifier reads. */
20
+ /** Whether a failed run says GitHub has no such `thing`. Reads the same text the classifier reads. */
22
21
  function noSuch(thing) {
23
22
  return (r) => isAbsent(thing, `${r.stderr}\n${r.stdout}`);
24
23
  }
24
+ /**
25
+ * A bare host, with an optional port: letters, digits, dots and hyphens, that
26
+ * starts and ends with a letter or digit, then optionally `:` and digits
27
+ * (`github.com`, `ghe.acme.com`, `localhost:8080`). No scheme, path, spaces or
28
+ * leading `-`, so the value cannot be read by gh as a flag or a URL. It does
29
+ * not check that the host exists or that its labels are well formed, and it
30
+ * does not accept IPv6 literals.
31
+ */
25
32
  const HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::\d+)?$/;
33
+ /** The target's host, or `undefined` when it has none. `''` is "not set". */
34
+ function hostOf(target) {
35
+ return target?.hostname === '' ? undefined : target?.hostname;
36
+ }
26
37
  /**
27
38
  * Throw `RangeError` unless `target.repo` is `owner/name` and
28
39
  * `target.hostname` is a bare host. Run by every verb that uses a target, so
@@ -32,11 +43,12 @@ export function checkTarget(target) {
32
43
  if (target?.repo !== undefined && !isRepoSlug(target.repo)) {
33
44
  throw new RangeError(`repo must be owner/name, got ${JSON.stringify(target.repo)}`);
34
45
  }
35
- if (target?.hostname !== undefined && !HOSTNAME.test(target.hostname)) {
36
- throw new RangeError(`hostname must be a bare host, got ${JSON.stringify(target.hostname)}`);
46
+ const hostname = hostOf(target);
47
+ if (hostname !== undefined && !HOSTNAME.test(hostname)) {
48
+ throw new RangeError(`hostname must be a bare host, got ${JSON.stringify(hostname)}`);
37
49
  }
38
50
  }
39
- /** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs; `repo` overrides the target's. */
51
+ /** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs. `repo` overrides the target's. */
40
52
  function repoArgs(target, repo) {
41
53
  checkTarget(target);
42
54
  const slug = repo ?? target?.repo;
@@ -44,17 +56,20 @@ function repoArgs(target, repo) {
44
56
  return [];
45
57
  if (repo !== undefined)
46
58
  checkTarget({ repo });
47
- return ['--repo', target?.hostname === undefined ? slug : `${target.hostname}/${slug}`];
59
+ const hostname = hostOf(target);
60
+ return ['--repo', hostname === undefined ? slug : `${hostname}/${slug}`];
48
61
  }
49
62
  /** `--hostname <host>` for `api` and `auth` verbs. */
50
63
  function hostArgs(target) {
51
64
  checkTarget(target);
52
- return target?.hostname === undefined ? [] : ['--hostname', target.hostname];
65
+ const hostname = hostOf(target);
66
+ return hostname === undefined ? [] : ['--hostname', hostname];
53
67
  }
54
68
  /** `GH_HOST` for the verbs gh gives no host flag. */
55
69
  function hostEnv(target) {
56
70
  checkTarget(target);
57
- return target?.hostname === undefined ? undefined : { GH_HOST: target.hostname };
71
+ const hostname = hostOf(target);
72
+ return hostname === undefined ? undefined : { GH_HOST: hostname };
58
73
  }
59
74
  /**
60
75
  * `endpoint` with `{owner}` and `{repo}` filled from the target. With no
@@ -68,21 +83,21 @@ function fillRepo(endpoint, target) {
68
83
  return endpoint.replaceAll('{owner}', owner).replaceAll('{repo}', name);
69
84
  }
70
85
  /**
71
- * The cheapest useful set: enough to identify a PR and nothing more.
86
+ * The smallest useful set: enough to identify a PR and nothing more.
72
87
  *
73
- * Reach for this whenever the question is "which PR is this branch?".
74
- * `mergeable` and `mergeStateStatus` are absent on purpose — GitHub computes
75
- * them per PR on demand, so asking for them across a fan-out of branches
76
- * turns a cheap listing into a rate-limit incident.
88
+ * Use it when the question is "which PR is this branch?". `mergeable` and
89
+ * `mergeStateStatus` are left out on purpose. GitHub computes them per PR
90
+ * when asked, so asking for them across many branches can hit the rate
91
+ * limit.
77
92
  */
78
93
  export const PR_REF_FIELDS = ['number', 'state', 'headRefName', 'url'];
79
94
  /**
80
- * The light field set for triage: everything needed to decide what to do with
81
- * a PR without paying for its body, comments, reviews, or check rollup.
95
+ * The smaller field set for triage: enough to decide what to do with a PR,
96
+ * without its body, comments, reviews, or check rollup.
82
97
  *
83
- * `mergeable` and `mergeStateStatus` are the expensive pair — GitHub computes
84
- * them lazily and answers `UNKNOWN` until it has, so a cold query over many
85
- * PRs commonly needs a retry. `prViewSettled` on the clients does it.
98
+ * `mergeable` and `mergeStateStatus` are the costly pair. GitHub computes
99
+ * them only when asked and answers `UNKNOWN` until it has. So a first query
100
+ * over many PRs often needs a retry. `prViewSettled` on the clients does it.
86
101
  */
87
102
  export const PR_LIST_FIELDS = [
88
103
  'number',
@@ -103,7 +118,7 @@ export const PR_LIST_FIELDS = [
103
118
  'createdAt',
104
119
  'updatedAt',
105
120
  ];
106
- /** {@link PR_LIST_FIELDS} plus the heavy ones a single PR can afford. */
121
+ /** {@link PR_LIST_FIELDS} plus the costly fields, for a single PR. */
107
122
  export const PR_VIEW_FIELDS = [
108
123
  ...PR_LIST_FIELDS,
109
124
  'body',
@@ -125,8 +140,8 @@ function oneOrNull(r) {
125
140
  return parsePullRequest(data);
126
141
  }
127
142
  /**
128
- * Parse a `--json` payload that is an array. Null — not `[]` — when the call
129
- * failed: a caller must be able to tell "no PRs" from "couldn't ask".
143
+ * Parse a `--json` payload that is an array. Null, not `[]`, when the call
144
+ * failed. A caller must be able to tell "no PRs" from "couldn't ask".
130
145
  */
131
146
  function manyOrNull(r) {
132
147
  if (r.exitCode !== 0)
@@ -163,10 +178,9 @@ function limited(rows, limit) {
163
178
  /** A mutation's result carries no value. */
164
179
  function nothing() { }
165
180
  /**
166
- * Parse a `gh label list --json name,color,description` payload. Null — not
167
- * `[]` — when the call failed, same "no rows" vs. "couldn't ask" split as
168
- * {@link manyOrNull}: a repo with genuinely zero labels and a `gh` call that
169
- * never ran must not collapse onto the same value.
181
+ * Parse a `gh label list --json name,color,description` payload. Null, not
182
+ * `[]`, when the call failed, as in {@link manyOrNull}. A repo with no labels
183
+ * and a `gh` call that never ran must not give the same value.
170
184
  */
171
185
  function manyLabelsOrNull(r) {
172
186
  if (r.exitCode !== 0)
@@ -182,7 +196,7 @@ function manyLabelsOrNull(r) {
182
196
  return null;
183
197
  return parseLabels(data);
184
198
  }
185
- /** The `gh issue list`/`view --json` field set the `issues` surface needs. */
199
+ /** The `gh issue list`/`view --json` field set an {@link IssueRow} reads. */
186
200
  const ISSUE_FIELDS = 'number,title,body,labels,state';
187
201
  /** Parse one `gh issue list`/`view --json` row into an {@link IssueRow}. */
188
202
  function parseIssueRow(data) {
@@ -214,9 +228,8 @@ function oneIssueOrNull(r) {
214
228
  return parseIssueRow(data);
215
229
  }
216
230
  /**
217
- * Parse a `gh issue list --json` payload that is an array. Null — not `[]` —
218
- * on failure, the same "no rows" vs. "couldn't ask" split {@link manyOrNull}
219
- * makes for PRs.
231
+ * Parse a `gh issue list --json` payload that is an array. Null, not `[]`,
232
+ * on failure, as {@link manyOrNull} does for PRs.
220
233
  */
221
234
  function manyIssuesOrNull(r) {
222
235
  if (r.exitCode !== 0)
@@ -238,7 +251,7 @@ function manyIssuesOrNull(r) {
238
251
  }
239
252
  return rows;
240
253
  }
241
- /** `gh pr list …` — open PRs by default. `opts.repo` overrides the target's repo. */
254
+ /** `gh pr list …`: open PRs by default. `opts.repo` overrides the target's repo. */
242
255
  export function prList(opts = {}, target) {
243
256
  const limit = opts.limit ?? 100;
244
257
  checkLimit(limit);
@@ -251,8 +264,8 @@ export function prList(opts = {}, target) {
251
264
  argv.push('--search', opts.search);
252
265
  if (opts.author !== undefined)
253
266
  argv.push('--author', opts.author);
254
- // A row without `number` does not parse and is dropped, so always ask for it;
255
- // the exact-head filter below also reads `headRefName`.
267
+ // A row without `number` does not parse and is dropped, so always ask for it.
268
+ // The exact-head filter below also reads `headRefName`.
256
269
  const asked = withFields(opts.fields ?? PR_LIST_FIELDS, [
257
270
  'number',
258
271
  ...(opts.head === undefined ? [] : ['headRefName']),
@@ -286,7 +299,7 @@ function notAFlag(what, value) {
286
299
  }
287
300
  }
288
301
  /**
289
- * `gh pr view <number|branch|url> --json …` — one PR, with the heavy fields
302
+ * `gh pr view <number|branch|url> --json …`: one PR, with the full field set
290
303
  * by default.
291
304
  *
292
305
  * A branch is the PR whose head is that branch, as gh picks it. A URL names
@@ -318,15 +331,15 @@ export function prView(ref, fields = PR_VIEW_FIELDS, target) {
318
331
  /**
319
332
  * Open PRs whose head is exactly `branch`.
320
333
  *
321
- * The common shape behind "which PR is this branch?", bundling the cheap
322
- * field set with a small limit. More than one row comes back only when forks
334
+ * The usual way to ask "which PR is this branch?". It uses the smallest field
335
+ * set and a small limit. More than one row comes back only when forks
323
336
  * share the branch name.
324
337
  */
325
338
  export function openPrsForBranch(branch, limit = 5, target) {
326
339
  return prList({ head: branch, limit, fields: PR_REF_FIELDS }, target);
327
340
  }
328
341
  /**
329
- * `gh pr view <n> --json body -q .body` — just the PR body. Null when gh
342
+ * `gh pr view <n> --json body -q .body`: just the PR body. Null when gh
330
343
  * failed. gh's `-q` output ends with a newline that is not part of the body,
331
344
  * so every trailing `\r` and `\n` is trimmed (including any that were part of
332
345
  * the body, such as the `\r\n` of a body saved with CRLF line endings).
@@ -340,10 +353,10 @@ export function prBody(number, target) {
340
353
  };
341
354
  }
342
355
  /**
343
- * `gh api [-i] [-H If-None-Match: …] <endpoint>` — a GET against the REST API. `{owner}` and
356
+ * `gh api [-i] [-H If-None-Match: …] <endpoint>`: a GET against the REST API. `{owner}` and
344
357
  * `{repo}` placeholders in the endpoint are filled from the target's repo, or by gh from the
345
358
  * cwd's repo when the target names none.
346
- * Unchecked: failure is a value (`ok: false`), so a caller can tell a 304
359
+ * Unchecked. Failure is a value (`ok: false`), so a caller can tell a 304
347
360
  * from an outage.
348
361
  */
349
362
  export function api(endpoint, opts = {}, target) {
@@ -380,16 +393,16 @@ function flattenPages(body) {
380
393
  return body.flat();
381
394
  }
382
395
  /**
383
- * `gh api graphql -f query=… [--paginate --slurp]` — a GraphQL query.
396
+ * `gh api graphql -f query=… [--paginate --slurp]`: a GraphQL query.
384
397
  *
385
- * Returns each page's `data`, one entry per page (one without `paginate`),
386
- * or null when the call failed, the output did not parse, or any page
387
- * carries `errors` or lacks `data`: a partial answer is no answer. Read one
398
+ * Returns each page's `data`, one entry per page (one without `paginate`).
399
+ * Returns null when the call failed, the output did not parse, or any page
400
+ * carries `errors` or lacks `data`. A partial answer counts as a failure. Read one
388
401
  * connection across the pages with {@link nodesAt}.
389
402
  *
390
403
  * A string variable of exactly `{owner}` or `{repo}` is filled from the
391
404
  * target's repo, or by gh from the clone. A mutation document throws
392
- * `RangeError`: this is a read, and changes go through `raw`.
405
+ * `RangeError`. This is a read, and changes go through `raw`.
393
406
  *
394
407
  * @experimental
395
408
  */
@@ -458,8 +471,8 @@ export function graphql(query, variables = {}, opts = {}, target) {
458
471
  };
459
472
  }
460
473
  /**
461
- * The `nodes` of one connection, joined across {@link graphql} pages: walk
462
- * `path` into each page and take its `nodes` array. Null when `pages` is
474
+ * The `nodes` of one connection, joined across {@link graphql} pages. It
475
+ * follows `path` into each page and takes its `nodes` array. Null when `pages` is
463
476
  * null, or any page lacks the path or a `nodes` array, so a page that came
464
477
  * back short is never read as fewer rows.
465
478
  *
@@ -485,7 +498,7 @@ export function nodesAt(pages, path) {
485
498
  }
486
499
  return out;
487
500
  }
488
- /** One header block off the front of `text`: the block, and what follows its blank line. */
501
+ /** Split one header block off the front of `text`: the block, and what follows its blank line. */
489
502
  function takeHeaderBlock(text) {
490
503
  const crlf = text.indexOf('\r\n\r\n');
491
504
  const lf = text.indexOf('\n\n');
@@ -498,7 +511,7 @@ function takeHeaderBlock(text) {
498
511
  const STATUS_LINE = /^HTTP\/[\d.]+\s+\d{3}/i;
499
512
  /**
500
513
  * Split a `gh api -i` payload (status line + headers, blank line, body) and decode it.
501
- * A real `gh` prints one block. As a guard, if several are stacked, the last one is read:
514
+ * A real `gh` prints one block. If there are several, the last one is read:
502
515
  * its status and ETag, and the body that follows it.
503
516
  */
504
517
  function parseApi(r, withHeaders) {
@@ -542,7 +555,7 @@ function parseApi(r, withHeaders) {
542
555
  ...(r.error === undefined ? {} : { error: r.error }),
543
556
  };
544
557
  }
545
- /** `gh --version` — the install probe. True when `gh` runs. */
558
+ /** `gh --version`: checks that `gh` is installed. True when `gh` runs. */
546
559
  export function installed() {
547
560
  return {
548
561
  argv: ['--version'],
@@ -552,7 +565,7 @@ export function installed() {
552
565
  parse: (r) => r.exitCode === 0,
553
566
  };
554
567
  }
555
- /** `gh auth status [--hostname <host>]` — the credential probe, for one host when the target names it. */
568
+ /** `gh auth status [--hostname <host>]`: checks credentials, for one host when the target names it. */
556
569
  export function authStatus(target) {
557
570
  return {
558
571
  argv: ['auth', 'status', ...hostArgs(target)],
@@ -563,8 +576,8 @@ export function authStatus(target) {
563
576
  };
564
577
  }
565
578
  /**
566
- * `gh label list --json name,color,description` — every label on the repo,
567
- * up to `limit` (default 1000; `gh`'s own default is 30).
579
+ * `gh label list --json name,color,description`: every label on the repo,
580
+ * up to `limit` (default 1000. `gh`'s own default is 30).
568
581
  */
569
582
  export function labelList(opts = {}, target) {
570
583
  const limit = opts.limit ?? 1000;
@@ -584,7 +597,7 @@ export function labelList(opts = {}, target) {
584
597
  parse: (r) => limited(manyLabelsOrNull(r), limit),
585
598
  };
586
599
  }
587
- /** `gh issue list --json …` — open issues by default, optionally by label. */
600
+ /** `gh issue list --json …`: open issues, optionally by label. */
588
601
  export function issueList(opts = {}, target) {
589
602
  const limit = opts.limit ?? 200;
590
603
  checkLimit(limit);
@@ -594,7 +607,7 @@ export function issueList(opts = {}, target) {
594
607
  argv.push('--json', ISSUE_FIELDS, '--limit', String(limit + 1));
595
608
  return { argv, checked: false, readOnly: true, parse: (r) => limited(manyIssuesOrNull(r), limit) };
596
609
  }
597
- /** `gh issue view <n> --json …` — one issue, same field set as {@link issueList}. */
610
+ /** `gh issue view <n> --json …`: one issue, same field set as {@link issueList}. */
598
611
  export function issueView(number, target) {
599
612
  return {
600
613
  argv: ['issue', 'view', String(number), ...repoArgs(target), '--json', ISSUE_FIELDS],
@@ -643,8 +656,8 @@ function rowsOrNull(r, parse) {
643
656
  return data.map(parse).filter((row) => row !== null);
644
657
  }
645
658
  /**
646
- * `gh repo list <owner> --json …` — an owner's repositories. The host comes
647
- * from the target (`GH_HOST`); the target's repo is not used.
659
+ * `gh repo list <owner> --json …`: an owner's repositories. The host comes
660
+ * from the target (`GH_HOST`). The target's repo is not used.
648
661
  */
649
662
  export function repoList(owner, opts = {}, target) {
650
663
  notAFlag('repoList owner', owner);
@@ -668,7 +681,7 @@ export function repoList(owner, opts = {}, target) {
668
681
  };
669
682
  }
670
683
  /**
671
- * `gh repo view [<[host/]owner/name>] --json …` — one repository: `repo`,
684
+ * `gh repo view [<[host/]owner/name>] --json …`: one repository. That is `repo`,
672
685
  * else the target's repo, else the clone at the client's cwd.
673
686
  */
674
687
  export function repoView(repo, fields = REPO_FIELDS, target) {
@@ -699,9 +712,9 @@ export function repoView(repo, fields = REPO_FIELDS, target) {
699
712
  };
700
713
  }
701
714
  /**
702
- * `gh search prs [flags] -- <query>` — pull requests across repositories,
715
+ * `gh search prs [flags] -- <query>`: pull requests across repositories,
703
716
  * with GitHub's search syntax in `query` (it may start with `-`, as in
704
- * `-label:bug`). The host comes from the target (`GH_HOST`); the target's
717
+ * `-label:bug`). The host comes from the target (`GH_HOST`). The target's
705
718
  * repo is not used, so pass `opts.repo` or `opts.owner` to narrow.
706
719
  */
707
720
  export function searchPrs(query, opts = {}, target) {
@@ -755,7 +768,7 @@ export function createPr(opts, target) {
755
768
  },
756
769
  };
757
770
  }
758
- /** `gh pr edit <n>` — retitle or rewrite the body. */
771
+ /** `gh pr edit <n>`: change the title or the body. */
759
772
  export function editPr(number, opts, target) {
760
773
  const argv = ['pr', 'edit', String(number), ...repoArgs(target)];
761
774
  if (opts.title !== undefined)
@@ -764,18 +777,18 @@ export function editPr(number, opts, target) {
764
777
  argv.push('--body', opts.body);
765
778
  return { argv, checked: true, readOnly: false, parse: nothing };
766
779
  }
767
- /** `gh pr edit <n> --body` — rewrite just the body. */
780
+ /** `gh pr edit <n> --body`: rewrite just the body. */
768
781
  export function editPrBody(number, body, target) {
769
782
  return editPr(number, { body }, target);
770
783
  }
771
- /** `gh pr update-branch <n> [--rebase]` — bring the head up to date with its base. */
784
+ /** `gh pr update-branch <n> [--rebase]`: bring the head up to date with its base. */
772
785
  export function prUpdateBranch(number, style = 'merge', target) {
773
786
  const argv = ['pr', 'update-branch', String(number), ...repoArgs(target)];
774
787
  if (style === 'rebase')
775
788
  argv.push('--rebase');
776
789
  return { argv, checked: true, readOnly: false, parse: nothing };
777
790
  }
778
- /** `gh pr ready <n> [--undo]` — mark a draft PR ready for review, or (with `draft`) convert it back to a draft. */
791
+ /** `gh pr ready <n> [--undo]`: mark a draft PR ready for review, or (with `draft`) convert it back to a draft. */
779
792
  export function prSetDraft(number, draft, target) {
780
793
  const argv = ['pr', 'ready', String(number), ...repoArgs(target)];
781
794
  if (draft)
@@ -783,13 +796,13 @@ export function prSetDraft(number, draft, target) {
783
796
  return { argv, checked: true, readOnly: false, parse: nothing };
784
797
  }
785
798
  /**
786
- * `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<id> -f body=…` — rewrite one
799
+ * `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<id> -f body=…`: rewrite one
787
800
  * conversation comment.
788
801
  *
789
- * `commentId` is the comment's numeric REST id, as text: the `id` that
802
+ * `commentId` is the comment's numeric REST id, as text. That is the `id` that
790
803
  * `gh api repos/{owner}/{repo}/issues/<n>/comments` returns. It is not the
791
804
  * GraphQL node id (such as `IC_kw…`) that `gh pr view --json comments` gives
792
- * in `Comment.id`; the REST path answers 404 for a node id.
805
+ * in `Comment.id`. The REST path answers 404 for a node id.
793
806
  *
794
807
  * Throws `RangeError` when `commentId` is not all digits, so a node id fails
795
808
  * at once and no text can change which endpoint the PATCH reaches.
@@ -817,10 +830,10 @@ export function issueCommentEdit(commentId, body, target) {
817
830
  }
818
831
  /**
819
832
  * `gh pr merge <n> --<method> --match-head-commit <sha>`. The head sha is
820
- * required, not optional: GitHub refuses the merge when the PR head moved
821
- * since the caller judged it, so a push that lands between a gate and the
822
- * merge can never ride in unreviewed. The branch is left alone (no
823
- * `--delete-branch`); deleting it is the caller's job.
833
+ * required. GitHub refuses the merge when the PR head moved since the caller
834
+ * checked it. So a push that lands between the check and the merge cannot be
835
+ * merged without review. The branch is left alone (no `--delete-branch`).
836
+ * Deleting it is the caller's job.
824
837
  */
825
838
  export function prMerge(number, opts, target) {
826
839
  const argv = [
@@ -837,14 +850,13 @@ export function prMerge(number, opts, target) {
837
850
  const REVIEW_THREADS_QUERY = 'query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){reviewThreads(first:100){pageInfo{hasNextPage} nodes{isResolved isOutdated comments(last:1){nodes{author{login}}}}}}}}';
838
851
  /**
839
852
  * A PR's review threads over GraphQL (`gh pr view --json` has no thread
840
- * field). Null — never `[]` — when the call failed, the payload was not the
841
- * expected shape, or more than 100 threads exist: a caller deciding whether a
842
- * PR is clear to merge must read "could not tell" differently from "no
843
- * threads".
853
+ * field). Null, never `[]`, when the call failed, the payload was not the
854
+ * expected shape, or more than 100 threads exist. A caller deciding whether a
855
+ * PR is clear to merge must tell "could not tell" from "no threads".
844
856
  */
845
857
  export function prReviewThreads(number, target) {
846
858
  checkTarget(target);
847
- // `-F` lets gh fill the placeholders from the clone; real values go as plain strings.
859
+ // `-F` lets gh fill the placeholders from the clone. Real values go as plain strings.
848
860
  const [owner, name] = target?.repo?.split('/') ?? [];
849
861
  const repo = owner === undefined || name === undefined
850
862
  ? ['-F', 'owner={owner}', '-F', 'name={repo}']
@@ -890,7 +902,7 @@ export function prReviewThreads(number, target) {
890
902
  }
891
903
  /**
892
904
  * `gh label create <name> --color <hex> --description <text> --force`.
893
- * `--force` makes it safe to repeat: when the label already exists, its color
905
+ * `--force` makes it safe to repeat. When the label already exists, its color
894
906
  * and description are set to these instead of failing.
895
907
  */
896
908
  export function createLabel(opts, target) {
@@ -907,7 +919,7 @@ export function createLabel(opts, target) {
907
919
  ];
908
920
  return { argv, checked: true, readOnly: false, parse: nothing };
909
921
  }
910
- /** `gh label edit <name>` — recolor or redescribe; omits a flag not given. */
922
+ /** `gh label edit <name>`: change the color or description. A flag not given is left out. */
911
923
  export function editLabel(name, opts, target) {
912
924
  const argv = ['label', 'edit', name, ...repoArgs(target)];
913
925
  if (opts.color !== undefined)
@@ -917,21 +929,18 @@ export function editLabel(name, opts, target) {
917
929
  return { argv, checked: true, readOnly: false, parse: nothing };
918
930
  }
919
931
  /**
920
- * `gh issue edit <n> [--remove-label <current>] --add-label <new>` — swap
921
- * one label for another on an issue in ONE call, so the issue is never
922
- * briefly unlabeled or briefly holding both labels BETWEEN the remove and the
923
- * add — that atomicity is real and is this one call's own guarantee. It does
924
- * NOT cover the window before the call: `currentLabel` is whatever the caller
925
- * already read (perhaps minutes earlier, from a `gh issue list` snapshot),
926
- * never re-read here. If the issue's live label changed since then (a human,
927
- * or another process, relabeled it in between), `--remove-label
928
- * <currentLabel>` targets a label the issue no longer carries — `gh` no-ops
929
- * that removal silently rather than erroring — so the call still adds
930
- * `newLabel`, and the issue ends up holding BOTH the label it actually has
931
- * now and `newLabel`. A caller that cares about that race re-reads the live
932
- * label immediately before calling this. `currentLabel` is `null` when the
933
- * issue carries no prior label to remove — the `--remove-label` flag is then
934
- * omitted rather than passed empty.
932
+ * `gh issue edit <n> [--remove-label <current>] --add-label <new>`: swap one
933
+ * label for another on an issue in one call. The issue is never left with
934
+ * neither label or both labels between the remove and the add.
935
+ *
936
+ * That covers only the call itself. `currentLabel` is whatever the caller
937
+ * read earlier, perhaps from a `gh issue list` minutes before. It is not read
938
+ * again here. If someone relabeled the issue since then, `--remove-label
939
+ * <currentLabel>` names a label the issue no longer has. `gh` skips that
940
+ * removal with no error and still adds `newLabel`. The issue then has both
941
+ * its current label and `newLabel`. A caller that cares reads the label again
942
+ * just before calling this. Pass `null` for `currentLabel` when there is no
943
+ * label to remove. The `--remove-label` flag is then left out.
935
944
  */
936
945
  export function setIssueLabel(number, currentLabel, newLabel, target) {
937
946
  const argv = ['issue', 'edit', String(number), ...repoArgs(target)];
@@ -940,7 +949,7 @@ export function setIssueLabel(number, currentLabel, newLabel, target) {
940
949
  argv.push('--add-label', newLabel);
941
950
  return { argv, checked: true, readOnly: false, parse: nothing };
942
951
  }
943
- /** `gh pr diff <n>` — the PR's unified diff, as text. Checked: an empty diff is a value, a failed call is not. */
952
+ /** `gh pr diff <n>`: the PR's unified diff, as text. Checked, so an empty diff is a value and a failed call throws. */
944
953
  export function prDiff(number, target) {
945
954
  return {
946
955
  argv: ['pr', 'diff', String(number), ...repoArgs(target)],
@@ -949,7 +958,7 @@ export function prDiff(number, target) {
949
958
  parse: (r) => r.stdout,
950
959
  };
951
960
  }
952
- /** `gh pr comment <n> --body <body>` — post a conversation comment, returning the URL gh prints ('' if none). */
961
+ /** `gh pr comment <n> --body <body>`: post a conversation comment, returning the URL gh prints ('' if none). */
953
962
  export function prComment(number, body, target) {
954
963
  return {
955
964
  argv: ['pr', 'comment', String(number), ...repoArgs(target), '--body', body],
@@ -967,10 +976,10 @@ export function prComment(number, body, target) {
967
976
  }
968
977
  /**
969
978
  * Create a PR review with inline comments, returning the review's URL (empty
970
- * when GitHub's reply carried none). The event is always `COMMENT`: this
971
- * library does not offer a way to approve or request changes. Goes through
979
+ * when GitHub's reply carried none). The event is always `COMMENT`. This
980
+ * library does not offer a way to approve or request changes. It goes through
972
981
  * `gh api` with the payload on stdin, because `gh pr review` cannot attach
973
- * line-anchored comments; `{owner}`/`{repo}` come from the target's repo, or
982
+ * comments to lines. `{owner}`/`{repo}` come from the target's repo, or
974
983
  * are filled in by `gh` from the client's working directory.
975
984
  *
976
985
  * @experimental
@@ -1041,10 +1050,10 @@ export function branchPrFields(fields) {
1041
1050
  /**
1042
1051
  * True when GitHub has not finished computing this PR's mergeability.
1043
1052
  *
1044
- * The answer is not stable on a cold query: GitHub kicks off the merge
1045
- * computation when asked and answers `UNKNOWN` until it lands. A caller that
1046
- * treats the first `UNKNOWN` as final will mis-triage a PR that is merely
1047
- * slow, so re-ask a few times before believing it.
1053
+ * The first answer may not be final. GitHub starts the merge computation when
1054
+ * asked and answers `UNKNOWN` until it finishes. A caller that treats the
1055
+ * first `UNKNOWN` as final misjudges a PR whose computation is slow. So ask a
1056
+ * few times before trusting it.
1048
1057
  */
1049
1058
  export function needsMergeStateRetry(pr) {
1050
1059
  return pr.mergeable === 'UNKNOWN' || pr.mergeStateStatus === 'UNKNOWN';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osdlc/typed-gh",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "A typed GitHub client for TypeScript over the gh CLI. A total PullRequest/Review/CheckRun model, pure argv builders and parsers, and a tri-modal client — queries return parsed values, mutations throw, raw() is the escape hatch. Uses gh's own credentials; reads no token.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "sksizer",
@@ -22,8 +22,8 @@
22
22
  "LICENSE-APACHE"
23
23
  ],
24
24
  "dependencies": {
25
- "@osdlc/command-seam": "^0.9.1",
26
- "@osdlc/typed-git": "^0.7.0",
25
+ "@osdlc/command-seam": "^0.10.0",
26
+ "@osdlc/typed-git": "^0.7.1",
27
27
  "@osdlc/result": "^0.1.0"
28
28
  },
29
29
  "engines": {