@osdlc/typed-gh 0.9.6 → 0.11.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @osdlc/typed-gh
2
2
 
3
+ ## 0.11.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 272da7a: A list that goes through GitHub search reports `truncated: true` when 1000 rows come back. Search returns at most 1000 results whatever the `limit`, so before, a `limit` of 1000 or more read as complete (`truncated: false`) while more matched. gh uses search for `searchPrs`, for `prList` with a non-empty `search` or an `author`, and for `issueList` with a `label`. A list that does not search is unchanged. `searchPrs` now asks gh for at most 1000 rows: a `limit` of 1000 or more used to fail, because gh refuses a larger search limit.
8
+
9
+ ## 0.11.0
10
+
11
+ ### Minor Changes
12
+
13
+ - da5e6a3: `parseOwnerRepo` is now `@osdlc/typed-git`'s `parseRemoteRepo` without the host. Behaviour change: a remote that `parseGitUrl` recognises but names no owner and repository for is read host-agnostically (`host/owner/repo`) instead of answering null; every other remote reads as before.
14
+ `settle`, `runCheckedGh`, `runCheckedGhAsync`, `gh.try`, `gh.soft`, `classifyGhFailure` and the leading-dash guard now use `@osdlc/command-seam`'s `runFailed`, `tryMethods`, `methodFacade`, `classifyFailed` and `notAnOption`, with no change a caller sees: the same failures, and the same `RangeError` and message for an argument that starts with `-`. `repoView` and every `--repo` verb build `[host/]owner/name` through one helper.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [da5e6a3]
19
+ - Updated dependencies [da5e6a3]
20
+ - @osdlc/command-seam@0.14.0
21
+ - @osdlc/typed-git@0.10.0
22
+
23
+ ## 0.10.0
24
+
25
+ ### Minor Changes
26
+
27
+ - b835ae4: Behaviour change: output that the runner cut short (`outputIncomplete`, at the output cap or a timeout) now fails as `CommandFailed` for every verb but a probe, and in `runCheckedGh`/`runCheckedGhAsync`, instead of being parsed as whole. Behaviour change: every `AsyncGh` method that builds a verb now rejects its Promise on a bad argument (a verb's `RangeError`) instead of throwing at the call site.
28
+ `parseOwnerRepo` now reads a GitHub Enterprise remote (a host `parseGitUrl` does not know) through `parseRemoteUrl` instead of answering null. `graphql` refuses a document holding a mutation or subscription anywhere (after a fragment, a leading `,`, or a second operation), and a variable named `query` or `operationName`. `prView` passes a scheme-less PR URL (`github.com/o/r/pull/7`) to gh with `https://`, so gh no longer takes it for a branch name. `repoView` treats a hostname of `''` as not set. `createLabel` and `editLabel` refuse a name that starts with `-`. `openPrFor` takes `timeoutMs`.
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies [b835ae4]
33
+ - Updated dependencies [b835ae4]
34
+ - Updated dependencies [fdef74f]
35
+ - @osdlc/command-seam@0.13.0
36
+ - @osdlc/typed-git@0.9.0
37
+
3
38
  ## 0.9.6
4
39
 
5
40
  ### Patch Changes
package/README.md CHANGED
@@ -116,8 +116,9 @@ stderr and stdout, as the classifier does.
116
116
 
117
117
  Setup checks return their failure as a value. `installed()`, `authenticated()`,
118
118
  `authStatus()`, `ready()` and `api()` return their failure as part of their
119
- value, in all three call styles. For them, "gh is not installed" is the answer
120
- to the question they ask.
119
+ value, from the plain method and from `try`. They have no soft form:
120
+ `gh.soft.api(...)` throws a `TypeError`. For them, "gh is not installed" is the
121
+ answer to the question they ask.
121
122
 
122
123
  ```ts
123
124
  import { describe } from '@osdlc/result'
@@ -169,6 +170,13 @@ if (page.truncated) console.warn('more than 50 open PRs; raise the limit')
169
170
  `prList`, `issueList`, `labelList`, `repoList` and `searchPrs` all work this
170
171
  way. A `limit` that is not a positive integer throws `RangeError`.
171
172
 
173
+ GitHub search returns at most 1000 results, whatever the `limit`. gh uses
174
+ search for `searchPrs`, for `prList` with a non-empty `search` or an `author`,
175
+ and for `issueList` with a `label`. Those report `truncated: true` when 1000
176
+ rows come back: there may be more that search would not return. Narrow the
177
+ query to see them. `searchPrs` asks gh for at most 1000 rows, since gh refuses
178
+ a larger search limit.
179
+
172
180
  ## A repository without a clone
173
181
 
174
182
  By default a client talks to the repository of the clone at its directory.
@@ -375,7 +383,8 @@ const repos = await runVerbAsync(repoList('acme', {}, { hostname: 'ghe.example.c
375
383
  })
376
384
  ```
377
385
 
378
- They take a `CommandRunner`. A plain `(binary, argv, opts) => result` function is wrapped as
386
+ `runVerb` takes a `CommandRunner` and `runVerbAsync` an `AsyncCommandRunner`. A plain
387
+ `(binary, argv, opts) => result` function is not a runner; wrap it yourself as
379
388
  `{ run: (argv, opts) => fn('gh', argv, opts) }`.
380
389
 
381
390
  ## `AsyncGh`
@@ -440,7 +449,7 @@ const prs = nodesAt(pages, ['repository', 'pullRequests'])
440
449
  ```
441
450
 
442
451
  With `paginate`, the query must take `$endCursor` and page one connection. gh follows only that
443
- one. `graphql` refuses a mutation with `RangeError`. Send one through `raw`.
452
+ one. `graphql` refuses a document holding a mutation or subscription with `RangeError`. Send one through `raw`.
444
453
 
445
454
  ## Repositories and search
446
455
 
@@ -547,7 +556,9 @@ Each verb takes an optional `GhTarget` last. Each client method has a verb of th
547
556
  - `parsePullRequest`: reads a `gh --json` payload into a `PullRequest`, or `null`.
548
557
  - `anyCheckFailed`: true if any check in a list failed.
549
558
  - `normalizeAuthorLogin`: drops a `[bot]` suffix from a login.
550
- - `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`.
559
+ - `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`. It is
560
+ `@osdlc/typed-git`'s `parseRemoteRepo` without the host, so any host's remote reads, not only
561
+ GitHub's. Use `parseRemoteRepo` when you need the host too.
551
562
  - `ownerRepoSlug`: writes `{ owner, repo }` as `owner/repo`.
552
563
  - `parsePrUrl`: reads a pull request URL on any GitHub host into its host, owner, repository and
553
564
  number, or `null`.
package/SECURITY.md CHANGED
@@ -42,9 +42,10 @@ not intend are in scope.
42
42
  value into a REST path. Pass endpoints you built, not text from a user.
43
43
  `issueCommentEdit(commentId)` accepts only digits and throws `RangeError`
44
44
  for anything else, so its id cannot change the path.
45
- - **`gh` chooses the repository.** It reads the working directory's git
46
- remotes, or `GH_REPO`. A checkout whose `.git/config` points `origin` at
47
- another repository sends mutations there. `ownerRepo` only parses a URL you
45
+ - **Without a `repo` option, `gh` chooses the repository.** It reads the
46
+ working directory's git remotes, or `GH_REPO`. A checkout whose
47
+ `.git/config` points `origin` at another repository sends mutations there.
48
+ Pass `repo` (and `hostname`) to name the target in argv. `ownerRepo` only parses a URL you
48
49
  give it. It does not check where `gh` will act.
49
50
  - **`raw` and `rawChecked` run any `gh` command.** They have no allow list.
50
51
  - **`CommandFailed` carries the full argv.** Its message quotes the arguments,
package/dist/async.js CHANGED
@@ -79,12 +79,15 @@ export class AsyncGh {
79
79
  return (await this.exec(verb, call));
80
80
  }
81
81
  // ── queries ───────────────────────────────────────────────────────────────
82
+ //
83
+ // Every method that builds a verb is `async`, so a bad argument (a verb's
84
+ // RangeError) rejects the Promise instead of throwing at the call site.
82
85
  /** Pull requests matching `opts`. Rejects when gh fails. `soft.prList` resolves null instead. */
83
- prList(opts = {}) {
86
+ async prList(opts = {}) {
84
87
  return this.read(v.prList(opts, this.target), opts);
85
88
  }
86
89
  /** One PR by number, branch or URL, with the heavy fields, or null. See `Gh.prView`. */
87
- prView(ref, opts = {}) {
90
+ async prView(ref, opts = {}) {
88
91
  return this.exec(v.prView(ref, opts.fields ?? v.PR_VIEW_FIELDS, this.target), opts);
89
92
  }
90
93
  /** The one PR that `input` names, or null. See `Gh.prFind`. */
@@ -127,11 +130,11 @@ export class AsyncGh {
127
130
  return last;
128
131
  }
129
132
  /** A PR's body text. Rejects when gh fails. */
130
- prBody(number, opts = {}) {
133
+ async prBody(number, opts = {}) {
131
134
  return this.read(v.prBody(number, this.target), opts);
132
135
  }
133
136
  /** A REST GET through `gh api`. Failure is `ok: false`, never a rejection. */
134
- api(endpoint, opts = {}) {
137
+ async api(endpoint, opts = {}) {
135
138
  return this.exec(v.api(endpoint, opts, this.target), opts);
136
139
  }
137
140
  /**
@@ -139,41 +142,41 @@ export class AsyncGh {
139
142
  *
140
143
  * @experimental
141
144
  */
142
- graphql(query, variables = {}, opts = {}) {
145
+ async graphql(query, variables = {}, opts = {}) {
143
146
  return this.read(v.graphql(query, variables, opts, this.target), opts);
144
147
  }
145
148
  /** Every label on the repository (up to `opts.limit`, default 1000). Rejects when gh fails. */
146
- labelList(opts = {}) {
149
+ async labelList(opts = {}) {
147
150
  return this.read(v.labelList(opts, this.target), opts);
148
151
  }
149
152
  /** Issues matching `opts`. Rejects when gh fails. */
150
- issueList(opts = {}) {
153
+ async issueList(opts = {}) {
151
154
  return this.read(v.issueList(opts, this.target), opts);
152
155
  }
153
156
  /** One issue with its labels, or null when there is no such issue. */
154
- issueView(number, opts = {}) {
157
+ async issueView(number, opts = {}) {
155
158
  return this.exec(v.issueView(number, this.target), opts);
156
159
  }
157
160
  /**
158
161
  * An owner's repositories. Rejects when gh fails. Each row's `owner` is in
159
162
  * GitHub's casing, not as passed. See `Gh.repoList`.
160
163
  */
161
- repoList(owner, opts = {}) {
164
+ async repoList(owner, opts = {}) {
162
165
  return this.read(v.repoList(owner, opts, this.target), opts);
163
166
  }
164
167
  /**
165
168
  * One repository, or null when there is no such repository. `owner` and
166
169
  * `name` are in GitHub's casing, not as passed. See `Gh.repoView`.
167
170
  */
168
- repoView(repo, opts = {}) {
171
+ async repoView(repo, opts = {}) {
169
172
  return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts);
170
173
  }
171
174
  /** Pull requests across repositories matching a search query. Rejects when gh fails. See `Gh.searchPrs`. */
172
- searchPrs(query, opts = {}) {
175
+ async searchPrs(query, opts = {}) {
173
176
  return this.read(v.searchPrs(query, opts, this.target), opts);
174
177
  }
175
178
  /** True iff the `gh` binary is on PATH and runs. */
176
- installed() {
179
+ async installed() {
177
180
  return this.exec(v.installed());
178
181
  }
179
182
  /** True iff `gh` holds credentials. */
@@ -181,7 +184,7 @@ export class AsyncGh {
181
184
  return (await this.authStatus()).ok;
182
185
  }
183
186
  /** Login state and what `gh auth status` printed. */
184
- authStatus() {
187
+ async authStatus() {
185
188
  return this.exec(v.authStatus(this.target));
186
189
  }
187
190
  /** True iff `gh` is installed and authenticated. Skips the auth check when `gh` is not installed. */
@@ -197,23 +200,23 @@ export class AsyncGh {
197
200
  }
198
201
  // ── mutations (checked) ───────────────────────────────────────────────────
199
202
  /** Open a PR and resolve to its URL ('' if gh printed none). */
200
- createPr(opts) {
203
+ async createPr(opts) {
201
204
  return this.exec(v.createPr(opts, this.target));
202
205
  }
203
206
  /** Retitle or rewrite a PR body. */
204
- editPr(number, opts) {
207
+ async editPr(number, opts) {
205
208
  return this.exec(v.editPr(number, opts, this.target));
206
209
  }
207
210
  /** Rewrite just a PR's body. */
208
- editPrBody(number, body) {
211
+ async editPrBody(number, body) {
209
212
  return this.exec(v.editPrBody(number, body, this.target));
210
213
  }
211
214
  /** The PR's unified diff. Rejects with the classified failure when gh cannot produce it. */
212
- prDiff(number) {
215
+ async prDiff(number) {
213
216
  return this.exec(v.prDiff(number, this.target));
214
217
  }
215
218
  /** Post one conversation comment on a PR. Resolves to its URL ('' if gh printed none). */
216
- prComment(number, body) {
219
+ async prComment(number, body) {
217
220
  return this.exec(v.prComment(number, body, this.target));
218
221
  }
219
222
  /**
@@ -221,15 +224,15 @@ export class AsyncGh {
221
224
  *
222
225
  * @experimental
223
226
  */
224
- createReview(number, opts) {
227
+ async createReview(number, opts) {
225
228
  return this.exec(v.createReview(number, opts, this.target));
226
229
  }
227
230
  /** Bring a PR's head branch up to date with its base. */
228
- prUpdateBranch(number, style = 'merge') {
231
+ async prUpdateBranch(number, style = 'merge') {
229
232
  return this.exec(v.prUpdateBranch(number, style, this.target));
230
233
  }
231
234
  /** Mark a PR ready for review (`draft: false`) or convert it to a draft (`draft: true`). */
232
- prSetDraft(number, draft) {
235
+ async prSetDraft(number, draft) {
233
236
  return this.exec(v.prSetDraft(number, draft, this.target));
234
237
  }
235
238
  /**
@@ -238,27 +241,26 @@ export class AsyncGh {
238
241
  * @experimental
239
242
  */
240
243
  async issueCommentEdit(commentId, body) {
241
- // async, so a bad id rejects the Promise instead of throwing at the call site.
242
244
  return this.exec(v.issueCommentEdit(commentId, body, this.target));
243
245
  }
244
246
  /** Merge a PR. GitHub refuses when its head is no longer `opts.headSha`. */
245
- prMerge(number, opts) {
247
+ async prMerge(number, opts) {
246
248
  return this.exec(v.prMerge(number, opts, this.target));
247
249
  }
248
250
  /** A PR's review threads. Rejects when they could not be read completely. `soft.prReviewThreads` resolves null instead. */
249
- prReviewThreads(number) {
251
+ async prReviewThreads(number) {
250
252
  return this.read(v.prReviewThreads(number, this.target));
251
253
  }
252
254
  /** Create a label (or force it to `opts`'s color/description if it exists). */
253
- createLabel(opts) {
255
+ async createLabel(opts) {
254
256
  return this.exec(v.createLabel(opts, this.target));
255
257
  }
256
258
  /** Recolor and/or redescribe an existing label. */
257
- editLabel(name, opts) {
259
+ async editLabel(name, opts) {
258
260
  return this.exec(v.editLabel(name, opts, this.target));
259
261
  }
260
262
  /** Swap an issue's label in a single `gh issue edit` call. */
261
- setIssueLabel(number, currentLabel, newLabel) {
263
+ async setIssueLabel(number, currentLabel, newLabel) {
262
264
  return this.exec(v.setIssueLabel(number, currentLabel, newLabel, this.target));
263
265
  }
264
266
  }
package/dist/doors.d.ts CHANGED
@@ -27,12 +27,17 @@ import type { GhVerb } from './verbs.ts';
27
27
  * Read the result of running `verb` as the methods that throw do.
28
28
  *
29
29
  * A checked verb has already thrown on a non-zero exit (the runner is
30
- * `runChecked`). A probe (`installed`, `authStatus`, `api`) carries its
30
+ * {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`) carries its
31
31
  * failure in its value. A read that exited non-zero throws the classified
32
32
  * failure, unless the verb's `absent` says gh's answer was "there is no such
33
33
  * thing", which `parse` turns into `null`. A read that exited 0 but parsed to `null`
34
34
  * could not be read completely, and throws.
35
35
  *
36
+ * A result flagged `outputIncomplete` throws for every verb but a probe, as
37
+ * typed-git's `gate` does: gh exited, but the runner stopped reading at the
38
+ * output cap or a timeout, so a parse would take cut-short output as whole.
39
+ * Both read a result through command-seam's `runFailed`.
40
+ *
36
41
  * @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
37
42
  * @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
38
43
  */
@@ -43,9 +48,14 @@ export declare function settle<T>(verb: GhVerb<T>, result: CommandResult, binary
43
48
  * `CommandFailed` as its `cause`. Anything else is returned as it is.
44
49
  */
45
50
  export declare function ghFailureError(error: unknown): unknown;
46
- /** {@link runChecked}, throwing {@link ghFailureError}'s classified failure. */
51
+ /**
52
+ * Run `argv` and throw {@link ghFailureError}'s classified failure unless gh
53
+ * exited 0 with its whole output. Output cut short (`outputIncomplete`) is a
54
+ * failure too, which `runChecked` alone would not catch: it checks only the
55
+ * exit code.
56
+ */
47
57
  export declare function runCheckedGh(runner: CommandRunner, argv: string[], opts: CommandRunOptions): CommandResult;
48
- /** {@link runCheckedAsync}, rejecting with {@link ghFailureError}'s classified failure. */
58
+ /** {@link runCheckedGh} for an awaitable runner, rejecting with the classified failure. */
49
59
  export declare function runCheckedGhAsync(runner: AsyncCommandRunner, argv: string[], opts: AsyncCommandRunOptions): Promise<CommandResult>;
50
60
  /** The members `try` and `soft` leave out: `raw`, `rawChecked`, helpers that run nothing, and properties. */
51
61
  type Triable<C> = Omit<C, 'raw' | 'rawChecked' | 'at' | 'withRepo' | 'ownerRepo' | 'try' | 'soft' | 'cwd' | 'repo' | 'hostname'>;
@@ -73,7 +83,10 @@ export type Soft<C> = {
73
83
  export declare function failureOf(error: {
74
84
  failure: Failure;
75
85
  }): GhFailure;
76
- /** Wrap a synchronous client so every method returns a `Result<T, GhFailure>`. */
86
+ /**
87
+ * Wrap a synchronous client so every method returns a `Result<T, GhFailure>`.
88
+ * The proxy is command-seam's `tryMethods`, so a new method needs no entry.
89
+ */
77
90
  export declare function triedSync<C extends object>(client: C): TriedSync<C>;
78
91
  /** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
79
92
  export declare function triedAsync<C extends object>(client: C): TriedAsync<C>;
package/dist/doors.js CHANGED
@@ -19,30 +19,39 @@
19
19
  * `try` and `soft` are proxies over the whole client, so a new method needs no
20
20
  * change here.
21
21
  */
22
- import { CommandFailed, isCommandFailed, runChecked, runCheckedAsync, } from '@osdlc/command-seam';
23
- import { err, FailureError, isFailure, ok } from '@osdlc/result';
22
+ import { CommandFailed, isCommandFailed, methodFacade, runFailed, tryMethods, tryMethodsAsync, } from '@osdlc/command-seam';
23
+ import { FailureError, isFailure } from '@osdlc/result';
24
24
  import { classifyGhFailure, unreadableFailure } from "./failure.js";
25
25
  /**
26
26
  * Read the result of running `verb` as the methods that throw do.
27
27
  *
28
28
  * A checked verb has already thrown on a non-zero exit (the runner is
29
- * `runChecked`). A probe (`installed`, `authStatus`, `api`) carries its
29
+ * {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`) carries its
30
30
  * failure in its value. A read that exited non-zero throws the classified
31
31
  * failure, unless the verb's `absent` says gh's answer was "there is no such
32
32
  * thing", which `parse` turns into `null`. A read that exited 0 but parsed to `null`
33
33
  * could not be read completely, and throws.
34
34
  *
35
+ * A result flagged `outputIncomplete` throws for every verb but a probe, as
36
+ * typed-git's `gate` does: gh exited, but the runner stopped reading at the
37
+ * output cap or a timeout, so a parse would take cut-short output as whole.
38
+ * Both read a result through command-seam's `runFailed`.
39
+ *
35
40
  * @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
36
41
  * @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
37
42
  */
38
43
  export function settle(verb, result, binary) {
39
- if (verb.checked || verb.probe === true)
44
+ if (verb.probe === true)
40
45
  return verb.parse(result);
41
- if (result.neverExited === true || result.exitCode !== 0) {
42
- if (result.neverExited !== true && verb.absent?.(result) === true)
43
- return verb.parse(result);
46
+ // A checked verb's runner has already thrown on a non-zero exit.
47
+ const failed = verb.checked
48
+ ? result.outputIncomplete === true
49
+ : runFailed(result, (r) => verb.absent?.(r) === true);
50
+ if (failed)
44
51
  throw ghFailureError(new CommandFailed(verb.argv, result, binary));
45
- }
52
+ // A non-zero exit that got this far is gh's "there is no such thing".
53
+ if (verb.checked || result.exitCode !== 0)
54
+ return verb.parse(result);
46
55
  const value = verb.parse(result);
47
56
  if (value === null) {
48
57
  throw new FailureError(unreadableFailure(verb.argv, result.stdout, binary));
@@ -57,24 +66,35 @@ export function settle(verb, result, binary) {
57
66
  export function ghFailureError(error) {
58
67
  return isCommandFailed(error) ? new FailureError(classifyGhFailure(error)) : error;
59
68
  }
60
- /** {@link runChecked}, throwing {@link ghFailureError}'s classified failure. */
69
+ /**
70
+ * Run `argv` and throw {@link ghFailureError}'s classified failure unless gh
71
+ * exited 0 with its whole output. Output cut short (`outputIncomplete`) is a
72
+ * failure too, which `runChecked` alone would not catch: it checks only the
73
+ * exit code.
74
+ */
61
75
  export function runCheckedGh(runner, argv, opts) {
62
76
  try {
63
- return runChecked(runner, argv, opts);
77
+ return whole(runner.run(argv, opts), argv, runner.binary);
64
78
  }
65
79
  catch (error) {
66
80
  throw ghFailureError(error);
67
81
  }
68
82
  }
69
- /** {@link runCheckedAsync}, rejecting with {@link ghFailureError}'s classified failure. */
83
+ /** {@link runCheckedGh} for an awaitable runner, rejecting with the classified failure. */
70
84
  export async function runCheckedGhAsync(runner, argv, opts) {
71
85
  try {
72
- return await runCheckedAsync(runner, argv, opts);
86
+ return whole(await runner.run(argv, opts), argv, runner.binary);
73
87
  }
74
88
  catch (error) {
75
89
  throw ghFailureError(error);
76
90
  }
77
91
  }
92
+ /** `result`, or a `CommandFailed` thrown when it is a failure (command-seam's `runFailed`). */
93
+ function whole(result, argv, binary) {
94
+ if (runFailed(result))
95
+ throw new CommandFailed(argv, result, binary);
96
+ return result;
97
+ }
78
98
  /**
79
99
  * The queries that have a soft form. A mutation has none. If a write's failure
80
100
  * were dropped, the caller would believe the write worked.
@@ -110,45 +130,20 @@ export function failureOf(error) {
110
130
  const thrown = error;
111
131
  return isCommandFailed(thrown) ? classifyGhFailure(thrown) : error.failure;
112
132
  }
113
- function facade(client, wrap) {
114
- return new Proxy(client, {
115
- get(target, prop) {
116
- // Read from the target, not the proxy. A getter (`try` itself) must run
117
- // against the real client, not recurse through the proxy.
118
- const value = Reflect.get(target, prop);
119
- if (typeof value !== 'function')
120
- return value;
121
- const method = value;
122
- const name = String(prop);
123
- return (...args) => wrap(() => method.apply(target, args), name);
124
- },
125
- });
133
+ /** The failure a thrown value carries, or `null` for a bug to rethrow. */
134
+ function recognize(error) {
135
+ return isGhFailure(error) ? failureOf(error) : null;
126
136
  }
127
- /** Wrap a synchronous client so every method returns a `Result<T, GhFailure>`. */
137
+ /**
138
+ * Wrap a synchronous client so every method returns a `Result<T, GhFailure>`.
139
+ * The proxy is command-seam's `tryMethods`, so a new method needs no entry.
140
+ */
128
141
  export function triedSync(client) {
129
- return facade(client, (call) => {
130
- try {
131
- return ok(call());
132
- }
133
- catch (error) {
134
- if (isGhFailure(error))
135
- return err(failureOf(error));
136
- throw error;
137
- }
138
- });
142
+ return tryMethods(client, recognize);
139
143
  }
140
144
  /** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
141
145
  export function triedAsync(client) {
142
- return facade(client, async (call) => {
143
- try {
144
- return ok(await call());
145
- }
146
- catch (error) {
147
- if (isGhFailure(error))
148
- return err(failureOf(error));
149
- throw error;
150
- }
151
- });
146
+ return tryMethodsAsync(client, recognize);
152
147
  }
153
148
  function checkSoft(name) {
154
149
  if (!SOFT_METHODS.includes(name)) {
@@ -157,7 +152,7 @@ function checkSoft(name) {
157
152
  }
158
153
  /** Wrap a synchronous client so a failed query returns `null`. */
159
154
  export function softSync(client) {
160
- return facade(client, (call, name) => {
155
+ return methodFacade(client, (call, name) => {
161
156
  checkSoft(name);
162
157
  try {
163
158
  return call();
@@ -171,7 +166,7 @@ export function softSync(client) {
171
166
  }
172
167
  /** Wrap an awaitable client so a failed query resolves to `null`. */
173
168
  export function softAsync(client) {
174
- return facade(client, async (call, name) => {
169
+ return methodFacade(client, async (call, name) => {
175
170
  checkSoft(name);
176
171
  try {
177
172
  return await call();
package/dist/failure.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * in English. A message that matches none stays the subprocess layer's `exit`
8
8
  * with `gh`'s first stderr line, never a wrong kind.
9
9
  */
10
- import { classifyFailure } from '@osdlc/command-seam';
10
+ import { classifyFailed } from '@osdlc/command-seam';
11
11
  import {} from '@osdlc/result';
12
12
  const INSTALL_HINT = 'Install it from https://cli.github.com, then run `gh auth login`.';
13
13
  const RATE_LIMITED = /API rate limit exceeded|secondary rate limit|rate limit exceeded|HTTP 429/i;
@@ -37,13 +37,8 @@ export function isAbsent(thing, text) {
37
37
  export function classifyGhFailure(failed) {
38
38
  // The client's runner may not name its binary. It is gh either way, so the
39
39
  // messages say `gh` rather than "The command".
40
- const f = failed.binary === undefined
41
- ? (classifyFailure(failed.result, { binary: 'gh', argv: failed.argv }) ?? failed.failure)
42
- : failed.failure;
43
- const base = { ...f, cause: failed };
44
- if (f.kind === 'not-installed')
45
- return { ...base, hint: INSTALL_HINT };
46
- if (f.kind !== 'exit')
40
+ const base = classifyFailed(failed, { binary: 'gh', installHint: INSTALL_HINT });
41
+ if (base.kind !== 'exit')
47
42
  return base;
48
43
  const text = `${failed.result.stderr}\n${failed.result.stdout}`;
49
44
  if (RATE_LIMITED.test(text)) {
package/dist/repo.d.ts CHANGED
@@ -2,15 +2,20 @@
2
2
  * Reading the GitHub owner and repository from a git remote, and a pull
3
3
  * request from a URL or what a person typed.
4
4
  *
5
- * `parseOwnerRepo` uses `@osdlc/typed-git`'s `parseGitUrl`. It lives here so
6
- * callers do not each write a regex over `remote.origin.url`. Such a regex
7
- * usually misses a URL form: scp-style, a trailing `.git`, an `ssh://` port,
8
- * or a bare `owner/repo`.
5
+ * `parseOwnerRepo` is `@osdlc/typed-git`'s `parseRemoteRepo` without the
6
+ * host, so it reads every remote form typed-git reads (scp-style, a trailing
7
+ * `.git`, an `ssh://` port, a bare `owner/repo`, an Enterprise host) and no
8
+ * caller writes its own regex over `remote.origin.url`.
9
9
  */
10
10
  import type { OwnerRepo } from './types.ts';
11
11
  /**
12
12
  * `{owner, repo}` from any git remote reference, or null when it names no
13
13
  * project. Handles https, ssh, scp-style, and bare `owner/repo`.
14
+ *
15
+ * It is typed-git's `parseRemoteRepo` without the host: a clone URL on a host
16
+ * `parseGitUrl` does not know, such as a GitHub Enterprise host or another
17
+ * forge's `host/owner/repo`, is read host-agnostically. It does not check
18
+ * that the host is GitHub; read `parseRemoteRepo(url).host` for that.
14
19
  */
15
20
  export declare function parseOwnerRepo(remoteUrl: string): OwnerRepo | null;
16
21
  /** `owner/repo`, the form `gh api` paths and `--repo` both take. */
package/dist/repo.js CHANGED
@@ -2,24 +2,24 @@
2
2
  * Reading the GitHub owner and repository from a git remote, and a pull
3
3
  * request from a URL or what a person typed.
4
4
  *
5
- * `parseOwnerRepo` uses `@osdlc/typed-git`'s `parseGitUrl`. It lives here so
6
- * callers do not each write a regex over `remote.origin.url`. Such a regex
7
- * usually misses a URL form: scp-style, a trailing `.git`, an `ssh://` port,
8
- * or a bare `owner/repo`.
5
+ * `parseOwnerRepo` is `@osdlc/typed-git`'s `parseRemoteRepo` without the
6
+ * host, so it reads every remote form typed-git reads (scp-style, a trailing
7
+ * `.git`, an `ssh://` port, a bare `owner/repo`, an Enterprise host) and no
8
+ * caller writes its own regex over `remote.origin.url`.
9
9
  */
10
- import { parseGitUrl } from '@osdlc/typed-git';
10
+ import { parseRemoteRepo } from '@osdlc/typed-git';
11
11
  /**
12
12
  * `{owner, repo}` from any git remote reference, or null when it names no
13
13
  * project. Handles https, ssh, scp-style, and bare `owner/repo`.
14
+ *
15
+ * It is typed-git's `parseRemoteRepo` without the host: a clone URL on a host
16
+ * `parseGitUrl` does not know, such as a GitHub Enterprise host or another
17
+ * forge's `host/owner/repo`, is read host-agnostically. It does not check
18
+ * that the host is GitHub; read `parseRemoteRepo(url).host` for that.
14
19
  */
15
20
  export function parseOwnerRepo(remoteUrl) {
16
- const info = parseGitUrl(remoteUrl);
17
- if (info === null)
18
- return null;
19
- const { user, project } = info;
20
- if (!user || !project)
21
- return null;
22
- return { owner: user, repo: project };
21
+ const remote = parseRemoteRepo(remoteUrl);
22
+ return remote === null ? null : { owner: remote.owner, repo: remote.repo };
23
23
  }
24
24
  /** `owner/repo`, the form `gh api` paths and `--repo` both take. */
25
25
  export function ownerRepoSlug(r) {
package/dist/sync.d.ts CHANGED
@@ -130,6 +130,7 @@ export declare class Gh {
130
130
  openPrFor(branch: string, opts?: {
131
131
  limit?: number | undefined;
132
132
  fields?: readonly string[] | undefined;
133
+ timeoutMs?: number | undefined;
133
134
  }): PullRequest | null;
134
135
  /**
135
136
  * {@link prView}, asked again until GitHub has computed the merge state.
package/dist/sync.js CHANGED
@@ -154,6 +154,7 @@ export class Gh {
154
154
  head: branch,
155
155
  limit: opts.limit ?? 5,
156
156
  fields: opts.fields ?? v.PR_REF_FIELDS,
157
+ timeoutMs: opts.timeoutMs,
157
158
  });
158
159
  return v.pickPrForBranch(rows.rows, branch);
159
160
  }
package/dist/verbs.d.ts CHANGED
@@ -35,9 +35,9 @@ export interface GhVerb<T> {
35
35
  env?: Record<string, string> | undefined;
36
36
  /**
37
37
  * True for a read whose failure is part of its answer. `installed`,
38
- * `authStatus` and `api` return what `parse` makes of any result, in all
39
- * three call styles. Other reads throw from the plain method when gh exits
40
- * non-zero.
38
+ * `authStatus` and `api` return what `parse` makes of any result, from the
39
+ * plain method and from `try`; they have no soft form. Other reads throw
40
+ * from the plain method when gh exits non-zero.
41
41
  */
42
42
  probe?: boolean | undefined;
43
43
  /**
@@ -94,7 +94,10 @@ export declare const PR_VIEW_FIELDS: readonly ["number", "state", "title", "url"
94
94
  /**
95
95
  * A list query's answer: the rows, and whether `limit` cut the list short.
96
96
  * The verb asks gh for one row more than `limit`. `truncated` is true when
97
- * that row came back, and `rows` holds at most `limit`.
97
+ * that row came back, and `rows` holds at most `limit`. A query that goes
98
+ * through GitHub search, which returns at most 1000 results, is also
99
+ * `truncated` when 1000 rows came back: `prList` with a non-empty `search` or
100
+ * an `author`, `issueList` with a `label`, and `searchPrs`.
98
101
  */
99
102
  export interface ListResult<T> {
100
103
  rows: T[];
@@ -223,8 +226,10 @@ export interface GraphqlOptions {
223
226
  * connection across the pages with {@link nodesAt}.
224
227
  *
225
228
  * A string variable of exactly `{owner}` or `{repo}` is filled from the
226
- * target's repo, or by gh from the clone. A mutation document throws
227
- * `RangeError`. This is a read, and changes go through `raw`.
229
+ * target's repo, or by gh from the clone. A document holding any mutation or
230
+ * subscription throws `RangeError`, as does a variable named `query` or
231
+ * `operationName`, which gh would send as the request's own fields. This is a
232
+ * read, and changes go through `raw`.
228
233
  *
229
234
  * @experimental
230
235
  */
package/dist/verbs.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * `settle` (see `doors.ts`) instead. There a failure throws, except where the
14
14
  * verb declares `absent` or is a `probe`.
15
15
  */
16
- import { stderrOrError } from '@osdlc/command-seam';
16
+ import { notAnOption, stderrOrError } from '@osdlc/command-seam';
17
17
  import { parseLabels, parsePrSearchHit, parsePullRequest, parseRepository } from "./parse.js";
18
18
  import { isAbsent } from "./failure.js";
19
19
  import { isRepoSlug, parsePrUrl } from "./repo.js";
@@ -48,16 +48,26 @@ export function checkTarget(target) {
48
48
  throw new RangeError(`hostname must be a bare host, got ${JSON.stringify(hostname)}`);
49
49
  }
50
50
  }
51
- /** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs. `repo` overrides the target's. */
52
- function repoArgs(target, repo) {
51
+ /**
52
+ * `[host/]owner/name` for `repo` (else the target's repo), the form `--repo`
53
+ * and `gh repo view` take, or `undefined` when neither names one. The host is
54
+ * the target's, through {@link hostOf}, so a hostname of `''` is "not set".
55
+ * Both are checked first.
56
+ */
57
+ function qualifiedRepo(target, repo) {
53
58
  checkTarget(target);
54
- const slug = repo ?? target?.repo;
55
- if (slug === undefined)
56
- return [];
57
59
  if (repo !== undefined)
58
60
  checkTarget({ repo });
61
+ const slug = repo ?? target?.repo;
62
+ if (slug === undefined)
63
+ return undefined;
59
64
  const hostname = hostOf(target);
60
- return ['--repo', hostname === undefined ? slug : `${hostname}/${slug}`];
65
+ return hostname === undefined ? slug : `${hostname}/${slug}`;
66
+ }
67
+ /** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs. `repo` overrides the target's. */
68
+ function repoArgs(target, repo) {
69
+ const qualified = qualifiedRepo(target, repo);
70
+ return qualified === undefined ? [] : ['--repo', qualified];
61
71
  }
62
72
  /** `--hostname <host>` for `api` and `auth` verbs. */
63
73
  function hostArgs(target) {
@@ -144,24 +154,7 @@ function oneOrNull(r) {
144
154
  * failed. A caller must be able to tell "no PRs" from "couldn't ask".
145
155
  */
146
156
  function manyOrNull(r) {
147
- if (r.exitCode !== 0)
148
- return null;
149
- let data;
150
- try {
151
- data = JSON.parse(r.stdout || '[]');
152
- }
153
- catch {
154
- return null;
155
- }
156
- if (!Array.isArray(data))
157
- return null;
158
- const rows = [];
159
- for (const item of data) {
160
- const pr = parsePullRequest(item);
161
- if (pr !== null)
162
- rows.push(pr);
163
- }
164
- return rows;
157
+ return rowsOrNull(r, (item) => parsePullRequest(item));
165
158
  }
166
159
  /** Throw `RangeError` unless `limit` is a positive integer. */
167
160
  function checkLimit(limit) {
@@ -169,11 +162,23 @@ function checkLimit(limit) {
169
162
  throw new RangeError(`limit must be a positive integer, got ${limit}`);
170
163
  }
171
164
  }
172
- /** `rows` cut to `limit`, with whether there were more. Null stays null. */
173
- function limited(rows, limit) {
165
+ /**
166
+ * The most results GitHub search returns for one query. A call that goes
167
+ * through search and gets this many rows may have been cut off by search,
168
+ * whatever the `limit`. gh uses search for `pr list` with a non-empty
169
+ * `--search` or with `--author`, for `issue list` with `--label`, and for
170
+ * `search prs`.
171
+ */
172
+ const SEARCH_RESULT_CAP = 1000;
173
+ /**
174
+ * `rows` cut to `limit`, with whether there were more. With `cap`, reaching
175
+ * it counts as more too, since the source stops there. Null stays null.
176
+ */
177
+ function limited(rows, limit, cap) {
174
178
  if (rows === null)
175
179
  return null;
176
- return { rows: rows.slice(0, limit), truncated: rows.length > limit };
180
+ const truncated = rows.length > limit || (cap !== undefined && rows.length >= cap);
181
+ return { rows: rows.slice(0, limit), truncated };
177
182
  }
178
183
  /** A mutation's result carries no value. */
179
184
  function nothing() { }
@@ -232,24 +237,7 @@ function oneIssueOrNull(r) {
232
237
  * on failure, as {@link manyOrNull} does for PRs.
233
238
  */
234
239
  function manyIssuesOrNull(r) {
235
- if (r.exitCode !== 0)
236
- return null;
237
- let data;
238
- try {
239
- data = JSON.parse(r.stdout || '[]');
240
- }
241
- catch {
242
- return null;
243
- }
244
- if (!Array.isArray(data))
245
- return null;
246
- const rows = [];
247
- for (const item of data) {
248
- const row = parseIssueRow(item);
249
- if (row !== null)
250
- rows.push(row);
251
- }
252
- return rows;
240
+ return rowsOrNull(r, parseIssueRow);
253
241
  }
254
242
  /** `gh pr list …`: open PRs by default. `opts.repo` overrides the target's repo. */
255
243
  export function prList(opts = {}, target) {
@@ -278,7 +266,8 @@ export function prList(opts = {}, target) {
278
266
  checked: false,
279
267
  readOnly: true,
280
268
  parse(r) {
281
- const result = limited(manyOrNull(r), limit);
269
+ // A search stops at 1000 results, so 1000 rows may be a cut, not the end.
270
+ const result = limited(manyOrNull(r), limit, (opts.search ?? '') !== '' || opts.author !== undefined ? SEARCH_RESULT_CAP : undefined);
282
271
  if (result === null || head === undefined)
283
272
  return result;
284
273
  return { ...result, rows: result.rows.filter((pr) => pr.headRefName === head) };
@@ -294,9 +283,8 @@ function withFields(fields, required) {
294
283
  * argument would read as a flag.
295
284
  */
296
285
  function notAFlag(what, value) {
297
- if (value.startsWith('-')) {
298
- throw new RangeError(`${what} must not start with "-", got ${JSON.stringify(value)}`);
299
- }
286
+ const message = (v) => `${what} must not start with "-", got ${JSON.stringify(v)}`;
287
+ notAnOption(value, (v) => new RangeError(message(v)));
300
288
  }
301
289
  /**
302
290
  * `gh pr view <number|branch|url> --json …`: one PR, with the full field set
@@ -313,15 +301,14 @@ export function prView(ref, fields = PR_VIEW_FIELDS, target) {
313
301
  // omits it would read as "no such PR". Always ask for it, and each field once.
314
302
  const asked = withFields(fields, ['number']);
315
303
  const isUrl = typeof ref === 'string' && parsePrUrl(ref) !== null;
304
+ // gh reads a selector as a URL only with an http(s) scheme; a scheme-less
305
+ // one (`github.com/o/r/pull/7`, which parsePrUrl accepts) would be taken as
306
+ // a branch name.
307
+ const selector = typeof ref === 'string' && isUrl && !/^https?:\/\//i.test(ref.trim())
308
+ ? `https://${ref.trim()}`
309
+ : String(ref);
316
310
  return {
317
- argv: [
318
- 'pr',
319
- 'view',
320
- String(ref),
321
- ...(isUrl ? [] : repoArgs(target)),
322
- '--json',
323
- asked.join(','),
324
- ],
311
+ argv: ['pr', 'view', selector, ...(isUrl ? [] : repoArgs(target)), '--json', asked.join(',')],
325
312
  checked: false,
326
313
  readOnly: true,
327
314
  absent: noSuch('pr'),
@@ -392,6 +379,27 @@ function flattenPages(body) {
392
379
  return body;
393
380
  return body.flat();
394
381
  }
382
+ /**
383
+ * True when any operation in a GraphQL document is a `mutation` or a
384
+ * `subscription`. Strings and comments are dropped first, and only the words
385
+ * outside every `{…}` and `(…)` are read, which is where an operation's
386
+ * keyword stands. So a leading fragment, a leading `,` (an ignored token) or a
387
+ * second operation after a query cannot hide one.
388
+ */
389
+ function writesOrStreams(document) {
390
+ const code = document.replace(/"""[\s\S]*?"""|"(?:[^"\\\n]|\\.)*"|#[^\n]*/g, ' ');
391
+ let depth = 0;
392
+ for (const token of code.matchAll(/[{}()]|[_A-Za-z][_0-9A-Za-z]*/g)) {
393
+ const t = token[0];
394
+ if (t === '{' || t === '(')
395
+ depth += 1;
396
+ else if (t === '}' || t === ')')
397
+ depth = Math.max(0, depth - 1);
398
+ else if (depth === 0 && (t === 'mutation' || t === 'subscription'))
399
+ return true;
400
+ }
401
+ return false;
402
+ }
395
403
  /**
396
404
  * `gh api graphql -f query=… [--paginate --slurp]`: a GraphQL query.
397
405
  *
@@ -401,15 +409,16 @@ function flattenPages(body) {
401
409
  * connection across the pages with {@link nodesAt}.
402
410
  *
403
411
  * A string variable of exactly `{owner}` or `{repo}` is filled from the
404
- * target's repo, or by gh from the clone. A mutation document throws
405
- * `RangeError`. This is a read, and changes go through `raw`.
412
+ * target's repo, or by gh from the clone. A document holding any mutation or
413
+ * subscription throws `RangeError`, as does a variable named `query` or
414
+ * `operationName`, which gh would send as the request's own fields. This is a
415
+ * read, and changes go through `raw`.
406
416
  *
407
417
  * @experimental
408
418
  */
409
419
  export function graphql(query, variables = {}, opts = {}, target) {
410
420
  checkTarget(target);
411
- const body = query.replace(/^(?:\s|#[^\n]*\n?)*/, '');
412
- if (/^mutation\b/.test(body) || /^subscription\b/.test(body)) {
421
+ if (writesOrStreams(query)) {
413
422
  throw new RangeError('graphql: only queries are allowed; send a mutation through raw');
414
423
  }
415
424
  const paginate = opts.paginate === true;
@@ -421,7 +430,10 @@ export function graphql(query, variables = {}, opts = {}, target) {
421
430
  argv.push('--paginate', '--slurp');
422
431
  const [owner, name] = target?.repo?.split('/') ?? [];
423
432
  for (const [key, value] of Object.entries(variables)) {
424
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
433
+ // gh lifts `query` and `operationName` out of the variables into the
434
+ // request itself, so either would replace the checked document or pick
435
+ // another operation in it.
436
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key === 'query' || key === 'operationName') {
425
437
  throw new RangeError(`graphql: bad variable name ${JSON.stringify(key)}`);
426
438
  }
427
439
  if (typeof value === 'string') {
@@ -605,7 +617,14 @@ export function issueList(opts = {}, target) {
605
617
  if (opts.label !== undefined)
606
618
  argv.push('--label', opts.label);
607
619
  argv.push('--json', ISSUE_FIELDS, '--limit', String(limit + 1));
608
- return { argv, checked: false, readOnly: true, parse: (r) => limited(manyIssuesOrNull(r), limit) };
620
+ // gh lists issues by label through search, which stops at 1000 results.
621
+ const cap = opts.label === undefined ? undefined : SEARCH_RESULT_CAP;
622
+ return {
623
+ argv,
624
+ checked: false,
625
+ readOnly: true,
626
+ parse: (r) => limited(manyIssuesOrNull(r), limit, cap),
627
+ };
609
628
  }
610
629
  /** `gh issue view <n> --json …`: one issue, same field set as {@link issueList}. */
611
630
  export function issueView(number, target) {
@@ -688,14 +707,11 @@ export function repoList(owner, opts = {}, target) {
688
707
  * `owner` and `name` are in GitHub's casing, not as passed.
689
708
  */
690
709
  export function repoView(repo, fields = REPO_FIELDS, target) {
691
- checkTarget(target);
692
- if (repo !== undefined)
693
- checkTarget({ repo });
694
- const slug = repo ?? target?.repo;
710
+ // The same `[host/]owner/name` rule as `--repo`.
711
+ const qualified = qualifiedRepo(target, repo);
695
712
  const argv = ['repo', 'view'];
696
- if (slug !== undefined) {
697
- argv.push(target?.hostname === undefined ? slug : `${target.hostname}/${slug}`);
698
- }
713
+ if (qualified !== undefined)
714
+ argv.push(qualified);
699
715
  argv.push('--json', withFields(fields, ['nameWithOwner']).join(','));
700
716
  return {
701
717
  argv,
@@ -737,7 +753,9 @@ export function searchPrs(query, opts = {}, target) {
737
753
  else if (opts.state !== undefined)
738
754
  argv.push('--state', opts.state);
739
755
  argv.push('--json', withFields(opts.fields ?? SEARCH_PR_FIELDS, ['number']).join(','));
740
- argv.push('--limit', String(limit + 1));
756
+ // gh refuses a search limit over 1000, so ask for at most that. 1000 rows
757
+ // then read as truncated: search may hold more.
758
+ argv.push('--limit', String(Math.min(limit + 1, SEARCH_RESULT_CAP)));
741
759
  if (query !== '')
742
760
  argv.push('--', query);
743
761
  return {
@@ -745,7 +763,7 @@ export function searchPrs(query, opts = {}, target) {
745
763
  checked: false,
746
764
  readOnly: true,
747
765
  env: hostEnv(target),
748
- parse: (r) => limited(rowsOrNull(r, parsePrSearchHit), limit),
766
+ parse: (r) => limited(rowsOrNull(r, parsePrSearchHit), limit, SEARCH_RESULT_CAP),
749
767
  };
750
768
  }
751
769
  /**
@@ -909,6 +927,7 @@ export function prReviewThreads(number, target) {
909
927
  * and description are set to these instead of failing.
910
928
  */
911
929
  export function createLabel(opts, target) {
930
+ notAFlag('createLabel name', opts.name);
912
931
  const argv = [
913
932
  'label',
914
933
  'create',
@@ -924,6 +943,7 @@ export function createLabel(opts, target) {
924
943
  }
925
944
  /** `gh label edit <name>`: change the color or description. A flag not given is left out. */
926
945
  export function editLabel(name, opts, target) {
946
+ notAFlag('editLabel name', name);
927
947
  const argv = ['label', 'edit', name, ...repoArgs(target)];
928
948
  if (opts.color !== undefined)
929
949
  argv.push('--color', opts.color);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osdlc/typed-gh",
3
- "version": "0.9.6",
3
+ "version": "0.11.1",
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,9 +22,9 @@
22
22
  "LICENSE-APACHE"
23
23
  ],
24
24
  "dependencies": {
25
- "@osdlc/command-seam": "^0.12.1",
26
- "@osdlc/result": "^0.1.0",
27
- "@osdlc/typed-git": "^0.8.2"
25
+ "@osdlc/command-seam": "^0.14.0",
26
+ "@osdlc/typed-git": "^0.10.0",
27
+ "@osdlc/result": "^0.1.0"
28
28
  },
29
29
  "engines": {
30
30
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"