@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/async.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `AsyncGh` — the awaitable GitHub client, with the same methods as {@link Gh}.
|
|
3
3
|
*
|
|
4
|
-
* Same
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
import {
|
|
12
|
+
import { resolveRunnerAsync, } from '@osdlc/command-seam';
|
|
13
|
+
import { runCheckedGhAsync, softAsync, triedAsync } from "./doors.js";
|
|
14
|
+
import { runVerbAsync } from "./run.js";
|
|
11
15
|
import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from "./repo.js";
|
|
12
16
|
import * as v from "./verbs.js";
|
|
13
17
|
/** How long to wait between merge-state retries, in milliseconds. */
|
|
@@ -22,6 +26,8 @@ export class AsyncGh {
|
|
|
22
26
|
hostname;
|
|
23
27
|
target;
|
|
24
28
|
runner;
|
|
29
|
+
tried;
|
|
30
|
+
softened;
|
|
25
31
|
constructor(cwd, opts = {}) {
|
|
26
32
|
v.checkTarget(opts);
|
|
27
33
|
this.cwd = cwd;
|
|
@@ -38,35 +44,43 @@ export class AsyncGh {
|
|
|
38
44
|
withRepo(repo) {
|
|
39
45
|
return new AsyncGh(this.cwd, { runner: this.runner, repo, hostname: this.hostname });
|
|
40
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The whole surface again, resolving to a `Result<T, GhFailure>` instead of
|
|
49
|
+
* rejecting. A query for one thing that finds nothing is `ok: true` with
|
|
50
|
+
* `null`. See `Gh.try`.
|
|
51
|
+
*/
|
|
52
|
+
get try() {
|
|
53
|
+
return (this.tried ??= triedAsync(this));
|
|
54
|
+
}
|
|
55
|
+
/** Each query again, resolving `null` instead of rejecting when `gh` fails. See `Gh.soft`. */
|
|
56
|
+
get soft() {
|
|
57
|
+
return (this.softened ??= softAsync(this));
|
|
58
|
+
}
|
|
41
59
|
// ── escape hatches ────────────────────────────────────────────────────────
|
|
42
60
|
/** Unchecked passthrough for verbs not yet lifted into the API. */
|
|
43
61
|
raw(argv, opts = {}) {
|
|
44
62
|
return this.runner.run(argv, { cwd: this.cwd, ...opts });
|
|
45
63
|
}
|
|
46
|
-
/** Checked passthrough — rejects with
|
|
64
|
+
/** Checked passthrough — rejects with the classified failure on a non-zero exit. */
|
|
47
65
|
rawChecked(argv, opts = {}) {
|
|
48
|
-
return
|
|
66
|
+
return runCheckedGhAsync(this.runner, argv, { cwd: this.cwd, ...opts });
|
|
49
67
|
}
|
|
50
68
|
/** Run one verb from the table, honouring its checked/unchecked contract. */
|
|
51
69
|
async exec(verb, call = {}) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const result = verb.checked
|
|
62
|
-
? await runCheckedAsync(this.runner, verb.argv, opts)
|
|
63
|
-
: await this.runner.run(verb.argv, opts);
|
|
64
|
-
return verb.parse(result);
|
|
70
|
+
return runVerbAsync(verb, this.runner, {
|
|
71
|
+
cwd: this.cwd,
|
|
72
|
+
timeoutMs: call.timeoutMs,
|
|
73
|
+
signal: call.signal,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** {@link exec} for a verb that never answers `null` as a value: a failed or unreadable run rejects. */
|
|
77
|
+
async read(verb, call = {}) {
|
|
78
|
+
return (await this.exec(verb, call));
|
|
65
79
|
}
|
|
66
80
|
// ── queries ───────────────────────────────────────────────────────────────
|
|
67
|
-
/** Pull requests matching `opts
|
|
81
|
+
/** Pull requests matching `opts`. Rejects when gh fails; `soft.prList` resolves null instead. */
|
|
68
82
|
prList(opts = {}) {
|
|
69
|
-
return this.
|
|
83
|
+
return this.read(v.prList(opts, this.target), opts);
|
|
70
84
|
}
|
|
71
85
|
/** One PR by number, branch or URL, with the heavy fields, or null. See `Gh.prView`. */
|
|
72
86
|
prView(ref, opts = {}) {
|
|
@@ -95,7 +109,7 @@ export class AsyncGh {
|
|
|
95
109
|
timeoutMs: opts.timeoutMs,
|
|
96
110
|
signal: opts.signal,
|
|
97
111
|
});
|
|
98
|
-
return v.pickPrForBranch(rows
|
|
112
|
+
return v.pickPrForBranch(rows.rows, branch);
|
|
99
113
|
}
|
|
100
114
|
/** {@link prView}, re-asked until GitHub settles the merge state. See `Gh.prViewSettled`. */
|
|
101
115
|
async prViewSettled(number, opts = {}) {
|
|
@@ -111,45 +125,45 @@ export class AsyncGh {
|
|
|
111
125
|
}
|
|
112
126
|
return last;
|
|
113
127
|
}
|
|
114
|
-
/** A PR's body text
|
|
128
|
+
/** A PR's body text. Rejects when gh fails. */
|
|
115
129
|
prBody(number, opts = {}) {
|
|
116
|
-
return this.
|
|
130
|
+
return this.read(v.prBody(number, this.target), opts);
|
|
117
131
|
}
|
|
118
132
|
/** A REST GET through `gh api`; failure is `ok: false`, never a rejection. */
|
|
119
133
|
api(endpoint, opts = {}) {
|
|
120
134
|
return this.exec(v.api(endpoint, opts, this.target), opts);
|
|
121
135
|
}
|
|
122
136
|
/**
|
|
123
|
-
* A GraphQL query; each page's `data
|
|
137
|
+
* A GraphQL query; each page's `data`. Rejects on a failed or partial result. See `Gh.graphql`.
|
|
124
138
|
*
|
|
125
139
|
* @experimental
|
|
126
140
|
*/
|
|
127
141
|
graphql(query, variables = {}, opts = {}) {
|
|
128
|
-
return this.
|
|
142
|
+
return this.read(v.graphql(query, variables, opts, this.target), opts);
|
|
129
143
|
}
|
|
130
|
-
/** Every label on the repository (up to `opts.limit`, default 1000)
|
|
144
|
+
/** Every label on the repository (up to `opts.limit`, default 1000). Rejects when gh fails. */
|
|
131
145
|
labelList(opts = {}) {
|
|
132
|
-
return this.
|
|
146
|
+
return this.read(v.labelList(opts, this.target), opts);
|
|
133
147
|
}
|
|
134
|
-
/** Issues matching `opts
|
|
148
|
+
/** Issues matching `opts`. Rejects when gh fails. */
|
|
135
149
|
issueList(opts = {}) {
|
|
136
|
-
return this.
|
|
150
|
+
return this.read(v.issueList(opts, this.target), opts);
|
|
137
151
|
}
|
|
138
|
-
/** One issue with its labels
|
|
152
|
+
/** One issue with its labels; null when there is no such issue. */
|
|
139
153
|
issueView(number, opts = {}) {
|
|
140
154
|
return this.exec(v.issueView(number, this.target), opts);
|
|
141
155
|
}
|
|
142
|
-
/** An owner's repositories
|
|
156
|
+
/** An owner's repositories. Rejects when gh fails. See `Gh.repoList`. */
|
|
143
157
|
repoList(owner, opts = {}) {
|
|
144
|
-
return this.
|
|
158
|
+
return this.read(v.repoList(owner, opts, this.target), opts);
|
|
145
159
|
}
|
|
146
|
-
/** One repository
|
|
160
|
+
/** One repository; null when there is no such repository. See `Gh.repoView`. */
|
|
147
161
|
repoView(repo, opts = {}) {
|
|
148
162
|
return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts);
|
|
149
163
|
}
|
|
150
|
-
/** Pull requests across repositories matching a search query
|
|
164
|
+
/** Pull requests across repositories matching a search query. Rejects when gh fails. See `Gh.searchPrs`. */
|
|
151
165
|
searchPrs(query, opts = {}) {
|
|
152
|
-
return this.
|
|
166
|
+
return this.read(v.searchPrs(query, opts, this.target), opts);
|
|
153
167
|
}
|
|
154
168
|
/** True iff the `gh` binary is on PATH and runs. */
|
|
155
169
|
installed() {
|
|
@@ -187,7 +201,7 @@ export class AsyncGh {
|
|
|
187
201
|
editPrBody(number, body) {
|
|
188
202
|
return this.exec(v.editPrBody(number, body, this.target));
|
|
189
203
|
}
|
|
190
|
-
/** The PR's unified diff; rejects with
|
|
204
|
+
/** The PR's unified diff; rejects with the classified failure when gh cannot produce it. */
|
|
191
205
|
prDiff(number) {
|
|
192
206
|
return this.exec(v.prDiff(number, this.target));
|
|
193
207
|
}
|
|
@@ -226,7 +240,7 @@ export class AsyncGh {
|
|
|
226
240
|
}
|
|
227
241
|
/** A PR's review threads, or null when they could not be read completely. */
|
|
228
242
|
prReviewThreads(number) {
|
|
229
|
-
return this.
|
|
243
|
+
return this.read(v.prReviewThreads(number, this.target));
|
|
230
244
|
}
|
|
231
245
|
/** Create a label (or force it to `opts`'s color/description if it exists). */
|
|
232
246
|
createLabel(opts) {
|
package/dist/doors.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three doors, as `@osdlc/typed-git` has them: the plain verb (checked),
|
|
3
|
+
* `gh.try.*` and `gh.soft.*`.
|
|
4
|
+
*
|
|
5
|
+
* - **Plain / checked**: a value, or a throw. A failed `gh` throws a
|
|
6
|
+
* `FailureError` carrying the classified `GhFailure` (its `cause` is the
|
|
7
|
+
* `CommandFailed`), so a caller that reports it, such as `runMain`, reads
|
|
8
|
+
* "signed out" or "rate limited" and not a bare exit; output that exited 0
|
|
9
|
+
* but could not be read throws one of kind `unreadable`. A verb that
|
|
10
|
+
* reads one thing (`prView`, `issueView`, `repoView`) answers `null` when
|
|
11
|
+
* GitHub says there is no such thing: absence is an answer.
|
|
12
|
+
* - **`try`**: the same call, with the failure returned as
|
|
13
|
+
* `Result<T, GhFailure>`. It catches a thrown failure (a `FailureError`, or a
|
|
14
|
+
* `CommandFailed` from `raw`-level code) and nothing else: a `RangeError` for a bad argument still throws.
|
|
15
|
+
* - **`soft`**: the same call, with `null` for a failure. Only the queries
|
|
16
|
+
* have a soft form; no mutation does.
|
|
17
|
+
*
|
|
18
|
+
* Both are facades over the whole client, so a new method costs nothing here.
|
|
19
|
+
*/
|
|
20
|
+
import { type AsyncCommandRunner, type AsyncCommandRunOptions, type CommandResult, type CommandRunner, type CommandRunOptions } from '@osdlc/command-seam';
|
|
21
|
+
import { type Failure, type Result } from '@osdlc/result';
|
|
22
|
+
import { type GhFailure } from './failure.ts';
|
|
23
|
+
import type { GhVerb } from './verbs.ts';
|
|
24
|
+
/**
|
|
25
|
+
* Read the result of running `verb` as the plain door does.
|
|
26
|
+
*
|
|
27
|
+
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
28
|
+
* `runChecked`), and a probe (`installed`, `authStatus`, `api`) carries its
|
|
29
|
+
* failure in its value. A read that exited non-zero throws `CommandFailed`,
|
|
30
|
+
* unless the verb's `absent` says gh's answer was "there is no such thing",
|
|
31
|
+
* which `parse` turns into `null`. A read that exited 0 but parsed to `null`
|
|
32
|
+
* could not be read completely, and throws.
|
|
33
|
+
*
|
|
34
|
+
* @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
|
|
35
|
+
* @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
|
|
36
|
+
*/
|
|
37
|
+
export declare function settle<T>(verb: GhVerb<T>, result: CommandResult, binary: string | undefined): T;
|
|
38
|
+
/**
|
|
39
|
+
* The error the plain door throws for a failed run: a `FailureError` carrying
|
|
40
|
+
* the {@link GhFailure} that {@link classifyGhFailure} reads from it, with the
|
|
41
|
+
* `CommandFailed` as its `cause`. Anything else is returned as it is.
|
|
42
|
+
*/
|
|
43
|
+
export declare function ghFailureError(error: unknown): unknown;
|
|
44
|
+
/** {@link runChecked}, throwing {@link ghFailureError}'s classified failure. */
|
|
45
|
+
export declare function runCheckedGh(runner: CommandRunner, argv: string[], opts: CommandRunOptions): CommandResult;
|
|
46
|
+
/** {@link runCheckedAsync}, rejecting with {@link ghFailureError}'s classified failure. */
|
|
47
|
+
export declare function runCheckedGhAsync(runner: AsyncCommandRunner, argv: string[], opts: AsyncCommandRunOptions): Promise<CommandResult>;
|
|
48
|
+
/** The methods the doors leave out: the escape hatches, pure helpers and plain properties. */
|
|
49
|
+
type Triable<C> = Omit<C, 'raw' | 'rawChecked' | 'at' | 'withRepo' | 'ownerRepo' | 'try' | 'soft' | 'cwd' | 'repo' | 'hostname'>;
|
|
50
|
+
/** Every synchronous client method, returning a `Result<T, GhFailure>`. */
|
|
51
|
+
export type TriedSync<C> = {
|
|
52
|
+
[K in keyof Triable<C>]: Triable<C>[K] extends (...a: infer A) => infer R ? (...a: A) => Result<R, GhFailure> : never;
|
|
53
|
+
};
|
|
54
|
+
/** Every awaitable client method, promising a `Result<T, GhFailure>`. */
|
|
55
|
+
export type TriedAsync<C> = {
|
|
56
|
+
[K in keyof Triable<C>]: Triable<C>[K] extends (...a: infer A) => Promise<infer R> ? (...a: A) => Promise<Result<R, GhFailure>> : never;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The queries that have a soft form. A mutation has none: swallowing the failure
|
|
60
|
+
* of a write is how a caller ends up believing it worked.
|
|
61
|
+
*/
|
|
62
|
+
export declare const SOFT_METHODS: readonly ["prList", "prView", "prFind", "openPrFor", "prViewSettled", "prBody", "graphql", "labelList", "issueList", "issueView", "repoList", "repoView", "searchPrs", "prReviewThreads"];
|
|
63
|
+
/** The name of a method with a soft form. */
|
|
64
|
+
export type SoftMethod = (typeof SOFT_METHODS)[number];
|
|
65
|
+
type OrNull<R> = R extends Promise<infer U> ? Promise<U | null> : R | null;
|
|
66
|
+
/** Each queried method again, returning `null` instead of throwing. One type for both clients. */
|
|
67
|
+
export type Soft<C> = {
|
|
68
|
+
[K in SoftMethod & keyof C]: C[K] extends (...a: infer A) => infer R ? (...a: A) => OrNull<R> : never;
|
|
69
|
+
};
|
|
70
|
+
/** The failure a caught `CommandFailed` or `FailureError` stands for. */
|
|
71
|
+
export declare function failureOf(error: {
|
|
72
|
+
failure: Failure;
|
|
73
|
+
}): GhFailure;
|
|
74
|
+
/** Wrap a synchronous client so every method returns a `Result<T, GhFailure>`. */
|
|
75
|
+
export declare function triedSync<C extends object>(client: C): TriedSync<C>;
|
|
76
|
+
/** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
|
|
77
|
+
export declare function triedAsync<C extends object>(client: C): TriedAsync<C>;
|
|
78
|
+
/** Wrap a synchronous client so a failed query returns `null`. */
|
|
79
|
+
export declare function softSync<C extends object>(client: C): Soft<C>;
|
|
80
|
+
/** Wrap an awaitable client so a failed query resolves to `null`. */
|
|
81
|
+
export declare function softAsync<C extends object>(client: C): Soft<C>;
|
|
82
|
+
export {};
|
package/dist/doors.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three doors, as `@osdlc/typed-git` has them: the plain verb (checked),
|
|
3
|
+
* `gh.try.*` and `gh.soft.*`.
|
|
4
|
+
*
|
|
5
|
+
* - **Plain / checked**: a value, or a throw. A failed `gh` throws a
|
|
6
|
+
* `FailureError` carrying the classified `GhFailure` (its `cause` is the
|
|
7
|
+
* `CommandFailed`), so a caller that reports it, such as `runMain`, reads
|
|
8
|
+
* "signed out" or "rate limited" and not a bare exit; output that exited 0
|
|
9
|
+
* but could not be read throws one of kind `unreadable`. A verb that
|
|
10
|
+
* reads one thing (`prView`, `issueView`, `repoView`) answers `null` when
|
|
11
|
+
* GitHub says there is no such thing: absence is an answer.
|
|
12
|
+
* - **`try`**: the same call, with the failure returned as
|
|
13
|
+
* `Result<T, GhFailure>`. It catches a thrown failure (a `FailureError`, or a
|
|
14
|
+
* `CommandFailed` from `raw`-level code) and nothing else: a `RangeError` for a bad argument still throws.
|
|
15
|
+
* - **`soft`**: the same call, with `null` for a failure. Only the queries
|
|
16
|
+
* have a soft form; no mutation does.
|
|
17
|
+
*
|
|
18
|
+
* Both are facades over the whole client, so a new method costs nothing here.
|
|
19
|
+
*/
|
|
20
|
+
import { CommandFailed, isCommandFailed, runChecked, runCheckedAsync, } from '@osdlc/command-seam';
|
|
21
|
+
import { err, FailureError, isFailure, ok } from '@osdlc/result';
|
|
22
|
+
import { classifyGhFailure, unreadableFailure } from "./failure.js";
|
|
23
|
+
/**
|
|
24
|
+
* Read the result of running `verb` as the plain door does.
|
|
25
|
+
*
|
|
26
|
+
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
27
|
+
* `runChecked`), and a probe (`installed`, `authStatus`, `api`) carries its
|
|
28
|
+
* failure in its value. A read that exited non-zero throws `CommandFailed`,
|
|
29
|
+
* unless the verb's `absent` says gh's answer was "there is no such thing",
|
|
30
|
+
* which `parse` turns into `null`. A read that exited 0 but parsed to `null`
|
|
31
|
+
* could not be read completely, and throws.
|
|
32
|
+
*
|
|
33
|
+
* @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
|
|
34
|
+
* @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
|
|
35
|
+
*/
|
|
36
|
+
export function settle(verb, result, binary) {
|
|
37
|
+
if (verb.checked || verb.probe === true)
|
|
38
|
+
return verb.parse(result);
|
|
39
|
+
if (result.neverExited === true || result.exitCode !== 0) {
|
|
40
|
+
if (result.neverExited !== true && verb.absent?.(result) === true)
|
|
41
|
+
return verb.parse(result);
|
|
42
|
+
throw ghFailureError(new CommandFailed(verb.argv, result, binary));
|
|
43
|
+
}
|
|
44
|
+
const value = verb.parse(result);
|
|
45
|
+
if (value === null) {
|
|
46
|
+
throw new FailureError(unreadableFailure(verb.argv, result.stdout, binary));
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The error the plain door throws for a failed run: a `FailureError` carrying
|
|
52
|
+
* the {@link GhFailure} that {@link classifyGhFailure} reads from it, with the
|
|
53
|
+
* `CommandFailed` as its `cause`. Anything else is returned as it is.
|
|
54
|
+
*/
|
|
55
|
+
export function ghFailureError(error) {
|
|
56
|
+
return isCommandFailed(error) ? new FailureError(classifyGhFailure(error)) : error;
|
|
57
|
+
}
|
|
58
|
+
/** {@link runChecked}, throwing {@link ghFailureError}'s classified failure. */
|
|
59
|
+
export function runCheckedGh(runner, argv, opts) {
|
|
60
|
+
try {
|
|
61
|
+
return runChecked(runner, argv, opts);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw ghFailureError(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** {@link runCheckedAsync}, rejecting with {@link ghFailureError}'s classified failure. */
|
|
68
|
+
export async function runCheckedGhAsync(runner, argv, opts) {
|
|
69
|
+
try {
|
|
70
|
+
return await runCheckedAsync(runner, argv, opts);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw ghFailureError(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The queries that have a soft form. A mutation has none: swallowing the failure
|
|
78
|
+
* of a write is how a caller ends up believing it worked.
|
|
79
|
+
*/
|
|
80
|
+
export const SOFT_METHODS = [
|
|
81
|
+
'prList',
|
|
82
|
+
'prView',
|
|
83
|
+
'prFind',
|
|
84
|
+
'openPrFor',
|
|
85
|
+
'prViewSettled',
|
|
86
|
+
'prBody',
|
|
87
|
+
'graphql',
|
|
88
|
+
'labelList',
|
|
89
|
+
'issueList',
|
|
90
|
+
'issueView',
|
|
91
|
+
'repoList',
|
|
92
|
+
'repoView',
|
|
93
|
+
'searchPrs',
|
|
94
|
+
'prReviewThreads',
|
|
95
|
+
];
|
|
96
|
+
/**
|
|
97
|
+
* Whether `error` carries a failure to hand back rather than a bug to let
|
|
98
|
+
* through. By shape, not `instanceof`: two installed copies of command-seam or
|
|
99
|
+
* result each have their own classes.
|
|
100
|
+
*/
|
|
101
|
+
function isGhFailure(error) {
|
|
102
|
+
return (typeof error === 'object' &&
|
|
103
|
+
error !== null &&
|
|
104
|
+
isFailure(error.failure));
|
|
105
|
+
}
|
|
106
|
+
/** The failure a caught `CommandFailed` or `FailureError` stands for. */
|
|
107
|
+
export function failureOf(error) {
|
|
108
|
+
const thrown = error;
|
|
109
|
+
return isCommandFailed(thrown) ? classifyGhFailure(thrown) : error.failure;
|
|
110
|
+
}
|
|
111
|
+
function facade(client, wrap) {
|
|
112
|
+
return new Proxy(client, {
|
|
113
|
+
get(target, prop) {
|
|
114
|
+
// Read from the target, not the proxy: a getter (`try` itself) must run
|
|
115
|
+
// against the real client rather than recursing through the facade.
|
|
116
|
+
const value = Reflect.get(target, prop);
|
|
117
|
+
if (typeof value !== 'function')
|
|
118
|
+
return value;
|
|
119
|
+
const method = value;
|
|
120
|
+
const name = String(prop);
|
|
121
|
+
return (...args) => wrap(() => method.apply(target, args), name);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Wrap a synchronous client so every method returns a `Result<T, GhFailure>`. */
|
|
126
|
+
export function triedSync(client) {
|
|
127
|
+
return facade(client, (call) => {
|
|
128
|
+
try {
|
|
129
|
+
return ok(call());
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (isGhFailure(error))
|
|
133
|
+
return err(failureOf(error));
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
|
|
139
|
+
export function triedAsync(client) {
|
|
140
|
+
return facade(client, async (call) => {
|
|
141
|
+
try {
|
|
142
|
+
return ok(await call());
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (isGhFailure(error))
|
|
146
|
+
return err(failureOf(error));
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function checkSoft(name) {
|
|
152
|
+
if (!SOFT_METHODS.includes(name)) {
|
|
153
|
+
throw new TypeError(`gh.soft.${name} has no soft form; call it through gh.try (or the plain method) instead`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Wrap a synchronous client so a failed query returns `null`. */
|
|
157
|
+
export function softSync(client) {
|
|
158
|
+
return facade(client, (call, name) => {
|
|
159
|
+
checkSoft(name);
|
|
160
|
+
try {
|
|
161
|
+
return call();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (isGhFailure(error))
|
|
165
|
+
return null;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** Wrap an awaitable client so a failed query resolves to `null`. */
|
|
171
|
+
export function softAsync(client) {
|
|
172
|
+
return facade(client, async (call, name) => {
|
|
173
|
+
checkSoft(name);
|
|
174
|
+
try {
|
|
175
|
+
return await call();
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (isGhFailure(error))
|
|
179
|
+
return null;
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link GhFailure}: a failed `gh` call, classified. The `gh.try.*` door
|
|
3
|
+
* returns it as the error of a `Result`; its `cause` is the `CommandFailed`
|
|
4
|
+
* it was read from.
|
|
5
|
+
*
|
|
6
|
+
* The kinds beyond the subprocess layer's come from `gh`'s stderr, matched in
|
|
7
|
+
* English. A message that matches none stays the subprocess layer's `exit`
|
|
8
|
+
* with `gh`'s first stderr line, never a wrong kind.
|
|
9
|
+
*/
|
|
10
|
+
import { type CommandFailed, type CommandFailureKind } from '@osdlc/command-seam';
|
|
11
|
+
import { type Failure } from '@osdlc/result';
|
|
12
|
+
/**
|
|
13
|
+
* Why a `gh` call failed:
|
|
14
|
+
*
|
|
15
|
+
* - everything `@osdlc/command-seam` reports (`not-installed`, `not-executable`,
|
|
16
|
+
* `bad-cwd`, `timeout`, `output-cap`, `signal`, `aborted`, `spawn`, `exit`);
|
|
17
|
+
* - `not-authenticated`: `gh` has no usable credentials, or GitHub refused
|
|
18
|
+
* them ("gh auth login", "HTTP 401", "HTTP 403", "Bad credentials"). A 403 that
|
|
19
|
+
* is a rate limit stays `rate-limited`;
|
|
20
|
+
* - `network`: GitHub could not be reached ("error connecting to
|
|
21
|
+
* api.github.com", "Could not resolve host", "dial tcp", "i/o timeout");
|
|
22
|
+
* - `not-found`: GitHub has no such thing, or hides it ("Could not resolve to a
|
|
23
|
+
* PullRequest", "HTTP 404", "no pull requests found for branch");
|
|
24
|
+
* - `rate-limited`: "API rate limit exceeded", "secondary rate limit", "HTTP 429";
|
|
25
|
+
* - `unreadable`: `gh` exited 0 but its output could not be read completely
|
|
26
|
+
* (malformed JSON, a partial GraphQL answer, a review-thread list cut short).
|
|
27
|
+
*/
|
|
28
|
+
export type GhFailureKind = CommandFailureKind | 'not-authenticated' | 'network' | 'not-found' | 'rate-limited' | 'unreadable';
|
|
29
|
+
/** A failed `gh` call. `message` is one line for a person; `detail` is `gh`'s own output. */
|
|
30
|
+
export interface GhFailure extends Failure<GhFailureKind> {
|
|
31
|
+
/** The binary that ran (`gh`), when known. */
|
|
32
|
+
binary?: string | undefined;
|
|
33
|
+
/** The arguments `gh` ran with, without the binary name. */
|
|
34
|
+
argv: string[];
|
|
35
|
+
/** `gh`'s exit code; `null` when it never reported one. */
|
|
36
|
+
exitCode: number | null;
|
|
37
|
+
/** The signal that ended `gh`, when the runner knows it. */
|
|
38
|
+
signal?: NodeJS.Signals | null | undefined;
|
|
39
|
+
/** The `CommandFailed` this was classified from; absent for `unreadable`, where `gh` succeeded. */
|
|
40
|
+
cause?: CommandFailed | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** What a verb that reads one thing asks GitHub for. */
|
|
43
|
+
export type Thing = 'pr' | 'issue' | 'repo';
|
|
44
|
+
/**
|
|
45
|
+
* Whether the text of a failed `gh` run (stderr then stdout, as
|
|
46
|
+
* {@link classifyGhFailure} reads it) means "there is no such `thing`", for a
|
|
47
|
+
* verb that reads one thing: an answer, not a failure.
|
|
48
|
+
*
|
|
49
|
+
* Narrower than the `not-found` failure kind on purpose. A mistyped `--repo`
|
|
50
|
+
* makes `gh pr view 7` say "Could not resolve to a Repository", which is a
|
|
51
|
+
* wrong repository rather than a missing pull request, so it is a `not-found`
|
|
52
|
+
* failure and not absence. Likewise a bare `HTTP 404` does not say which part
|
|
53
|
+
* of the request was missing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isAbsent(thing: Thing, text: string): boolean;
|
|
56
|
+
/** The gh failure for a failed `gh` run. */
|
|
57
|
+
export declare function classifyGhFailure(failed: CommandFailed): GhFailure;
|
|
58
|
+
/** The failure for a run that exited 0 but whose output the verb could not read. */
|
|
59
|
+
export declare function unreadableFailure(argv: readonly string[], stdout: string, binary: string | undefined): GhFailure;
|
package/dist/failure.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link GhFailure}: a failed `gh` call, classified. The `gh.try.*` door
|
|
3
|
+
* returns it as the error of a `Result`; its `cause` is the `CommandFailed`
|
|
4
|
+
* it was read from.
|
|
5
|
+
*
|
|
6
|
+
* The kinds beyond the subprocess layer's come from `gh`'s stderr, matched in
|
|
7
|
+
* English. A message that matches none stays the subprocess layer's `exit`
|
|
8
|
+
* with `gh`'s first stderr line, never a wrong kind.
|
|
9
|
+
*/
|
|
10
|
+
import { classifyFailure } from '@osdlc/command-seam';
|
|
11
|
+
import {} from '@osdlc/result';
|
|
12
|
+
const INSTALL_HINT = 'Install it from https://cli.github.com, then run `gh auth login`.';
|
|
13
|
+
const RATE_LIMITED = /API rate limit exceeded|secondary rate limit|rate limit exceeded|HTTP 429/i;
|
|
14
|
+
const NOT_AUTHENTICATED = /gh auth login|HTTP 401|HTTP 403|Bad credentials|authentication required|not logged in/i;
|
|
15
|
+
const NETWORK = /error connecting to|Could not resolve host|no such host|dial tcp|i\/o timeout|network is unreachable|connection refused/i;
|
|
16
|
+
const NOT_FOUND = /Could not resolve to an? [A-Za-z]+|HTTP 404|no pull requests found for branch|\(HTTP 404\)|Not Found/;
|
|
17
|
+
const ABSENT = {
|
|
18
|
+
pr: /Could not resolve to a PullRequest|no pull requests found for branch/,
|
|
19
|
+
issue: /Could not resolve to an Issue/,
|
|
20
|
+
repo: /Could not resolve to a Repository/,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Whether the text of a failed `gh` run (stderr then stdout, as
|
|
24
|
+
* {@link classifyGhFailure} reads it) means "there is no such `thing`", for a
|
|
25
|
+
* verb that reads one thing: an answer, not a failure.
|
|
26
|
+
*
|
|
27
|
+
* Narrower than the `not-found` failure kind on purpose. A mistyped `--repo`
|
|
28
|
+
* makes `gh pr view 7` say "Could not resolve to a Repository", which is a
|
|
29
|
+
* wrong repository rather than a missing pull request, so it is a `not-found`
|
|
30
|
+
* failure and not absence. Likewise a bare `HTTP 404` does not say which part
|
|
31
|
+
* of the request was missing.
|
|
32
|
+
*/
|
|
33
|
+
export function isAbsent(thing, text) {
|
|
34
|
+
return ABSENT[thing].test(text);
|
|
35
|
+
}
|
|
36
|
+
/** The gh failure for a failed `gh` run. */
|
|
37
|
+
export function classifyGhFailure(failed) {
|
|
38
|
+
// The client's runner may not name its binary; this is gh either way, so the
|
|
39
|
+
// messages say `gh` rather than "The command".
|
|
40
|
+
const f = failed.binary === undefined
|
|
41
|
+
? (classifyFailure(failed.result, { binary: 'gh', argv: failed.argv }) ?? failed.failure)
|
|
42
|
+
: failed.failure;
|
|
43
|
+
const base = { ...f, cause: failed };
|
|
44
|
+
if (f.kind === 'not-installed')
|
|
45
|
+
return { ...base, hint: INSTALL_HINT };
|
|
46
|
+
if (f.kind !== 'exit')
|
|
47
|
+
return base;
|
|
48
|
+
const text = `${failed.result.stderr}\n${failed.result.stdout}`;
|
|
49
|
+
if (RATE_LIMITED.test(text)) {
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
kind: 'rate-limited',
|
|
53
|
+
message: "GitHub's API rate limit was reached.",
|
|
54
|
+
hint: 'Wait a few minutes and try again.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (NOT_AUTHENTICATED.test(text)) {
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
kind: 'not-authenticated',
|
|
61
|
+
message: 'gh is not signed in to GitHub.',
|
|
62
|
+
hint: 'Run `gh auth login`.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (NETWORK.test(text)) {
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
kind: 'network',
|
|
69
|
+
message: 'GitHub could not be reached.',
|
|
70
|
+
hint: 'Check your network connection.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (NOT_FOUND.test(text)) {
|
|
74
|
+
return {
|
|
75
|
+
...base,
|
|
76
|
+
kind: 'not-found',
|
|
77
|
+
message: 'GitHub has no such pull request, issue or repository, or you cannot see it.',
|
|
78
|
+
hint: 'Check the number or name, and that your account can see the repository.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return base;
|
|
82
|
+
}
|
|
83
|
+
/** The failure for a run that exited 0 but whose output the verb could not read. */
|
|
84
|
+
export function unreadableFailure(argv, stdout, binary) {
|
|
85
|
+
return {
|
|
86
|
+
kind: 'unreadable',
|
|
87
|
+
message: 'gh answered, but its output could not be read completely.',
|
|
88
|
+
...(stdout.trim() === '' ? {} : { detail: stdout.trim().slice(0, 2000) }),
|
|
89
|
+
binary: binary ?? 'gh',
|
|
90
|
+
argv: [...argv],
|
|
91
|
+
exitCode: 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,16 +11,21 @@
|
|
|
11
11
|
* - **Pure argv builders and parsers.** The verb table and `parse.ts` never
|
|
12
12
|
* spawn anything, so an async fan-out drives them through its own runner
|
|
13
13
|
* without duplicating a line of parsing.
|
|
14
|
-
* - **A client.** `Gh`,
|
|
15
|
-
*
|
|
14
|
+
* - **A client.** `Gh`, with `Git`'s three doors: the plain method returns a
|
|
15
|
+
* value or throws a `FailureError` carrying the classified
|
|
16
|
+
* `GhFailure`; `gh.try.*` returns a
|
|
17
|
+
* `Result<T, GhFailure>`; `gh.soft.*` returns `null` for a failed query.
|
|
18
|
+
* `raw` is the hatch.
|
|
16
19
|
*
|
|
17
20
|
* Authentication is `gh`'s. No token is read, stored, or forwarded.
|
|
18
21
|
*/
|
|
19
22
|
export { AsyncGh, type AsyncCallOptions, type AsyncGhOptions } from './async.ts';
|
|
23
|
+
export { classifyGhFailure, type GhFailure, type GhFailureKind } from './failure.ts';
|
|
24
|
+
export { type Soft, type TriedAsync, type TriedSync } from './doors.ts';
|
|
25
|
+
export { runVerb, runVerbAsync, type RunVerbAsyncOptions, type RunVerbOptions } from './run.ts';
|
|
20
26
|
export { Gh, defaultRunnerFactory, type GhOptions, type RunnerFactory } from './sync.ts';
|
|
21
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';
|
|
22
28
|
export { anyCheckFailed, normalizeAuthorLogin, parsePullRequest } from './parse.ts';
|
|
23
29
|
export { ownerRepoSlug, parseOwnerRepo, parsePrRef, parsePrUrl, type PrRef, type PullRequestRef, } from './repo.ts';
|
|
24
30
|
export type { Actor, CheckConclusion, CheckRun, Comment, IssueRow, Label, MergeableState, MergeStateStatus, OwnerRepo, PullRequest, PrSearchHit, PullRequestState, Repository, RepositoryVisibility, Review, ReviewDecision, ReviewState, } from './types.ts';
|
|
25
31
|
export { CommandFailed, type AsyncCommandRunner, type CommandResult, type CommandRunner, type CommandRunOptions, } from '@osdlc/command-seam';
|
|
26
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -11,12 +11,18 @@
|
|
|
11
11
|
* - **Pure argv builders and parsers.** The verb table and `parse.ts` never
|
|
12
12
|
* spawn anything, so an async fan-out drives them through its own runner
|
|
13
13
|
* without duplicating a line of parsing.
|
|
14
|
-
* - **A client.** `Gh`,
|
|
15
|
-
*
|
|
14
|
+
* - **A client.** `Gh`, with `Git`'s three doors: the plain method returns a
|
|
15
|
+
* value or throws a `FailureError` carrying the classified
|
|
16
|
+
* `GhFailure`; `gh.try.*` returns a
|
|
17
|
+
* `Result<T, GhFailure>`; `gh.soft.*` returns `null` for a failed query.
|
|
18
|
+
* `raw` is the hatch.
|
|
16
19
|
*
|
|
17
20
|
* Authentication is `gh`'s. No token is read, stored, or forwarded.
|
|
18
21
|
*/
|
|
19
22
|
export { AsyncGh } from "./async.js";
|
|
23
|
+
export { classifyGhFailure } from "./failure.js";
|
|
24
|
+
export {} from "./doors.js";
|
|
25
|
+
export { runVerb, runVerbAsync } from "./run.js";
|
|
20
26
|
export { Gh, defaultRunnerFactory } from "./sync.js";
|
|
21
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";
|
|
22
28
|
export { anyCheckFailed, normalizeAuthorLogin, parsePullRequest } from "./parse.js";
|
package/dist/parse.d.ts
CHANGED
|
@@ -47,4 +47,3 @@ export declare function parsePullRequest(raw: unknown): PullRequest | null;
|
|
|
47
47
|
export declare function parseRepository(raw: unknown): Repository | null;
|
|
48
48
|
/** A `gh search prs --json` row as a {@link PrSearchHit}, or null without a `number`. */
|
|
49
49
|
export declare function parsePrSearchHit(raw: unknown): PrSearchHit | null;
|
|
50
|
-
//# sourceMappingURL=parse.d.ts.map
|
package/dist/repo.d.ts
CHANGED