@osdlc/typed-gh 0.6.0 → 0.7.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 +37 -1
- package/CONTRIBUTING.md +4 -3
- package/README.md +94 -35
- package/dist/async.d.ts +37 -23
- package/dist/async.js +48 -27
- 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 +7 -3
- package/dist/index.js +7 -2
- package/dist/parse.d.ts +0 -1
- package/dist/repo.d.ts +0 -1
- package/dist/sync.d.ts +53 -29
- package/dist/sync.js +64 -33
- package/dist/types.d.ts +0 -1
- package/dist/verbs.d.ts +19 -4
- package/dist/verbs.js +23 -4
- 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/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,20 @@
|
|
|
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';
|
|
20
25
|
export { Gh, defaultRunnerFactory, type GhOptions, type RunnerFactory } from './sync.ts';
|
|
21
26
|
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
27
|
export { anyCheckFailed, normalizeAuthorLogin, parsePullRequest } from './parse.ts';
|
|
23
28
|
export { ownerRepoSlug, parseOwnerRepo, parsePrRef, parsePrUrl, type PrRef, type PullRequestRef, } from './repo.ts';
|
|
24
29
|
export type { Actor, CheckConclusion, CheckRun, Comment, IssueRow, Label, MergeableState, MergeStateStatus, OwnerRepo, PullRequest, PrSearchHit, PullRequestState, Repository, RepositoryVisibility, Review, ReviewDecision, ReviewState, } from './types.ts';
|
|
25
30
|
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,17 @@
|
|
|
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";
|
|
20
25
|
export { Gh, defaultRunnerFactory } from "./sync.js";
|
|
21
26
|
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
27
|
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
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
|