@osdlc/typed-gh 0.6.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/run.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `runVerb` and `runVerbAsync` — run one {@link GhVerb} through a runner the
3
+ * caller owns, with everything the verb asks for applied.
4
+ *
5
+ * A verb carries more than `argv`: `input` (a payload on stdin), `env` (the
6
+ * target host as `GH_HOST`), and `checked` (whether a non-zero exit throws).
7
+ * `verb.parse(await runner.run(verb.argv))` drops the first two without a
8
+ * word, so a query goes to github.com and a review is never sent. These two
9
+ * apply all of it, as `Gh` and `AsyncGh` do; both clients call them.
10
+ */
11
+ import type { AsyncCommandRunner, CommandRunner } from '@osdlc/command-seam';
12
+ import type { GhVerb } from './verbs.ts';
13
+ /** What a verb run is given besides the verb: the directory and the cap on the call. */
14
+ export interface RunVerbOptions {
15
+ cwd: string;
16
+ /** Hard cap on the call, as in command-seam. */
17
+ timeoutMs?: number | undefined;
18
+ }
19
+ /** {@link RunVerbOptions} for {@link runVerbAsync}, with the abort signal. */
20
+ export interface RunVerbAsyncOptions extends RunVerbOptions {
21
+ /** Aborts the call; the runner stops the child. */
22
+ signal?: AbortSignal | undefined;
23
+ }
24
+ /**
25
+ * Run `verb` through `runner` and return its parsed value, honouring the
26
+ * verb's `input`, `env`, `timeoutMs` and `checked`, and its checked or
27
+ * unchecked contract: a failed `gh` throws a `FailureError` carrying the
28
+ * classified `GhFailure`, output that cannot be read throws one of kind
29
+ * `unreadable`, and a query for one thing answers `null` when GitHub says
30
+ * there is no such thing.
31
+ *
32
+ * A plain `(binary, argv, opts) => result` function is not a runner: wrap it
33
+ * as `{ run: (argv, opts) => fn('gh', argv, opts) }`.
34
+ */
35
+ export declare function runVerb<T>(verb: GhVerb<T>, runner: CommandRunner, opts: RunVerbOptions): T;
36
+ /** {@link runVerb} for an `AsyncCommandRunner`; rejects where that throws. */
37
+ export declare function runVerbAsync<T>(verb: GhVerb<T>, runner: AsyncCommandRunner, opts: RunVerbAsyncOptions): Promise<T>;
package/dist/run.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `runVerb` and `runVerbAsync` — run one {@link GhVerb} through a runner the
3
+ * caller owns, with everything the verb asks for applied.
4
+ *
5
+ * A verb carries more than `argv`: `input` (a payload on stdin), `env` (the
6
+ * target host as `GH_HOST`), and `checked` (whether a non-zero exit throws).
7
+ * `verb.parse(await runner.run(verb.argv))` drops the first two without a
8
+ * word, so a query goes to github.com and a review is never sent. These two
9
+ * apply all of it, as `Gh` and `AsyncGh` do; both clients call them.
10
+ */
11
+ import { runCheckedGh, runCheckedGhAsync, settle } from "./doors.js";
12
+ /**
13
+ * Run `verb` through `runner` and return its parsed value, honouring the
14
+ * verb's `input`, `env`, `timeoutMs` and `checked`, and its checked or
15
+ * unchecked contract: a failed `gh` throws a `FailureError` carrying the
16
+ * classified `GhFailure`, output that cannot be read throws one of kind
17
+ * `unreadable`, and a query for one thing answers `null` when GitHub says
18
+ * there is no such thing.
19
+ *
20
+ * A plain `(binary, argv, opts) => result` function is not a runner: wrap it
21
+ * as `{ run: (argv, opts) => fn('gh', argv, opts) }`.
22
+ */
23
+ export function runVerb(verb, runner, opts) {
24
+ const run = { cwd: opts.cwd };
25
+ if (verb.input !== undefined)
26
+ run.input = verb.input;
27
+ if (verb.env !== undefined)
28
+ run.env = verb.env;
29
+ if (opts.timeoutMs !== undefined)
30
+ run.timeoutMs = opts.timeoutMs;
31
+ const result = verb.checked ? runCheckedGh(runner, verb.argv, run) : runner.run(verb.argv, run);
32
+ return settle(verb, result, runner.binary);
33
+ }
34
+ /** {@link runVerb} for an `AsyncCommandRunner`; rejects where that throws. */
35
+ export async function runVerbAsync(verb, runner, opts) {
36
+ const run = { cwd: opts.cwd };
37
+ if (verb.input !== undefined)
38
+ run.input = verb.input;
39
+ if (verb.env !== undefined)
40
+ run.env = verb.env;
41
+ if (opts.timeoutMs !== undefined)
42
+ run.timeoutMs = opts.timeoutMs;
43
+ if (opts.signal !== undefined)
44
+ run.signal = opts.signal;
45
+ const result = verb.checked
46
+ ? await runCheckedGhAsync(runner, verb.argv, run)
47
+ : await runner.run(verb.argv, run);
48
+ return settle(verb, result, runner.binary);
49
+ }
package/dist/sync.d.ts CHANGED
@@ -7,11 +7,15 @@
7
7
  * REST — the credential already exists in the user's keychain, and a library
8
8
  * that reaches for `GITHUB_TOKEN` is a library that can leak one.
9
9
  *
10
- * Tri-modal, same as the git client: queries return parsed values and never
11
- * throw, mutations throw `CommandFailed`, and `raw`/`rawChecked` are the
12
- * escape hatches.
10
+ * The same three doors as the git client. The plain method returns a value
11
+ * or throws: a failed `gh` throws a `FailureError` carrying the classified
12
+ * `GhFailure`, output that cannot be read throws one of kind `unreadable`, and a query for one thing (`prView`, `issueView`,
13
+ * `repoView`) answers `null` when GitHub says there is no such thing.
14
+ * `gh.try.*` returns the failure as a `Result<T, GhFailure>`; `gh.soft.*` returns
15
+ * `null` for it. `raw`/`rawChecked` are the escape hatches.
13
16
  */
14
17
  import { type CommandResult, type CommandRunner } from '@osdlc/command-seam';
18
+ import { type Soft, type TriedSync } from './doors.ts';
15
19
  import * as v from './verbs.ts';
16
20
  import type { ApiOptions, ApiResponse, AuthStatus, GhTarget, IssueListOptions, ListResult, PrListOptions } from './verbs.ts';
17
21
  import type { IssueRow, Label, OwnerRepo, PrSearchHit, PullRequest, Repository } from './types.ts';
@@ -47,28 +51,49 @@ export declare class Gh {
47
51
  readonly hostname: string | undefined;
48
52
  private readonly target;
49
53
  private readonly runner;
54
+ private tried;
55
+ private softened;
50
56
  constructor(cwd: string, opts?: GhOptions);
51
57
  /** The same client rooted at a different directory (same runner, repo and host). */
52
58
  at(cwd: string): Gh;
53
59
  /** The same client aimed at another repository (same runner, directory and host). */
54
60
  withRepo(repo: string): Gh;
61
+ /**
62
+ * The whole surface again, returning a `Result<T, GhFailure>` instead of
63
+ * throwing: `gh.try.prList()` is `{ ok: true, value } | { ok: false, error }`.
64
+ * A query for one thing that finds nothing is `ok: true` with `null`. See
65
+ * `./doors.ts`.
66
+ *
67
+ * The type leaves out the escape hatches and the pure helpers; at runtime
68
+ * the proxy wraps `raw` and `rawChecked` too. Call those on the client itself.
69
+ */
70
+ get try(): TriedSync<Gh>;
71
+ /**
72
+ * Each query again, returning `null` instead of throwing when `gh` fails. A
73
+ * mutation has no soft form. Do not gate a write on a `null` from `soft`:
74
+ * it is the answer for "could not ask" as well as for "nothing there". Use
75
+ * `try` where the difference matters.
76
+ */
77
+ get soft(): Soft<Gh>;
55
78
  /** Unchecked passthrough for verbs not yet lifted into the API. */
56
79
  raw(argv: string[], opts?: {
57
80
  timeoutMs?: number | undefined;
58
81
  }): CommandResult;
59
- /** Checked passthrough — throws `CommandFailed` on a non-zero exit. */
82
+ /** Checked passthrough — throws the classified failure on a non-zero exit. */
60
83
  rawChecked(argv: string[], opts?: {
61
84
  timeoutMs?: number | undefined;
62
85
  }): CommandResult;
63
86
  /** Run one verb from the table, honouring its checked/unchecked contract. */
64
87
  private exec;
65
- /** Pull requests matching `opts`, or null when gh failed. */
88
+ /** {@link exec} for a verb that never answers `null` as a value: a failed or unreadable run throws. */
89
+ private read;
90
+ /** Pull requests matching `opts`. Throws when gh fails; `soft.prList` gives null instead. */
66
91
  prList(opts?: PrListOptions & {
67
92
  timeoutMs?: number | undefined;
68
- }): ListResult<PullRequest> | null;
93
+ }): ListResult<PullRequest>;
69
94
  /**
70
- * One PR by number, branch or URL, with the heavy fields; null when gh
71
- * failed or there is no such PR.
95
+ * One PR by number, branch or URL, with the heavy fields; null when there is
96
+ * no such PR. Throws when gh fails otherwise.
72
97
  */
73
98
  prView(ref: number | string, opts?: {
74
99
  fields?: readonly string[] | undefined;
@@ -119,52 +144,52 @@ export declare class Gh {
119
144
  sleep?: ((ms: number) => void) | undefined;
120
145
  timeoutMs?: number | undefined;
121
146
  }): PullRequest | null;
122
- /** A PR's body text, or null when gh failed. */
147
+ /** A PR's body text. Throws when gh fails. */
123
148
  prBody(number: number, opts?: {
124
149
  timeoutMs?: number | undefined;
125
- }): string | null;
150
+ }): string;
126
151
  /**
127
- * A REST GET through `gh api`. Never throws: failure is `ok: false` on the
128
- * returned {@link ApiResponse}.
152
+ * A REST GET through `gh api`. Never throws for a failed call, in any door:
153
+ * failure is `ok: false` on the returned {@link ApiResponse}.
129
154
  */
130
155
  api(endpoint: string, opts?: ApiOptions & {
131
156
  timeoutMs?: number | undefined;
132
157
  }): ApiResponse;
133
158
  /**
134
- * A GraphQL query; each page's `data`, or null on a failed or partial
135
- * result. See the `graphql` verb.
159
+ * A GraphQL query; each page's `data`. Throws on a failed or partial result;
160
+ * `soft.graphql` gives null. See the `graphql` verb.
136
161
  *
137
162
  * @experimental
138
163
  */
139
164
  graphql(query: string, variables?: Record<string, v.GraphqlVariable>, opts?: v.GraphqlOptions & {
140
165
  timeoutMs?: number | undefined;
141
- }): unknown[] | null;
142
- /** Every label on the repository (up to `opts.limit`, default 1000), or null when gh failed. */
166
+ }): unknown[];
167
+ /** Every label on the repository (up to `opts.limit`, default 1000). Throws when gh fails. */
143
168
  labelList(opts?: {
144
169
  limit?: number | undefined;
145
170
  timeoutMs?: number | undefined;
146
- }): ListResult<Label> | null;
147
- /** Issues matching `opts` (open by default, up to 200), or null when gh failed. */
171
+ }): ListResult<Label>;
172
+ /** Issues matching `opts` (open by default, up to 200). Throws when gh fails. */
148
173
  issueList(opts?: IssueListOptions & {
149
174
  timeoutMs?: number | undefined;
150
- }): ListResult<IssueRow> | null;
151
- /** One issue with its labels, or null when gh failed. */
175
+ }): ListResult<IssueRow>;
176
+ /** One issue with its labels; null when there is no such issue. Throws when gh fails otherwise. */
152
177
  issueView(number: number, opts?: {
153
178
  timeoutMs?: number | undefined;
154
179
  }): IssueRow | null;
155
- /** An owner's repositories (up to `opts.limit`, default 100), or null when gh failed. */
180
+ /** An owner's repositories (up to `opts.limit`, default 100). Throws when gh fails. */
156
181
  repoList(owner: string, opts?: v.RepoListOptions & {
157
182
  timeoutMs?: number | undefined;
158
- }): ListResult<Repository> | null;
159
- /** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when gh failed. */
183
+ }): ListResult<Repository>;
184
+ /** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when there is no such repository. */
160
185
  repoView(repo?: string | undefined, opts?: {
161
186
  fields?: readonly string[] | undefined;
162
187
  timeoutMs?: number | undefined;
163
188
  }): Repository | null;
164
- /** Pull requests across repositories matching a search query, or null when gh failed. */
189
+ /** Pull requests across repositories matching a search query. Throws when gh fails. */
165
190
  searchPrs(query: string, opts?: v.SearchPrsOptions & {
166
191
  timeoutMs?: number | undefined;
167
- }): ListResult<PrSearchHit> | null;
192
+ }): ListResult<PrSearchHit>;
168
193
  /** True iff the `gh` binary is on PATH and runs. */
169
194
  installed(): boolean;
170
195
  /**
@@ -196,7 +221,7 @@ export declare class Gh {
196
221
  title?: string | undefined;
197
222
  body?: string | undefined;
198
223
  }): void;
199
- /** The PR's unified diff; throws `CommandFailed` when gh cannot produce it. */
224
+ /** The PR's unified diff; throws the classified failure when gh cannot produce it. */
200
225
  prDiff(number: number): string;
201
226
  /** Post one conversation comment on a PR; returns its URL ('' if gh printed none). */
202
227
  prComment(number: number, body: string): string;
@@ -223,8 +248,8 @@ export declare class Gh {
223
248
  method: v.PrMergeMethod;
224
249
  headSha: string;
225
250
  }): void;
226
- /** A PR's review threads, or null when they could not be read completely. */
227
- prReviewThreads(number: number): v.ReviewThread[] | null;
251
+ /** A PR's review threads. Throws when they could not be read completely. */
252
+ prReviewThreads(number: number): v.ReviewThread[];
228
253
  /** Create a label (or force it to `opts`'s color/description if it already exists). */
229
254
  createLabel(opts: v.CreateLabelOptions): void;
230
255
  /** Recolor and/or redescribe an existing label. */
@@ -239,4 +264,3 @@ export declare class Gh {
239
264
  */
240
265
  setIssueLabel(number: number, currentLabel: string | null, newLabel: string): void;
241
266
  }
242
- //# sourceMappingURL=sync.d.ts.map
package/dist/sync.js CHANGED
@@ -7,11 +7,16 @@
7
7
  * REST — the credential already exists in the user's keychain, and a library
8
8
  * that reaches for `GITHUB_TOKEN` is a library that can leak one.
9
9
  *
10
- * Tri-modal, same as the git client: queries return parsed values and never
11
- * throw, mutations throw `CommandFailed`, and `raw`/`rawChecked` are the
12
- * escape hatches.
10
+ * The same three doors as the git client. The plain method returns a value
11
+ * or throws: a failed `gh` throws a `FailureError` carrying the classified
12
+ * `GhFailure`, output that cannot be read throws one of kind `unreadable`, and a query for one thing (`prView`, `issueView`,
13
+ * `repoView`) answers `null` when GitHub says there is no such thing.
14
+ * `gh.try.*` returns the failure as a `Result<T, GhFailure>`; `gh.soft.*` returns
15
+ * `null` for it. `raw`/`rawChecked` are the escape hatches.
13
16
  */
14
- import { runChecked, resolveRunner, spawnRunner, } from '@osdlc/command-seam';
17
+ import { resolveRunner, spawnRunner, } from '@osdlc/command-seam';
18
+ import { runCheckedGh, softSync, triedSync } from "./doors.js";
19
+ import { runVerb } from "./run.js";
15
20
  import * as v from "./verbs.js";
16
21
  import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from "./repo.js";
17
22
  /** Default factory: real, spawnSync-backed runners. */
@@ -38,6 +43,8 @@ export class Gh {
38
43
  hostname;
39
44
  target;
40
45
  runner;
46
+ tried;
47
+ softened;
41
48
  constructor(cwd, opts = {}) {
42
49
  v.checkTarget(opts);
43
50
  this.cwd = cwd;
@@ -54,37 +61,52 @@ export class Gh {
54
61
  withRepo(repo) {
55
62
  return new Gh(this.cwd, { runner: this.runner, repo, hostname: this.hostname });
56
63
  }
64
+ /**
65
+ * The whole surface again, returning a `Result<T, GhFailure>` instead of
66
+ * throwing: `gh.try.prList()` is `{ ok: true, value } | { ok: false, error }`.
67
+ * A query for one thing that finds nothing is `ok: true` with `null`. See
68
+ * `./doors.ts`.
69
+ *
70
+ * The type leaves out the escape hatches and the pure helpers; at runtime
71
+ * the proxy wraps `raw` and `rawChecked` too. Call those on the client itself.
72
+ */
73
+ get try() {
74
+ return (this.tried ??= triedSync(this));
75
+ }
76
+ /**
77
+ * Each query again, returning `null` instead of throwing when `gh` fails. A
78
+ * mutation has no soft form. Do not gate a write on a `null` from `soft`:
79
+ * it is the answer for "could not ask" as well as for "nothing there". Use
80
+ * `try` where the difference matters.
81
+ */
82
+ get soft() {
83
+ return (this.softened ??= softSync(this));
84
+ }
57
85
  // ── escape hatches ────────────────────────────────────────────────────────
58
86
  /** Unchecked passthrough for verbs not yet lifted into the API. */
59
87
  raw(argv, opts = {}) {
60
88
  return this.runner.run(argv, { cwd: this.cwd, ...opts });
61
89
  }
62
- /** Checked passthrough — throws `CommandFailed` on a non-zero exit. */
90
+ /** Checked passthrough — throws the classified failure on a non-zero exit. */
63
91
  rawChecked(argv, opts = {}) {
64
- return runChecked(this.runner, argv, { cwd: this.cwd, ...opts });
92
+ return runCheckedGh(this.runner, argv, { cwd: this.cwd, ...opts });
65
93
  }
66
94
  /** Run one verb from the table, honouring its checked/unchecked contract. */
67
95
  exec(verb, timeoutMs) {
68
- const opts = { cwd: this.cwd };
69
- if (verb.input !== undefined)
70
- opts.input = verb.input;
71
- if (verb.env !== undefined)
72
- opts.env = verb.env;
73
- if (timeoutMs !== undefined)
74
- opts.timeoutMs = timeoutMs;
75
- const result = verb.checked
76
- ? runChecked(this.runner, verb.argv, opts)
77
- : this.runner.run(verb.argv, opts);
78
- return verb.parse(result);
96
+ return runVerb(verb, this.runner, { cwd: this.cwd, timeoutMs });
97
+ }
98
+ /** {@link exec} for a verb that never answers `null` as a value: a failed or unreadable run throws. */
99
+ read(verb, timeoutMs) {
100
+ return this.exec(verb, timeoutMs);
79
101
  }
80
102
  // ── queries ───────────────────────────────────────────────────────────────
81
- /** Pull requests matching `opts`, or null when gh failed. */
103
+ /** Pull requests matching `opts`. Throws when gh fails; `soft.prList` gives null instead. */
82
104
  prList(opts = {}) {
83
- return this.exec(v.prList(opts, this.target), opts.timeoutMs);
105
+ return this.read(v.prList(opts, this.target), opts.timeoutMs);
84
106
  }
85
107
  /**
86
- * One PR by number, branch or URL, with the heavy fields; null when gh
87
- * failed or there is no such PR.
108
+ * One PR by number, branch or URL, with the heavy fields; null when there is
109
+ * no such PR. Throws when gh fails otherwise.
88
110
  */
89
111
  prView(ref, opts = {}) {
90
112
  return this.exec(v.prView(ref, opts.fields ?? v.PR_VIEW_FIELDS, this.target), opts.timeoutMs);
@@ -128,7 +150,7 @@ export class Gh {
128
150
  limit: opts.limit ?? 5,
129
151
  fields: opts.fields ?? v.PR_REF_FIELDS,
130
152
  });
131
- return v.pickPrForBranch(rows?.rows ?? null, branch);
153
+ return v.pickPrForBranch(rows.rows, branch);
132
154
  }
133
155
  /**
134
156
  * {@link prView}, re-asked until GitHub settles the merge state.
@@ -155,49 +177,49 @@ export class Gh {
155
177
  }
156
178
  return last;
157
179
  }
158
- /** A PR's body text, or null when gh failed. */
180
+ /** A PR's body text. Throws when gh fails. */
159
181
  prBody(number, opts = {}) {
160
- return this.exec(v.prBody(number, this.target), opts.timeoutMs);
182
+ return this.read(v.prBody(number, this.target), opts.timeoutMs);
161
183
  }
162
184
  /**
163
- * A REST GET through `gh api`. Never throws: failure is `ok: false` on the
164
- * returned {@link ApiResponse}.
185
+ * A REST GET through `gh api`. Never throws for a failed call, in any door:
186
+ * failure is `ok: false` on the returned {@link ApiResponse}.
165
187
  */
166
188
  api(endpoint, opts = {}) {
167
189
  return this.exec(v.api(endpoint, opts, this.target), opts.timeoutMs);
168
190
  }
169
191
  /**
170
- * A GraphQL query; each page's `data`, or null on a failed or partial
171
- * result. See the `graphql` verb.
192
+ * A GraphQL query; each page's `data`. Throws on a failed or partial result;
193
+ * `soft.graphql` gives null. See the `graphql` verb.
172
194
  *
173
195
  * @experimental
174
196
  */
175
197
  graphql(query, variables = {}, opts = {}) {
176
- return this.exec(v.graphql(query, variables, opts, this.target), opts.timeoutMs);
198
+ return this.read(v.graphql(query, variables, opts, this.target), opts.timeoutMs);
177
199
  }
178
- /** Every label on the repository (up to `opts.limit`, default 1000), or null when gh failed. */
200
+ /** Every label on the repository (up to `opts.limit`, default 1000). Throws when gh fails. */
179
201
  labelList(opts = {}) {
180
- return this.exec(v.labelList(opts, this.target), opts.timeoutMs);
202
+ return this.read(v.labelList(opts, this.target), opts.timeoutMs);
181
203
  }
182
- /** Issues matching `opts` (open by default, up to 200), or null when gh failed. */
204
+ /** Issues matching `opts` (open by default, up to 200). Throws when gh fails. */
183
205
  issueList(opts = {}) {
184
- return this.exec(v.issueList(opts, this.target), opts.timeoutMs);
206
+ return this.read(v.issueList(opts, this.target), opts.timeoutMs);
185
207
  }
186
- /** One issue with its labels, or null when gh failed. */
208
+ /** One issue with its labels; null when there is no such issue. Throws when gh fails otherwise. */
187
209
  issueView(number, opts = {}) {
188
210
  return this.exec(v.issueView(number, this.target), opts.timeoutMs);
189
211
  }
190
- /** An owner's repositories (up to `opts.limit`, default 100), or null when gh failed. */
212
+ /** An owner's repositories (up to `opts.limit`, default 100). Throws when gh fails. */
191
213
  repoList(owner, opts = {}) {
192
- return this.exec(v.repoList(owner, opts, this.target), opts.timeoutMs);
214
+ return this.read(v.repoList(owner, opts, this.target), opts.timeoutMs);
193
215
  }
194
- /** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when gh failed. */
216
+ /** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when there is no such repository. */
195
217
  repoView(repo, opts = {}) {
196
218
  return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts.timeoutMs);
197
219
  }
198
- /** Pull requests across repositories matching a search query, or null when gh failed. */
220
+ /** Pull requests across repositories matching a search query. Throws when gh fails. */
199
221
  searchPrs(query, opts = {}) {
200
- return this.exec(v.searchPrs(query, opts, this.target), opts.timeoutMs);
222
+ return this.read(v.searchPrs(query, opts, this.target), opts.timeoutMs);
201
223
  }
202
224
  /** True iff the `gh` binary is on PATH and runs. */
203
225
  installed() {
@@ -242,7 +264,7 @@ export class Gh {
242
264
  editPr(number, opts) {
243
265
  this.exec(v.editPr(number, opts, this.target));
244
266
  }
245
- /** The PR's unified diff; throws `CommandFailed` when gh cannot produce it. */
267
+ /** The PR's unified diff; throws the classified failure when gh cannot produce it. */
246
268
  prDiff(number) {
247
269
  return this.exec(v.prDiff(number, this.target));
248
270
  }
@@ -282,9 +304,9 @@ export class Gh {
282
304
  prMerge(number, opts) {
283
305
  this.exec(v.prMerge(number, opts, this.target));
284
306
  }
285
- /** A PR's review threads, or null when they could not be read completely. */
307
+ /** A PR's review threads. Throws when they could not be read completely. */
286
308
  prReviewThreads(number) {
287
- return this.exec(v.prReviewThreads(number, this.target));
309
+ return this.read(v.prReviewThreads(number, this.target));
288
310
  }
289
311
  /** Create a label (or force it to `opts`'s color/description if it already exists). */
290
312
  createLabel(opts) {
package/dist/types.d.ts CHANGED
@@ -186,4 +186,3 @@ export interface PrSearchHit {
186
186
  updatedAt: string | null;
187
187
  closedAt: string | null;
188
188
  }
189
- //# sourceMappingURL=types.d.ts.map
package/dist/verbs.d.ts CHANGED
@@ -7,9 +7,12 @@
7
7
  * asynchronous fan-out drive the same definitions without either duplicating
8
8
  * the other's parsing.
9
9
  *
10
- * `checked` marks a verb whose failure is thrown with stderr: every mutation,
11
- * and `prDiff`, which is a read. Other queries are unchecked and fold failure
12
- * into an absence value.
10
+ * `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`.
13
16
  */
14
17
  import { type CommandResult } from '@osdlc/command-seam';
15
18
  import type { IssueRow, Label, PrSearchHit, PullRequest, Repository } from './types.ts';
@@ -31,6 +34,19 @@ export interface GhVerb<T> {
31
34
  * on `gh search` and `gh repo list`.
32
35
  */
33
36
  env?: Record<string, string> | undefined;
37
+ /**
38
+ * True for a read whose failure is part of its answer: `installed`,
39
+ * `authStatus` and `api` return what `parse` makes of any result, in every
40
+ * door. Other reads fail the plain door when gh exits non-zero.
41
+ */
42
+ probe?: boolean | undefined;
43
+ /**
44
+ * For a read of one thing: whether a non-zero result means GitHub has no such
45
+ * thing (`prView`, `issueView`, `repoView`). The plain door then returns what
46
+ * `parse` makes of it, `null`, because absence is an answer. Other non-zero
47
+ * results fail the call.
48
+ */
49
+ absent?: ((result: CommandResult) => boolean) | undefined;
34
50
  parse(result: CommandResult): T;
35
51
  }
36
52
  /**
@@ -104,7 +120,7 @@ export interface PrListOptions {
104
120
  search?: string | undefined;
105
121
  author?: string | undefined;
106
122
  limit?: number | undefined;
107
- /** Field subset; defaults to {@link PR_LIST_FIELDS}. */
123
+ /** Field subset; defaults to {@link PR_LIST_FIELDS}. `number` is always requested. */
108
124
  fields?: readonly string[] | undefined;
109
125
  }
110
126
  /** `gh pr list …` — open PRs by default. `opts.repo` overrides the target's repo. */
@@ -137,13 +153,15 @@ export declare function prBody(number: number, target?: GhTarget): GhVerb<string
137
153
  export interface ApiResponse {
138
154
  /** True iff gh exited 0 and the status is below 400 (2xx, or a 304 on a conditional GET). */
139
155
  ok: boolean;
140
- /** HTTP status; read off the header block under `-i`. Without `-i` it is synthesized (200 on exit 0, 0 on failure), not observed. */
156
+ /** HTTP status; set only when headers were requested (`-i`, `etag` or `includeHeaders`). Without them it is synthesized (200 on exit 0, 0 on failure), not observed. */
141
157
  status: number;
142
158
  /** The response `ETag`, when headers were requested and one came back. */
143
159
  etag: string | null;
144
- /** The decoded JSON body; null when empty (e.g. a 304), unparseable, or the call failed. */
160
+ /** The decoded JSON body, whatever gh's exit code (on failure, GitHub's error object); null when empty (e.g. a 304) or unparseable. */
145
161
  body: unknown;
146
- /** True when a non-empty body arrived but was not valid JSON (`body` is then null). */
162
+ /** `body.message` when the body is an object with a string `message` (GitHub's error text), else null. */
163
+ message: string | null;
164
+ /** True when a non-empty body arrived but was not valid JSON (`body` is then null); also true on a failed call with a non-JSON body. */
147
165
  malformed: boolean;
148
166
  /** gh's stderr, trimmed. */
149
167
  stderr: string;
@@ -461,4 +479,3 @@ export declare function branchPrFields(fields: readonly string[]): string[];
461
479
  * slow, so re-ask a few times before believing it.
462
480
  */
463
481
  export declare function needsMergeStateRetry(pr: PullRequest): boolean;
464
- //# sourceMappingURL=verbs.d.ts.map
package/dist/verbs.js CHANGED
@@ -7,13 +7,21 @@
7
7
  * asynchronous fan-out drive the same definitions without either duplicating
8
8
  * the other's parsing.
9
9
  *
10
- * `checked` marks a verb whose failure is thrown with stderr: every mutation,
11
- * and `prDiff`, which is a read. Other queries are unchecked and fold failure
12
- * into an absence value.
10
+ * `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`.
13
16
  */
14
17
  import { stderrOrError } from '@osdlc/command-seam';
15
18
  import { parseLabels, parsePrSearchHit, parsePullRequest, parseRepository } from "./parse.js";
19
+ import { isAbsent } from "./failure.js";
16
20
  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. */
22
+ function noSuch(thing) {
23
+ return (r) => isAbsent(thing, `${r.stderr}\n${r.stdout}`);
24
+ }
17
25
  const HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::\d+)?$/;
18
26
  /**
19
27
  * Throw `RangeError` unless `target.repo` is `owner/name` and
@@ -243,9 +251,12 @@ export function prList(opts = {}, target) {
243
251
  argv.push('--search', opts.search);
244
252
  if (opts.author !== undefined)
245
253
  argv.push('--author', opts.author);
246
- // The exact-head filter below reads `headRefName`, so ask for it.
247
- const fields = opts.fields ?? PR_LIST_FIELDS;
248
- const asked = opts.head === undefined ? fields : withFields(fields, ['headRefName']);
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`.
256
+ const asked = withFields(opts.fields ?? PR_LIST_FIELDS, [
257
+ 'number',
258
+ ...(opts.head === undefined ? [] : ['headRefName']),
259
+ ]);
249
260
  argv.push('--json', asked.join(','));
250
261
  argv.push('--limit', String(limit + 1));
251
262
  const head = opts.head;
@@ -300,6 +311,7 @@ export function prView(ref, fields = PR_VIEW_FIELDS, target) {
300
311
  ],
301
312
  checked: false,
302
313
  readOnly: true,
314
+ absent: noSuch('pr'),
303
315
  parse: oneOrNull,
304
316
  };
305
317
  }
@@ -354,6 +366,7 @@ export function api(endpoint, opts = {}, target) {
354
366
  argv,
355
367
  checked: false,
356
368
  readOnly: true,
369
+ probe: true,
357
370
  parse(r) {
358
371
  const response = parseApi(r, withHeaders);
359
372
  return paginate ? { ...response, body: flattenPages(response.body) } : response;
@@ -506,7 +519,7 @@ function parseApi(r, withHeaders) {
506
519
  }
507
520
  let body = null;
508
521
  let malformed = false;
509
- if (r.exitCode === 0 && bodyText.trim() !== '') {
522
+ if (bodyText.trim() !== '') {
510
523
  try {
511
524
  body = JSON.parse(bodyText);
512
525
  }
@@ -515,11 +528,15 @@ function parseApi(r, withHeaders) {
515
528
  }
516
529
  }
517
530
  const ok = r.exitCode === 0 && status >= 200 && status < 400;
531
+ const rawMessage = body !== null && typeof body === 'object' && !Array.isArray(body)
532
+ ? body['message']
533
+ : undefined;
518
534
  return {
519
535
  ok,
520
536
  status,
521
537
  etag,
522
538
  body,
539
+ message: typeof rawMessage === 'string' ? rawMessage : null,
523
540
  malformed,
524
541
  stderr: r.stderr.trim(),
525
542
  ...(r.error === undefined ? {} : { error: r.error }),
@@ -527,7 +544,13 @@ function parseApi(r, withHeaders) {
527
544
  }
528
545
  /** `gh --version` — the install probe. True when `gh` runs. */
529
546
  export function installed() {
530
- return { argv: ['--version'], checked: false, readOnly: true, parse: (r) => r.exitCode === 0 };
547
+ return {
548
+ argv: ['--version'],
549
+ checked: false,
550
+ readOnly: true,
551
+ probe: true,
552
+ parse: (r) => r.exitCode === 0,
553
+ };
531
554
  }
532
555
  /** `gh auth status [--hostname <host>]` — the credential probe, for one host when the target names it. */
533
556
  export function authStatus(target) {
@@ -535,6 +558,7 @@ export function authStatus(target) {
535
558
  argv: ['auth', 'status', ...hostArgs(target)],
536
559
  checked: false,
537
560
  readOnly: true,
561
+ probe: true,
538
562
  parse: (r) => ({ ok: r.exitCode === 0, detail: stderrOrError(r) || r.stdout.trim() }),
539
563
  };
540
564
  }
@@ -576,6 +600,7 @@ export function issueView(number, target) {
576
600
  argv: ['issue', 'view', String(number), ...repoArgs(target), '--json', ISSUE_FIELDS],
577
601
  checked: false,
578
602
  readOnly: true,
603
+ absent: noSuch('issue'),
579
604
  parse: oneIssueOrNull,
580
605
  };
581
606
  }
@@ -660,6 +685,7 @@ export function repoView(repo, fields = REPO_FIELDS, target) {
660
685
  argv,
661
686
  checked: false,
662
687
  readOnly: true,
688
+ absent: noSuch('repo'),
663
689
  parse(r) {
664
690
  if (r.exitCode !== 0)
665
691
  return null;