@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 +56 -1
- package/CONTRIBUTING.md +4 -3
- package/README.md +117 -36
- package/dist/async.d.ts +37 -23
- package/dist/async.js +52 -38
- package/dist/doors.d.ts +82 -0
- package/dist/doors.js +183 -0
- package/dist/failure.d.ts +59 -0
- package/dist/failure.js +93 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +8 -2
- package/dist/parse.d.ts +0 -1
- package/dist/repo.d.ts +0 -1
- package/dist/run.d.ts +37 -0
- package/dist/run.js +49 -0
- package/dist/sync.d.ts +53 -29
- package/dist/sync.js +64 -42
- package/dist/types.d.ts +0 -1
- package/dist/verbs.d.ts +25 -8
- package/dist/verbs.js +34 -8
- package/package.json +13 -17
- package/async.ts +0 -379
- package/dist/async.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/parse.d.ts.map +0 -1
- package/dist/repo.d.ts.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/verbs.d.ts.map +0 -1
- package/index.ts +0 -120
- package/parse.ts +0 -302
- package/repo.ts +0 -133
- package/sync.ts +0 -438
- package/types.ts +0 -226
- package/verbs.ts +0 -1324
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
93
|
+
}): ListResult<PullRequest>;
|
|
69
94
|
/**
|
|
70
|
-
* One PR by number, branch or URL, with the heavy fields; null when
|
|
71
|
-
*
|
|
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
|
|
147
|
+
/** A PR's body text. Throws when gh fails. */
|
|
123
148
|
prBody(number: number, opts?: {
|
|
124
149
|
timeoutMs?: number | undefined;
|
|
125
|
-
}): string
|
|
150
|
+
}): string;
|
|
126
151
|
/**
|
|
127
|
-
* A REST GET through `gh api`. Never throws
|
|
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
|
|
135
|
-
*
|
|
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[]
|
|
142
|
-
/** Every label on the repository (up to `opts.limit`, default 1000)
|
|
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
|
|
147
|
-
/** Issues matching `opts` (open by default, up to 200)
|
|
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
|
|
151
|
-
/** One issue with its labels
|
|
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)
|
|
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
|
|
159
|
-
/** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when
|
|
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
|
|
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
|
|
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
|
|
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
|
|
227
|
-
prReviewThreads(number: number): v.ReviewThread[]
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 {
|
|
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
|
|
90
|
+
/** Checked passthrough — throws the classified failure on a non-zero exit. */
|
|
63
91
|
rawChecked(argv, opts = {}) {
|
|
64
|
-
return
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
103
|
+
/** Pull requests matching `opts`. Throws when gh fails; `soft.prList` gives null instead. */
|
|
82
104
|
prList(opts = {}) {
|
|
83
|
-
return this.
|
|
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
|
|
87
|
-
*
|
|
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
|
|
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
|
|
180
|
+
/** A PR's body text. Throws when gh fails. */
|
|
159
181
|
prBody(number, opts = {}) {
|
|
160
|
-
return this.
|
|
182
|
+
return this.read(v.prBody(number, this.target), opts.timeoutMs);
|
|
161
183
|
}
|
|
162
184
|
/**
|
|
163
|
-
* A REST GET through `gh api`. Never throws
|
|
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
|
|
171
|
-
*
|
|
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.
|
|
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)
|
|
200
|
+
/** Every label on the repository (up to `opts.limit`, default 1000). Throws when gh fails. */
|
|
179
201
|
labelList(opts = {}) {
|
|
180
|
-
return this.
|
|
202
|
+
return this.read(v.labelList(opts, this.target), opts.timeoutMs);
|
|
181
203
|
}
|
|
182
|
-
/** Issues matching `opts` (open by default, up to 200)
|
|
204
|
+
/** Issues matching `opts` (open by default, up to 200). Throws when gh fails. */
|
|
183
205
|
issueList(opts = {}) {
|
|
184
|
-
return this.
|
|
206
|
+
return this.read(v.issueList(opts, this.target), opts.timeoutMs);
|
|
185
207
|
}
|
|
186
|
-
/** One issue with its labels
|
|
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)
|
|
212
|
+
/** An owner's repositories (up to `opts.limit`, default 100). Throws when gh fails. */
|
|
191
213
|
repoList(owner, opts = {}) {
|
|
192
|
-
return this.
|
|
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
|
|
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
|
|
220
|
+
/** Pull requests across repositories matching a search query. Throws when gh fails. */
|
|
199
221
|
searchPrs(query, opts = {}) {
|
|
200
|
-
return this.
|
|
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
|
|
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
|
|
307
|
+
/** A PR's review threads. Throws when they could not be read completely. */
|
|
286
308
|
prReviewThreads(number) {
|
|
287
|
-
return this.
|
|
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
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
|
|
11
|
-
* and `prDiff`, which is a read. Other queries are
|
|
12
|
-
* into
|
|
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;
|
|
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)
|
|
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
|
-
/**
|
|
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
|
|
11
|
-
* and `prDiff`, which is a read. Other queries are
|
|
12
|
-
* into
|
|
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
|
-
//
|
|
247
|
-
|
|
248
|
-
const asked = opts.
|
|
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 (
|
|
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 {
|
|
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;
|