@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/CHANGELOG.md CHANGED
@@ -1,4 +1,59 @@
1
- # Changelog
1
+ # @osdlc/typed-gh
2
+
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cc2abe4: `api` now keeps GitHub's error body when a request fails. `body` is parsed even when gh exits with an error, and a new `message` field holds `body.message`. `ApiResponse` gains this `message` field as required. `malformed` is now also set when a failed call prints something that is not JSON. The docs now say that `status` is only filled in when headers were requested.
8
+
9
+ ### Patch Changes
10
+
11
+ - 72f5049: `prList` always requests the `number` field. Before, a `fields` list without `number` returned no rows.
12
+ - Updated dependencies [b5aac07]
13
+ - Updated dependencies [41ef520]
14
+ - @osdlc/typed-git@0.7.0
15
+
16
+ ## 0.8.0
17
+
18
+ ### Minor Changes
19
+
20
+ - ae400e5: Added `runVerb` and `runVerbAsync`, with `RunVerbOptions` and `RunVerbAsyncOptions`. They run one verb through a runner the caller owns and apply what the verb carries: `env` (the target host, as `GH_HOST`, for `repoList` and `searchPrs`), `input` (the payload `createReview` sends on stdin), `timeoutMs` and the checked contract. Running `verb.parse(await run(verb.argv))` yourself dropped `env` and `input` without an error. `Gh` and `AsyncGh` now call them, so there is one copy of that logic.
21
+
22
+ ## 0.7.0
23
+
24
+ ### Minor Changes
25
+
26
+ - d9b0f77: **Breaking:** the plain door throws the classified failure. A failed `gh` (a mutation, `prDiff`, `rawChecked`, a query) now throws a `FailureError` whose `failure` is the `GhFailure` and whose `failure.cause` is the `CommandFailed`, where it threw the `CommandFailed` itself. The bare `CommandFailed` only had command-seam's generic `exit` kind, so a signed-out `gh` during a write exited 1 from `runMain` instead of 23. Read `error.failure.argv` and `error.failure.cause.result` where you read `error.argv` and `error.result`.
27
+
28
+ **Breaking:** the single-thing queries no longer treat every `Could not resolve to a …` as absence. `prView` answers `null` only for a missing pull request, `issueView` only for a missing issue and `repoView` only for a missing repository; a mistyped `--repo` is now a `not-found` failure, and a bare `HTTP 404` is a failure. The absence test reads stderr and stdout, as the classifier does.
29
+
30
+ An `HTTP 403` is now `not-authenticated` (SAML, a missing scope), the same as Forgejo's 401 and 403, unless the text says it is a rate limit, which stays `rate-limited`. The `try` and `soft` doors recognise a thrown failure by shape, so two installed copies of `@osdlc/command-seam` or `@osdlc/result` agree.
31
+ - d9b0f77: `Gh` and `AsyncGh` get `typed-git`'s three doors. **Breaking:** a query no longer answers `null` for a failure. The plain method returns its value or throws `CommandFailed` (a `FailureError` carrying a `GhFailure` of kind `unreadable` when `gh` exited 0 but its output could not be read). A query for one thing (`prView`, `issueView`, `repoView`, and `prFind`, `openPrFor`, `prViewSettled` over them) still answers `null` when GitHub says there is no such thing. `prList`, `prBody`, `graphql`, `labelList`, `issueList`, `repoList`, `searchPrs` and `prReviewThreads` now return a non-null value and throw. `installed`, `authenticated`, `authStatus`, `ready` and `api` are unchanged: their failure is part of their value.
32
+
33
+ New: `gh.try.*` returns `Result<T, GhFailure>` from `@osdlc/result`; `gh.soft.*` returns `null` for a failed query, which is the old query behaviour, now opt-in (`TriedSync`, `TriedAsync`, `Soft`). `GhFailure` carries a `kind`, a one-line `message`, a `hint` for a person, `detail`, `argv`, `exitCode` and the `CommandFailed` as `cause`. `GhFailureKind` is every `@osdlc/command-seam` kind plus `not-authenticated`, `network`, `not-found`, `rate-limited` and `unreadable`, read from `gh`'s stderr. `classifyGhFailure(commandFailed)` is exported. `@osdlc/result` is now a dependency.
34
+
35
+ ### Patch Changes
36
+
37
+ - fb712e0: Ship the compiled `dist/` only. The `bun` export condition and the TypeScript source are no longer in the package, so Bun, Node and every bundler load the same compiled ESM and `.d.ts` files. Bun still works through the compiled `dist/`.
38
+ - fb712e0: Make every package follow the same conventions. The published manifests and READMEs change; the runtime code and the `dist/` files do not.
39
+
40
+ - `author` is `sksizer` in every manifest (`@osdlc/typed-gh` had none).
41
+ - `engines.node` is no wider than any dependency's. `@osdlc/typed-gh` and `@osdlc/code-host` now declare `^22.22.2 || ^24.15.0 || >=26.0.0`, the range of `@osdlc/typed-git` and its dependency `hosted-git-info`. `@osdlc/cli-tool` and `@osdlc/trust-repo` declare `>=22`, not `>=22.18`: a `dist/` install needs no TypeScript stripping.
42
+ - `@osdlc/tsconfig` and `@osdlc/prettier-config` are ESM with `sideEffects: false`, and ship SECURITY.md and CONTRIBUTING.md. `@osdlc/prettier-config` has an `exports` map (`.` is `index.json`) in place of `main`.
43
+ - Every README has `## Install` and `## Licence` sections, and every CHANGELOG starts `# @osdlc/<name>`.
44
+ - `@osdlc/color` and `@osdlc/markdown-contract` no longer set a coverage threshold.
45
+ - `@osdlc/yaml-splice` reaches its WebAssembly glue through a `#wasm` entry in `imports`. The shipped `dist/` files are the same.
46
+ - Updated dependencies [d9b0f77]
47
+ - Updated dependencies [d9b0f77]
48
+ - Updated dependencies [fb712e0]
49
+ - Updated dependencies [fb712e0]
50
+ - Updated dependencies [d9b0f77]
51
+ - Updated dependencies [d9b0f77]
52
+ - Updated dependencies [d9b0f77]
53
+ - Updated dependencies [d9b0f77]
54
+ - @osdlc/command-seam@0.9.0
55
+ - @osdlc/typed-git@0.6.0
56
+ - @osdlc/result@0.1.0
2
57
 
3
58
  ## 0.6.0
4
59
 
package/CONTRIBUTING.md CHANGED
@@ -39,8 +39,9 @@ pnpm --filter @osdlc/typed-gh smoke
39
39
 
40
40
  ## Running the tests
41
41
 
42
- [Bun](https://bun.sh) is only the test runner. pnpm installs and
43
- runs the scripts, and the `test` script runs `bun test`. The tests run offline: a fake
42
+ pnpm installs and runs the scripts, and the `test` script runs Vitest under
43
+ Node. Build first, since the workspace packages resolve each other through
44
+ `dist/`. The tests run offline: a fake
44
45
  runner stands in for `gh`, so they need no network, no login and no `gh`
45
46
  binary.
46
47
 
@@ -71,4 +72,4 @@ version and writes the CHANGELOG from the pending changesets. See
71
72
  call.
72
73
  - Add a changeset for a change users can see. See above.
73
74
  - Record a design choice in
74
- [`docs/decisions/`](https://github.com/osdlc/osdlc/tree/main/packages/ts/typed-gh/docs/decisions).
75
+ [`docs/planning/decisions/`](https://github.com/osdlc/osdlc/tree/main/packages/ts/typed-gh/docs/planning/decisions).
package/README.md CHANGED
@@ -15,7 +15,9 @@ scripts, release bots and coding agents. It is the GitHub-side companion of
15
15
  npm install @osdlc/typed-gh
16
16
  ```
17
17
 
18
- It needs Node 22 or later (or Bun), and the `gh` CLI on `PATH`, signed in with `gh auth login`.
18
+ `pnpm add @osdlc/typed-gh` and `yarn add @osdlc/typed-gh` work too.
19
+
20
+ It needs Node `^22.22.2 || ^24.15.0 || >=26.0.0`, the range of its dependency `@osdlc/typed-git` (Bun also works, through the compiled `dist/`), and the `gh` CLI on `PATH`, signed in with `gh auth login`.
19
21
  It is supported on macOS and Linux. Windows is not supported yet; it will be once a Windows CI job
20
22
  runs the tests.
21
23
 
@@ -88,18 +90,69 @@ and `ReviewDecision` (`APPROVED`, `CHANGES_REQUESTED`, `REVIEW_REQUIRED`,
88
90
  `null`). A value GitHub adds to either reads as `UNKNOWN` or `null`. A check
89
91
  that decides whether to merge can then match every member.
90
92
 
91
- ## Three modes, so failure is never ambiguous
93
+ ## Three doors, as in `typed-git`, so failure is never ambiguous
94
+
95
+ | | a value | on failure |
96
+ | --- | --- | --- |
97
+ | **`gh.query()`** (the plain method) | the value | throws a `FailureError` carrying the classified `GhFailure` (kind `unreadable` when `gh` succeeded but its output cannot be read) |
98
+ | **`gh.try.query()`** | `{ ok: true, value }` | `{ ok: false, error: GhFailure }` |
99
+ | **`gh.soft.query()`** | the value | `null` (queries only; a mutation has no soft form) |
100
+ | **`raw(argv)`** | the raw `CommandResult`; you decide | the same |
101
+
102
+ A query returns a list or a value, never an empty list on failure.
103
+ `prList()` returning `{ rows: [], truncated: false }` means *this repository
104
+ has no open PRs*. A failed call throws, or is `{ ok: false }` through `try`, or
105
+ `null` through `soft`: *we could not ask*. If the two were the same, an
106
+ expired credential would read as "nothing to do".
107
+
108
+ **Absence is an answer.** A query for one thing (`prView`, `issueView`,
109
+ `repoView`, and `prFind` and `openPrFor` over them) returns `null` on every
110
+ door when GitHub says there is no such thing: "Could not resolve to a
111
+ PullRequest" or "no pull requests found for branch" for `prView`, "Could not
112
+ resolve to an Issue" for `issueView`, "Could not resolve to a Repository" for
113
+ `repoView`. `gh.try.prView(99)` is `{ ok: true, value: null }`. Every other
114
+ failure is a failure, including a wrong repository: `prView(7)` against a
115
+ mistyped `--repo` is a `not-found` failure, not an absent PR. The check reads
116
+ stderr and stdout, as the classifier does.
117
+
118
+ **Probes answer.** `installed()`, `authenticated()`, `authStatus()`, `ready()`
119
+ and `api()` return their failure as part of their value, in every door, because
120
+ "gh is not installed" is the answer to the question they ask.
121
+
122
+ ```ts
123
+ import { describe } from '@osdlc/result'
92
124
 
93
- | | on failure |
94
- | --- | --- |
95
- | **queries** | return the absence value, `null`. Never throw. |
96
- | **mutations** | throw `CommandFailed`, carrying argv and the full result. |
97
- | **`raw(argv)`** | hand back the raw `CommandResult`; you decide. |
125
+ const result = gh.try.prList({ limit: 50 })
126
+ if (!result.ok) {
127
+ console.error(describe(result.error)) // e.g. "gh is not signed in to GitHub. Run `gh auth login`."
128
+ }
129
+ ```
98
130
 
99
- A query returns `null`, never an empty list on failure. `prList()` returning
100
- `{ rows: [], truncated: false }` means *this repository has no open PRs*.
101
- `null` means *we could not ask*. If the two were the same, an expired
102
- credential would read as "nothing to do".
131
+ ### `GhFailure`
132
+
133
+ The error of `try` is a `GhFailure`, a `Failure` from `@osdlc/result`: a `kind`
134
+ to branch on, a one-line `message` and a `hint` for a person (`describe(error)`
135
+ joins them), `gh`'s own output in `detail`, the `argv` and `exitCode`, and the
136
+ `CommandFailed` it was read from in `cause`. The plain door throws the same
137
+ failure, wrapped in a `FailureError` (`error.failure`), so a CLI that reports a
138
+ thrown failure, such as `runMain` from `@osdlc/cli-tool`, shows "gh is not
139
+ signed in" for a write and exits with the same code as for a read.
140
+
141
+ | `kind` | When (gh's stderr) | `message` / `hint` |
142
+ | --- | --- | --- |
143
+ | `not-installed` | `gh` is not on `PATH` | `` `gh` is not installed or not on PATH. `` / ``Install it from https://cli.github.com, then run `gh auth login`.`` |
144
+ | `not-authenticated` | `gh auth login`, `HTTP 401`, `HTTP 403` (unless a rate limit), `Bad credentials`, `not logged in` | `gh is not signed in to GitHub.` / ``Run `gh auth login`.`` |
145
+ | `network` | `error connecting to`, `Could not resolve host`, `no such host`, `dial tcp`, `i/o timeout` | `GitHub could not be reached.` / `Check your network connection.` |
146
+ | `not-found` | `Could not resolve to a …`, `HTTP 404`, `no pull requests found for branch` | `GitHub has no such pull request, issue or repository, or you cannot see it.` / `Check the number or name, and that your account can see the repository.` |
147
+ | `rate-limited` | `API rate limit exceeded`, `secondary rate limit`, `HTTP 429` | `GitHub's API rate limit was reached.` / `Wait a few minutes and try again.` |
148
+ | `unreadable` | `gh` exited 0 but its output could not be read completely | `gh answered, but its output could not be read completely.` (no hint, no `cause`) |
149
+ | `not-executable`, `bad-cwd`, `timeout`, `output-cap`, `signal`, `aborted`, `spawn` | as in `@osdlc/command-seam` | command-seam's message, no hint |
150
+ | `exit` | any other non-zero exit | `gh`'s first stderr line |
151
+
152
+ The patterns are `gh`'s English messages; anything else is a plain `exit`,
153
+ never a wrong kind. Rate limiting is checked before authentication, then
154
+ network, then not-found. `classifyGhFailure(commandFailed)` makes the same
155
+ classification for a `CommandFailed` you caught yourself.
103
156
 
104
157
  ## A list says when its limit cut it short
105
158
 
@@ -108,9 +161,9 @@ one row more than `limit` and returns `{ rows, truncated }`: at most `limit`
108
161
  rows, and `truncated: true` when there were more.
109
162
 
110
163
  ```ts
111
- const page: ListResult<PullRequest> | null = gh.prList({ limit: 50 })
164
+ const page: ListResult<PullRequest> = gh.prList({ limit: 50 })
112
165
 
113
- if (page?.truncated) console.warn('more than 50 open PRs; raise the limit')
166
+ if (page.truncated) console.warn('more than 50 open PRs; raise the limit')
114
167
  ```
115
168
 
116
169
  `prList`, `issueList`, `labelList`, `repoList` and `searchPrs` all work this
@@ -130,7 +183,7 @@ acme.withRepo('acme/gadgets').prList()
130
183
  acme.api('repos/{owner}/{repo}/branches')
131
184
  ```
132
185
 
133
- The target goes into the argv, so a `CommandFailed` shows which repository a
186
+ The target goes into the argv, so a failure's `argv` shows which repository a
134
187
  call hit: `pr`, `issue` and `label` calls get `--repo github.acme.com/acme/widgets`,
135
188
  `api` calls get `--hostname` and a filled `{owner}/{repo}`, and `authStatus`
136
189
  asks about that host. `repoList` and `searchPrs` have no host flag in gh, so
@@ -213,7 +266,7 @@ about staleness, never the measure of it. Compute that from the commit graph
213
266
 
214
267
  ## Changing pull requests
215
268
 
216
- Mutations throw `CommandFailed` when `gh` fails.
269
+ Mutations throw the classified failure (a `FailureError`) when `gh` fails. `gh.try.editPr(…)` returns the failure as a `Result` instead; a mutation has no `soft` form.
217
270
 
218
271
  ```ts
219
272
  const url = gh.createPr(options)
@@ -241,23 +294,23 @@ gh.prMerge(9, { method, headSha: 'abc1234' })
241
294
  ```
242
295
 
243
296
  `prReviewThreads(n)` reads the review threads, so you can look for open ones before you merge. It
244
- returns `null` when it could not read all of them (a failed call, an unexpected payload, or more
245
- than 100 threads). That is not the same as "no threads".
297
+ throws when it could not read all of them (a failed call, an unexpected payload, or more
298
+ than 100 threads; `soft.prReviewThreads` is `null`). That is not the same as "no threads".
246
299
 
247
300
  ```ts
248
- const threads: ReviewThread[] | null = gh.prReviewThreads(9)
301
+ const threads: ReviewThread[] = gh.prReviewThreads(9)
249
302
 
250
- threads?.filter((t) => !t.isResolved)
303
+ threads.filter((t) => !t.isResolved)
251
304
  ```
252
305
 
253
306
  ## Issues and labels
254
307
 
255
308
  ```ts
256
- const issue: IssueRow | null = gh.issueView(7)
257
- const triage: ListResult<IssueRow> | null = gh.issueList({ label: 'status:triage', limit: 50 })
309
+ const issue: IssueRow | null = gh.issueView(7) // null: there is no issue 7
310
+ const triage: ListResult<IssueRow> = gh.issueList({ label: 'status:triage', limit: 50 })
258
311
  gh.setIssueLabel(7, 'status:triage', 'status:define')
259
312
 
260
- const labels: ListResult<Label> | null = gh.labelList({ limit: 100 })
313
+ const labels: ListResult<Label> = gh.labelList({ limit: 100 })
261
314
  gh.createLabel({ name: 'status:triage', color: '1f6feb', description: 'Triage stage' })
262
315
  gh.editLabel('status:triage', { description: 'Waiting for triage' })
263
316
  ```
@@ -279,7 +332,7 @@ const review = gh.createReview(9, {
279
332
  gh.issueCommentEdit('4204154439', 'Updated: CI is green.')
280
333
  ```
281
334
 
282
- `prDiff` returns the unified diff and throws `CommandFailed` when `gh` cannot produce it.
335
+ `prDiff` returns the unified diff and throws the classified failure when `gh` cannot produce it.
283
336
 
284
337
  `createReview` is experimental. It posts a review of kind `COMMENT`: it cannot approve or request
285
338
  changes. Each inline comment sits on one line of the new side of the diff. It returns the review's
@@ -304,12 +357,31 @@ const result = await runner.run(verb.argv, { cwd: '/repo', timeoutMs: 5_000 })
304
357
  const prs = verb.parse(result)
305
358
  ```
306
359
 
360
+ That is only safe for a verb with no `env` or `input`. `repoList` and `searchPrs` send the target
361
+ host as `GH_HOST` (`env`), and `createReview` sends its payload on stdin (`input`); a runner call
362
+ that passes only `verb.argv` drops both, silently: the query goes to github.com, and the review is
363
+ never sent. `runVerb` (and `runVerbAsync`, for an `AsyncCommandRunner`) run a verb the way `Gh` and
364
+ `AsyncGh` do, with `cwd`, `env`, `input`, `timeoutMs` and the checked contract applied, and return
365
+ the parsed value or throw the classified failure:
366
+
367
+ ```ts
368
+ import { repoList, runVerbAsync } from '@osdlc/typed-gh'
369
+
370
+ const repos = await runVerbAsync(repoList('acme', {}, { hostname: 'ghe.example.com' }), runner, {
371
+ cwd: '/repo',
372
+ timeoutMs: 5_000,
373
+ })
374
+ ```
375
+
376
+ They take a `CommandRunner`. A plain `(binary, argv, opts) => result` function is wrapped as
377
+ `{ run: (argv, opts) => fn('gh', argv, opts) }`.
378
+
307
379
  ## `AsyncGh`
308
380
 
309
381
  The awaitable form of `Gh`, with the same methods, for a fan-out that must not block the event loop.
310
- Same tri-modal contract (queries resolve to a value or `null` and never
311
- reject, mutations reject with `CommandFailed`, `raw`/`rawChecked` are the
312
- hatches), driven by the same verb table, so the two cannot drift. It takes an
382
+ Same three doors, promised (the plain method rejects with the classified failure,
383
+ `try` resolves to a `Result<T, GhFailure>`, `soft` resolves `null` for a failed
384
+ query, `raw`/`rawChecked` are the hatches), driven by the same verb table, so the two cannot drift. It takes an
313
385
  `AsyncCommandRunner` and defaults to `spawnRunnerAsync("gh")`.
314
386
 
315
387
  ```ts
@@ -317,7 +389,7 @@ import { AsyncGh } from '@osdlc/typed-gh'
317
389
 
318
390
  const gh = new AsyncGh('/repo')
319
391
  const prs = await Promise.all(branches.map((b) => gh.openPrFor(b)))
320
- const body = await gh.prBody(42) // string | null
392
+ const body = await gh.prBody(42) // string; rejects when gh fails
321
393
  await gh.editPrBody(42, `${body}\n\nfooter`)
322
394
  await gh.prComment(42, 'rebased') // the comment's URL, or ''
323
395
  await gh.prUpdateBranch(42, 'rebase')
@@ -329,7 +401,8 @@ These methods take `AsyncCallOptions`: `raw`, `rawChecked`, `prList`, `prView`,
329
401
  `signal`.
330
402
 
331
403
  `api(endpoint, opts)` (on `Gh` and `AsyncGh`) is a REST GET through `gh api`.
332
- It never throws; the result is `{ ok, status, etag, body, malformed, stderr, error? }`.
404
+ It never throws; the result is `{ ok, status, etag, body, message, malformed, stderr, error? }`.
405
+ On failure `body` still carries GitHub's error object and `message` its text; `status` is only observed when headers were requested (`etag` or `includeHeaders`).
333
406
  `stderr` is gh's own; `error` holds the runner's message when gh never ran or
334
407
  was stopped (a missing binary, a timeout).
335
408
  Pass `etag` for a conditional request (`If-None-Match`; a 304 comes back as
@@ -346,8 +419,8 @@ without one. If `gh` prints more than one header block, the last one is read. `{
346
419
  const branches = gh.api('repos/{owner}/{repo}/branches', { paginate: true }).body
347
420
  ```
348
421
 
349
- `graphql` (experimental) runs a query and returns each page's `data`. It returns `null` when the
350
- call failed or any page has `errors` or no `data`: a partial answer is no answer. `nodesAt` joins
422
+ `graphql` (experimental) runs a query and returns each page's `data`. It throws when the
423
+ call failed or any page has `errors` or no `data` (`soft.graphql` is `null`): a partial answer is no answer. `nodesAt` joins
351
424
  one connection's `nodes` across the pages, and is `null` when any page lacks them.
352
425
 
353
426
  ```ts
@@ -369,9 +442,9 @@ one. `graphql` refuses a mutation with `RangeError`; send one through `raw`.
369
442
  ## Repositories and search
370
443
 
371
444
  ```ts
372
- const repos: ListResult<Repository> | null = gh.repoList('acme', { archived: false, limit: 200 })
373
- const repo: Repository | null = gh.repoView('acme/widgets')
374
- const hits: ListResult<PrSearchHit> | null = gh.searchPrs('TICKET-123', { owner: 'acme' })
445
+ const repos: ListResult<Repository> = gh.repoList('acme', { archived: false, limit: 200 })
446
+ const repo: Repository | null = gh.repoView('acme/widgets') // null: no such repository
447
+ const hits: ListResult<PrSearchHit> = gh.searchPrs('TICKET-123', { owner: 'acme' })
375
448
  ```
376
449
 
377
450
  A `Repository` carries `nameWithOwner`, `owner`, `name`, `description`, `defaultBranch`,
@@ -409,6 +482,8 @@ experimental.
409
482
  - `GhOptions` and `AsyncGhOptions`: the runner, `repo` and `hostname` a client is built with.
410
483
  - `AsyncCallOptions`: `timeoutMs` and `signal`, accepted by sixteen methods of `AsyncGh`.
411
484
  - `defaultRunnerFactory` and `RunnerFactory`: how a client makes its default runner for `gh`.
485
+ - `runVerb` and `runVerbAsync`, with `RunVerbOptions` and `RunVerbAsyncOptions`: run one verb through
486
+ a runner you own, with its `env`, `input`, timeout and checked contract applied.
412
487
 
413
488
  Methods on both clients:
414
489
 
@@ -422,9 +497,15 @@ Methods on both clients:
422
497
  - Issue and label queries: `issueList`, `issueView` and `labelList`.
423
498
  - Repository queries: `repoList` and `repoView`.
424
499
  - Issue and label changes: `setIssueLabel`, `createLabel`, `editLabel` and `issueCommentEdit`.
425
- - REST and GraphQL: `api` is a GET through `gh api` and `graphql` a query; neither throws.
500
+ - REST and GraphQL: `api` is a GET through `gh api` and never throws (failure is `ok: false`); `graphql` is a query and throws on a failed or partial answer.
426
501
  - Setup checks: `installed`, `authenticated`, `authStatus` and `ready`.
427
502
  - `ownerRepo` reads the owner and repository from a remote URL. It starts no process.
503
+ - `try` and `soft`: the whole surface again as a `Result<T, GhFailure>`, and the queries again with `null` for a failure. `TriedSync`, `TriedAsync` and `Soft` are their types.
504
+
505
+ ### Failures
506
+
507
+ - `GhFailure` and `GhFailureKind`: the error of `try`, and its kinds (see "`GhFailure`" above).
508
+ - `classifyGhFailure(commandFailed)`: the `GhFailure` for a `CommandFailed`.
428
509
 
429
510
  ### Verbs
430
511
 
@@ -473,7 +554,7 @@ the escape hatches, `at`, `withRepo`, `prViewSettled`, `openPrFor`, `prFind`, `a
473
554
 
474
555
  ### From the command seam
475
556
 
476
- - `CommandFailed`: thrown by a mutation. It carries the argv and the result.
557
+ - `CommandFailed`: re-exported from `@osdlc/command-seam`. A failed call throws a `FailureError` whose `failure.cause` is the `CommandFailed`, which carries the argv and the result.
477
558
  - `CommandRunner`, `AsyncCommandRunner`, `CommandResult` and `CommandRunOptions`: the runner types
478
559
  that appear in signatures.
479
560
 
@@ -514,4 +595,4 @@ shell, so nothing in it is specific to one shell.
514
595
 
515
596
  ## Licence
516
597
 
517
- `MIT OR Apache-2.0`, at your option. See `LICENSE-MIT` and `LICENSE-APACHE`.
598
+ `MIT OR Apache-2.0`, at your option. See [LICENSE-MIT](./LICENSE-MIT) and [LICENSE-APACHE](./LICENSE-APACHE).
package/dist/async.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * `AsyncGh` — the awaitable GitHub client, with the same methods as {@link Gh}.
3
3
  *
4
- * Same tri-modal contract, promised: queries resolve to parsed values and
5
- * never reject on a gh failure, mutations reject with `CommandFailed`, and
6
- * `raw`/`rawChecked` are the hatches. It is driven by the same pure verb
4
+ * Same three doors, promised: the plain method resolves to a value or rejects
5
+ * (a `FailureError` carrying the classified `GhFailure` for a failed gh or output it cannot read;
6
+ * a query for one thing resolves `null` when GitHub has no such thing),
7
+ * `try` resolves to a `Result<T, GhFailure>`, `soft` resolves `null` for a
8
+ * failure, and `raw`/`rawChecked` are the hatches. It is driven by the same pure verb
7
9
  * table as `Gh`, so the two cannot drift on argv or parsing. Use it where the
8
10
  * sync client would block the event loop: a fan-out of per-PR queries.
9
11
  */
10
12
  import { type AsyncCommandRunner, type CommandResult } from '@osdlc/command-seam';
13
+ import { type Soft, type TriedAsync } from './doors.ts';
11
14
  import type { IssueRow, Label, OwnerRepo, PrSearchHit, PullRequest, Repository } from './types.ts';
12
15
  import * as v from './verbs.ts';
13
16
  import type { ApiOptions, ApiResponse, AuthStatus, GhTarget, IssueListOptions, ListResult, PrListOptions } from './verbs.ts';
@@ -40,19 +43,31 @@ export declare class AsyncGh {
40
43
  readonly hostname: string | undefined;
41
44
  private readonly target;
42
45
  private readonly runner;
46
+ private tried;
47
+ private softened;
43
48
  constructor(cwd: string, opts?: AsyncGhOptions);
44
49
  /** The same client rooted at a different directory (same runner, repo and host). */
45
50
  at(cwd: string): AsyncGh;
46
51
  /** The same client aimed at another repository (same runner, directory and host). */
47
52
  withRepo(repo: string): AsyncGh;
53
+ /**
54
+ * The whole surface again, resolving to a `Result<T, GhFailure>` instead of
55
+ * rejecting. A query for one thing that finds nothing is `ok: true` with
56
+ * `null`. See `Gh.try`.
57
+ */
58
+ get try(): TriedAsync<AsyncGh>;
59
+ /** Each query again, resolving `null` instead of rejecting when `gh` fails. See `Gh.soft`. */
60
+ get soft(): Soft<AsyncGh>;
48
61
  /** Unchecked passthrough for verbs not yet lifted into the API. */
49
62
  raw(argv: string[], opts?: AsyncCallOptions): Promise<CommandResult>;
50
- /** Checked passthrough — rejects with `CommandFailed` on a non-zero exit. */
63
+ /** Checked passthrough — rejects with the classified failure on a non-zero exit. */
51
64
  rawChecked(argv: string[], opts?: AsyncCallOptions): Promise<CommandResult>;
52
65
  /** Run one verb from the table, honouring its checked/unchecked contract. */
53
66
  private exec;
54
- /** Pull requests matching `opts`, or null when gh failed. */
55
- prList(opts?: PrListOptions & AsyncCallOptions): Promise<ListResult<PullRequest> | null>;
67
+ /** {@link exec} for a verb that never answers `null` as a value: a failed or unreadable run rejects. */
68
+ private read;
69
+ /** Pull requests matching `opts`. Rejects when gh fails; `soft.prList` resolves null instead. */
70
+ prList(opts?: PrListOptions & AsyncCallOptions): Promise<ListResult<PullRequest>>;
56
71
  /** One PR by number, branch or URL, with the heavy fields, or null. See `Gh.prView`. */
57
72
  prView(ref: number | string, opts?: {
58
73
  fields?: readonly string[] | undefined;
@@ -72,32 +87,32 @@ export declare class AsyncGh {
72
87
  fields?: readonly string[] | undefined;
73
88
  sleep?: ((ms: number) => Promise<void>) | undefined;
74
89
  } & AsyncCallOptions): Promise<PullRequest | null>;
75
- /** A PR's body text, or null when gh failed. */
76
- prBody(number: number, opts?: AsyncCallOptions): Promise<string | null>;
90
+ /** A PR's body text. Rejects when gh fails. */
91
+ prBody(number: number, opts?: AsyncCallOptions): Promise<string>;
77
92
  /** A REST GET through `gh api`; failure is `ok: false`, never a rejection. */
78
93
  api(endpoint: string, opts?: ApiOptions & AsyncCallOptions): Promise<ApiResponse>;
79
94
  /**
80
- * A GraphQL query; each page's `data`, or null. See `Gh.graphql`.
95
+ * A GraphQL query; each page's `data`. Rejects on a failed or partial result. See `Gh.graphql`.
81
96
  *
82
97
  * @experimental
83
98
  */
84
- graphql(query: string, variables?: Record<string, v.GraphqlVariable>, opts?: v.GraphqlOptions & AsyncCallOptions): Promise<unknown[] | null>;
85
- /** Every label on the repository (up to `opts.limit`, default 1000), or null when gh failed. */
99
+ graphql(query: string, variables?: Record<string, v.GraphqlVariable>, opts?: v.GraphqlOptions & AsyncCallOptions): Promise<unknown[]>;
100
+ /** Every label on the repository (up to `opts.limit`, default 1000). Rejects when gh fails. */
86
101
  labelList(opts?: {
87
102
  limit?: number | undefined;
88
- } & AsyncCallOptions): Promise<ListResult<Label> | null>;
89
- /** Issues matching `opts`, or null when gh failed. */
90
- issueList(opts?: IssueListOptions & AsyncCallOptions): Promise<ListResult<IssueRow> | null>;
91
- /** One issue with its labels, or null when gh failed. */
103
+ } & AsyncCallOptions): Promise<ListResult<Label>>;
104
+ /** Issues matching `opts`. Rejects when gh fails. */
105
+ issueList(opts?: IssueListOptions & AsyncCallOptions): Promise<ListResult<IssueRow>>;
106
+ /** One issue with its labels; null when there is no such issue. */
92
107
  issueView(number: number, opts?: AsyncCallOptions): Promise<IssueRow | null>;
93
- /** An owner's repositories, or null when gh failed. See `Gh.repoList`. */
94
- repoList(owner: string, opts?: v.RepoListOptions & AsyncCallOptions): Promise<ListResult<Repository> | null>;
95
- /** One repository, or null when gh failed. See `Gh.repoView`. */
108
+ /** An owner's repositories. Rejects when gh fails. See `Gh.repoList`. */
109
+ repoList(owner: string, opts?: v.RepoListOptions & AsyncCallOptions): Promise<ListResult<Repository>>;
110
+ /** One repository; null when there is no such repository. See `Gh.repoView`. */
96
111
  repoView(repo?: string | undefined, opts?: {
97
112
  fields?: readonly string[] | undefined;
98
113
  } & AsyncCallOptions): Promise<Repository | null>;
99
- /** Pull requests across repositories matching a search query, or null. See `Gh.searchPrs`. */
100
- searchPrs(query: string, opts?: v.SearchPrsOptions & AsyncCallOptions): Promise<ListResult<PrSearchHit> | null>;
114
+ /** Pull requests across repositories matching a search query. Rejects when gh fails. See `Gh.searchPrs`. */
115
+ searchPrs(query: string, opts?: v.SearchPrsOptions & AsyncCallOptions): Promise<ListResult<PrSearchHit>>;
101
116
  /** True iff the `gh` binary is on PATH and runs. */
102
117
  installed(): Promise<boolean>;
103
118
  /** True iff `gh` holds credentials. */
@@ -120,7 +135,7 @@ export declare class AsyncGh {
120
135
  }): Promise<void>;
121
136
  /** Rewrite just a PR's body. */
122
137
  editPrBody(number: number, body: string): Promise<void>;
123
- /** The PR's unified diff; rejects with `CommandFailed` when gh cannot produce it. */
138
+ /** The PR's unified diff; rejects with the classified failure when gh cannot produce it. */
124
139
  prDiff(number: number): Promise<string>;
125
140
  /** Post one conversation comment on a PR; resolves to its URL ('' if gh printed none). */
126
141
  prComment(number: number, body: string): Promise<string>;
@@ -146,7 +161,7 @@ export declare class AsyncGh {
146
161
  headSha: string;
147
162
  }): Promise<void>;
148
163
  /** A PR's review threads, or null when they could not be read completely. */
149
- prReviewThreads(number: number): Promise<v.ReviewThread[] | null>;
164
+ prReviewThreads(number: number): Promise<v.ReviewThread[]>;
150
165
  /** Create a label (or force it to `opts`'s color/description if it exists). */
151
166
  createLabel(opts: v.CreateLabelOptions): Promise<void>;
152
167
  /** Recolor and/or redescribe an existing label. */
@@ -157,4 +172,3 @@ export declare class AsyncGh {
157
172
  /** Swap an issue's label in a single `gh issue edit` call. */
158
173
  setIssueLabel(number: number, currentLabel: string | null, newLabel: string): Promise<void>;
159
174
  }
160
- //# sourceMappingURL=async.d.ts.map