@osdlc/typed-gh 0.7.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,5 +1,24 @@
1
1
  # @osdlc/typed-gh
2
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
+
3
22
  ## 0.7.0
4
23
 
5
24
  ### Minor Changes
package/README.md CHANGED
@@ -357,6 +357,25 @@ const result = await runner.run(verb.argv, { cwd: '/repo', timeoutMs: 5_000 })
357
357
  const prs = verb.parse(result)
358
358
  ```
359
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
+
360
379
  ## `AsyncGh`
361
380
 
362
381
  The awaitable form of `Gh`, with the same methods, for a fan-out that must not block the event loop.
@@ -382,7 +401,8 @@ These methods take `AsyncCallOptions`: `raw`, `rawChecked`, `prList`, `prView`,
382
401
  `signal`.
383
402
 
384
403
  `api(endpoint, opts)` (on `Gh` and `AsyncGh`) is a REST GET through `gh api`.
385
- 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`).
386
406
  `stderr` is gh's own; `error` holds the runner's message when gh never ran or
387
407
  was stopped (a missing binary, a timeout).
388
408
  Pass `etag` for a conditional request (`If-None-Match`; a 304 comes back as
@@ -462,6 +482,8 @@ experimental.
462
482
  - `GhOptions` and `AsyncGhOptions`: the runner, `repo` and `hostname` a client is built with.
463
483
  - `AsyncCallOptions`: `timeoutMs` and `signal`, accepted by sixteen methods of `AsyncGh`.
464
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.
465
487
 
466
488
  Methods on both clients:
467
489
 
package/dist/async.js CHANGED
@@ -10,7 +10,8 @@
10
10
  * sync client would block the event loop: a fan-out of per-PR queries.
11
11
  */
12
12
  import { resolveRunnerAsync, } from '@osdlc/command-seam';
13
- import { runCheckedGhAsync, settle, softAsync, triedAsync, } from "./doors.js";
13
+ import { runCheckedGhAsync, softAsync, triedAsync } from "./doors.js";
14
+ import { runVerbAsync } from "./run.js";
14
15
  import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from "./repo.js";
15
16
  import * as v from "./verbs.js";
16
17
  /** How long to wait between merge-state retries, in milliseconds. */
@@ -66,19 +67,11 @@ export class AsyncGh {
66
67
  }
67
68
  /** Run one verb from the table, honouring its checked/unchecked contract. */
68
69
  async exec(verb, call = {}) {
69
- const opts = { cwd: this.cwd };
70
- if (verb.input !== undefined)
71
- opts.input = verb.input;
72
- if (verb.env !== undefined)
73
- opts.env = verb.env;
74
- if (call.timeoutMs !== undefined)
75
- opts.timeoutMs = call.timeoutMs;
76
- if (call.signal !== undefined)
77
- opts.signal = call.signal;
78
- const result = verb.checked
79
- ? await runCheckedGhAsync(this.runner, verb.argv, opts)
80
- : await this.runner.run(verb.argv, opts);
81
- return settle(verb, result, this.runner.binary);
70
+ return runVerbAsync(verb, this.runner, {
71
+ cwd: this.cwd,
72
+ timeoutMs: call.timeoutMs,
73
+ signal: call.signal,
74
+ });
82
75
  }
83
76
  /** {@link exec} for a verb that never answers `null` as a value: a failed or unreadable run rejects. */
84
77
  async read(verb, call = {}) {
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  export { AsyncGh, type AsyncCallOptions, type AsyncGhOptions } from './async.ts';
23
23
  export { classifyGhFailure, type GhFailure, type GhFailureKind } from './failure.ts';
24
24
  export { type Soft, type TriedAsync, type TriedSync } from './doors.ts';
25
+ export { runVerb, runVerbAsync, type RunVerbAsyncOptions, type RunVerbOptions } from './run.ts';
25
26
  export { Gh, defaultRunnerFactory, type GhOptions, type RunnerFactory } from './sync.ts';
26
27
  export { api, authStatus, createPr, editPr, editPrBody, graphql, createLabel, createReview, editLabel, installed, issueList, issueView, labelList, nodesAt, openPrsForBranch, pickPrForBranch, prBody, prComment, prSetDraft, issueCommentEdit, prDiff, prList, prMerge, prReviewThreads, prUpdateBranch, prView, repoList, repoView, searchPrs, setIssueLabel, PR_LIST_FIELDS, PR_REF_FIELDS, PR_VIEW_FIELDS, REPO_FIELDS, SEARCH_PR_FIELDS, type ApiOptions, type ApiResponse, type AuthStatus, type CreateLabelOptions, type CreatePrOptions, type CreateReviewOptions, type GhTarget, type GraphqlOptions, type GraphqlVariable, type GhVerb, type IssueListOptions, type ListResult, type PrListOptions, type PrMergeMethod, type RepoListOptions, type ReviewComment, type ReviewThread, type SearchPrsOptions, } from './verbs.ts';
27
28
  export { anyCheckFailed, normalizeAuthorLogin, parsePullRequest } from './parse.ts';
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@
22
22
  export { AsyncGh } from "./async.js";
23
23
  export { classifyGhFailure } from "./failure.js";
24
24
  export {} from "./doors.js";
25
+ export { runVerb, runVerbAsync } from "./run.js";
25
26
  export { Gh, defaultRunnerFactory } from "./sync.js";
26
27
  export { api, authStatus, createPr, editPr, editPrBody, graphql, createLabel, createReview, editLabel, installed, issueList, issueView, labelList, nodesAt, openPrsForBranch, pickPrForBranch, prBody, prComment, prSetDraft, issueCommentEdit, prDiff, prList, prMerge, prReviewThreads, prUpdateBranch, prView, repoList, repoView, searchPrs, setIssueLabel, PR_LIST_FIELDS, PR_REF_FIELDS, PR_VIEW_FIELDS, REPO_FIELDS, SEARCH_PR_FIELDS, } from "./verbs.js";
27
28
  export { anyCheckFailed, normalizeAuthorLogin, parsePullRequest } from "./parse.js";
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.js CHANGED
@@ -15,7 +15,8 @@
15
15
  * `null` for it. `raw`/`rawChecked` are the escape hatches.
16
16
  */
17
17
  import { resolveRunner, spawnRunner, } from '@osdlc/command-seam';
18
- import { runCheckedGh, settle, softSync, triedSync } from "./doors.js";
18
+ import { runCheckedGh, softSync, triedSync } from "./doors.js";
19
+ import { runVerb } from "./run.js";
19
20
  import * as v from "./verbs.js";
20
21
  import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from "./repo.js";
21
22
  /** Default factory: real, spawnSync-backed runners. */
@@ -92,17 +93,7 @@ export class Gh {
92
93
  }
93
94
  /** Run one verb from the table, honouring its checked/unchecked contract. */
94
95
  exec(verb, timeoutMs) {
95
- const opts = { cwd: this.cwd };
96
- if (verb.input !== undefined)
97
- opts.input = verb.input;
98
- if (verb.env !== undefined)
99
- opts.env = verb.env;
100
- if (timeoutMs !== undefined)
101
- opts.timeoutMs = timeoutMs;
102
- const result = verb.checked
103
- ? runCheckedGh(this.runner, verb.argv, opts)
104
- : this.runner.run(verb.argv, opts);
105
- return settle(verb, result, this.runner.binary);
96
+ return runVerb(verb, this.runner, { cwd: this.cwd, timeoutMs });
106
97
  }
107
98
  /** {@link exec} for a verb that never answers `null` as a value: a failed or unreadable run throws. */
108
99
  read(verb, timeoutMs) {
package/dist/verbs.d.ts CHANGED
@@ -120,7 +120,7 @@ export interface PrListOptions {
120
120
  search?: string | undefined;
121
121
  author?: string | undefined;
122
122
  limit?: number | undefined;
123
- /** Field subset; defaults to {@link PR_LIST_FIELDS}. */
123
+ /** Field subset; defaults to {@link PR_LIST_FIELDS}. `number` is always requested. */
124
124
  fields?: readonly string[] | undefined;
125
125
  }
126
126
  /** `gh pr list …` — open PRs by default. `opts.repo` overrides the target's repo. */
@@ -153,13 +153,15 @@ export declare function prBody(number: number, target?: GhTarget): GhVerb<string
153
153
  export interface ApiResponse {
154
154
  /** True iff gh exited 0 and the status is below 400 (2xx, or a 304 on a conditional GET). */
155
155
  ok: boolean;
156
- /** 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. */
157
157
  status: number;
158
158
  /** The response `ETag`, when headers were requested and one came back. */
159
159
  etag: string | null;
160
- /** 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. */
161
161
  body: unknown;
162
- /** 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. */
163
165
  malformed: boolean;
164
166
  /** gh's stderr, trimmed. */
165
167
  stderr: string;
package/dist/verbs.js CHANGED
@@ -251,9 +251,12 @@ export function prList(opts = {}, target) {
251
251
  argv.push('--search', opts.search);
252
252
  if (opts.author !== undefined)
253
253
  argv.push('--author', opts.author);
254
- // The exact-head filter below reads `headRefName`, so ask for it.
255
- const fields = opts.fields ?? PR_LIST_FIELDS;
256
- 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
+ ]);
257
260
  argv.push('--json', asked.join(','));
258
261
  argv.push('--limit', String(limit + 1));
259
262
  const head = opts.head;
@@ -516,7 +519,7 @@ function parseApi(r, withHeaders) {
516
519
  }
517
520
  let body = null;
518
521
  let malformed = false;
519
- if (r.exitCode === 0 && bodyText.trim() !== '') {
522
+ if (bodyText.trim() !== '') {
520
523
  try {
521
524
  body = JSON.parse(bodyText);
522
525
  }
@@ -525,11 +528,15 @@ function parseApi(r, withHeaders) {
525
528
  }
526
529
  }
527
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;
528
534
  return {
529
535
  ok,
530
536
  status,
531
537
  etag,
532
538
  body,
539
+ message: typeof rawMessage === 'string' ? rawMessage : null,
533
540
  malformed,
534
541
  stderr: r.stderr.trim(),
535
542
  ...(r.error === undefined ? {} : { error: r.error }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osdlc/typed-gh",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "A typed GitHub client for TypeScript over the gh CLI. A total PullRequest/Review/CheckRun model, pure argv builders and parsers, and a tri-modal client — queries return parsed values, mutations throw, raw() is the escape hatch. Uses gh's own credentials; reads no token.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "sksizer",
@@ -22,8 +22,8 @@
22
22
  "LICENSE-APACHE"
23
23
  ],
24
24
  "dependencies": {
25
- "@osdlc/command-seam": "^0.9.0",
26
- "@osdlc/typed-git": "^0.6.0",
25
+ "@osdlc/command-seam": "^0.9.1",
26
+ "@osdlc/typed-git": "^0.7.0",
27
27
  "@osdlc/result": "^0.1.0"
28
28
  },
29
29
  "engines": {