@osdlc/typed-gh 0.5.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 +61 -1
- package/CONTRIBUTING.md +4 -3
- package/README.md +247 -56
- package/dist/async.d.ts +70 -28
- package/dist/async.js +127 -44
- 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 +12 -8
- package/dist/index.js +9 -4
- package/dist/parse.d.ts +5 -2
- package/dist/parse.js +48 -0
- package/dist/repo.d.ts +42 -11
- package/dist/repo.js +76 -12
- package/dist/sync.d.ts +104 -28
- package/dist/sync.js +155 -46
- package/dist/types.d.ts +37 -1
- package/dist/verbs.d.ts +213 -45
- package/dist/verbs.js +504 -86
- package/package.json +13 -17
- package/async.ts +0 -285
- 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 -97
- package/parse.ts +0 -252
- package/repo.ts +0 -59
- package/sync.ts +0 -326
- package/types.ts +0 -186
- package/verbs.ts +0 -799
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
|
-
export { AsyncGh, type AsyncCallOptions } from './async.ts';
|
|
20
|
-
export {
|
|
21
|
-
export {
|
|
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 { Gh, defaultRunnerFactory, type GhOptions, type RunnerFactory } from './sync.ts';
|
|
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
|
-
export { ownerRepoSlug, parseOwnerRepo, parsePrUrl, type PullRequestRef } from './repo.ts';
|
|
24
|
-
export type { Actor, CheckConclusion, CheckRun, Comment, IssueRow, Label, MergeableState, MergeStateStatus, OwnerRepo, PullRequest, PullRequestState, Review, ReviewDecision, ReviewState, } from './types.ts';
|
|
28
|
+
export { ownerRepoSlug, parseOwnerRepo, parsePrRef, parsePrUrl, type PrRef, type PullRequestRef, } from './repo.ts';
|
|
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,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 } from "./async.js";
|
|
23
|
+
export { classifyGhFailure } from "./failure.js";
|
|
24
|
+
export {} from "./doors.js";
|
|
20
25
|
export { Gh, defaultRunnerFactory } from "./sync.js";
|
|
21
|
-
export { api, authStatus, createPr, editPr, editPrBody, createLabel, createReview, editLabel, installed, issueList, issueView, labelList, openPrsForBranch, pickPrForBranch, prBody, prComment, prSetDraft, issueCommentEdit, prDiff, prList, prMerge, prReviewThreads, prUpdateBranch, prView, setIssueLabel, PR_LIST_FIELDS, PR_REF_FIELDS, PR_VIEW_FIELDS, } from "./verbs.js";
|
|
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";
|
|
23
|
-
export { ownerRepoSlug, parseOwnerRepo, parsePrUrl } from "./repo.js";
|
|
28
|
+
export { ownerRepoSlug, parseOwnerRepo, parsePrRef, parsePrUrl, } from "./repo.js";
|
|
24
29
|
// The seam's error and runner types appear in this package's signatures, so
|
|
25
30
|
// they are re-exported. Its functions are not: import them from
|
|
26
31
|
// `@osdlc/command-seam`.
|
package/dist/parse.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* `StatusContext` carries `context`/`state`. They are folded onto one
|
|
18
18
|
* verdict axis so a caller never has to know which it got.
|
|
19
19
|
*/
|
|
20
|
-
import type { Actor, CheckRun, Label, PullRequest } from './types.ts';
|
|
20
|
+
import type { Actor, CheckRun, Label, PrSearchHit, PullRequest, Repository } from './types.ts';
|
|
21
21
|
/**
|
|
22
22
|
* Strip a single trailing `[bot]` suffix — REST shape folded to GraphQL shape.
|
|
23
23
|
*
|
|
@@ -43,4 +43,7 @@ export declare function parseLabels(x: unknown): Label[];
|
|
|
43
43
|
* any field subset.
|
|
44
44
|
*/
|
|
45
45
|
export declare function parsePullRequest(raw: unknown): PullRequest | null;
|
|
46
|
-
|
|
46
|
+
/** A `gh repo list`/`view --json` row as a {@link Repository}, or null without `nameWithOwner`. */
|
|
47
|
+
export declare function parseRepository(raw: unknown): Repository | null;
|
|
48
|
+
/** A `gh search prs --json` row as a {@link PrSearchHit}, or null without a `number`. */
|
|
49
|
+
export declare function parsePrSearchHit(raw: unknown): PrSearchHit | null;
|
package/dist/parse.js
CHANGED
|
@@ -223,3 +223,51 @@ export function parsePullRequest(raw) {
|
|
|
223
223
|
updatedAt: strOrNull(raw['updatedAt'] ?? raw['updated_at']),
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
|
+
// ── repositories and search ─────────────────────────────────────────────────
|
|
227
|
+
/** A `gh repo list`/`view --json` row as a {@link Repository}, or null without `nameWithOwner`. */
|
|
228
|
+
export function parseRepository(raw) {
|
|
229
|
+
if (!isRecord(raw))
|
|
230
|
+
return null;
|
|
231
|
+
const nameWithOwner = str(raw['nameWithOwner']);
|
|
232
|
+
const slash = nameWithOwner.indexOf('/');
|
|
233
|
+
if (slash <= 0 || slash === nameWithOwner.length - 1)
|
|
234
|
+
return null;
|
|
235
|
+
const branch = raw['defaultBranchRef'];
|
|
236
|
+
return {
|
|
237
|
+
nameWithOwner,
|
|
238
|
+
owner: nameWithOwner.slice(0, slash),
|
|
239
|
+
name: nameWithOwner.slice(slash + 1),
|
|
240
|
+
description: str(raw['description']),
|
|
241
|
+
defaultBranch: isRecord(branch) ? strOrNull(branch['name']) : null,
|
|
242
|
+
visibility: strOrNull(raw['visibility']),
|
|
243
|
+
isArchived: bool(raw['isArchived']),
|
|
244
|
+
url: str(raw['url']),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** A `gh search prs --json` row as a {@link PrSearchHit}, or null without a `number`. */
|
|
248
|
+
export function parsePrSearchHit(raw) {
|
|
249
|
+
if (!isRecord(raw))
|
|
250
|
+
return null;
|
|
251
|
+
const number = raw['number'];
|
|
252
|
+
if (typeof number !== 'number')
|
|
253
|
+
return null;
|
|
254
|
+
const repo = raw['repository'];
|
|
255
|
+
const slug = isRecord(repo) ? str(repo['nameWithOwner']) : '';
|
|
256
|
+
const slash = slug.indexOf('/');
|
|
257
|
+
return {
|
|
258
|
+
number,
|
|
259
|
+
title: str(raw['title']),
|
|
260
|
+
// Search answers in lower case; `PullRequest.state` is upper case.
|
|
261
|
+
state: str(raw['state']).toUpperCase(),
|
|
262
|
+
url: str(raw['url']),
|
|
263
|
+
isDraft: bool(raw['isDraft']),
|
|
264
|
+
repository: slash > 0 && slash < slug.length - 1
|
|
265
|
+
? { owner: slug.slice(0, slash), repo: slug.slice(slash + 1) }
|
|
266
|
+
: null,
|
|
267
|
+
author: parseActor(raw['author']),
|
|
268
|
+
labels: parseLabels(raw['labels']),
|
|
269
|
+
createdAt: strOrNull(raw['createdAt']),
|
|
270
|
+
updatedAt: strOrNull(raw['updatedAt']),
|
|
271
|
+
closedAt: strOrNull(raw['closedAt']),
|
|
272
|
+
};
|
|
273
|
+
}
|
package/dist/repo.d.ts
CHANGED
|
@@ -15,22 +15,53 @@ import type { OwnerRepo } from './types.ts';
|
|
|
15
15
|
export declare function parseOwnerRepo(remoteUrl: string): OwnerRepo | null;
|
|
16
16
|
/** `owner/repo`, the form `gh api` paths and `--repo` both take. */
|
|
17
17
|
export declare function ownerRepoSlug(r: OwnerRepo): string;
|
|
18
|
-
/** A pull request coordinate: the repository and the PR's number within it. */
|
|
18
|
+
/** A pull request coordinate: the host, the repository and the PR's number within it. */
|
|
19
19
|
export interface PullRequestRef extends OwnerRepo {
|
|
20
|
+
/** The GitHub host, lower-cased: `github.com`, or an Enterprise host. */
|
|
21
|
+
hostname: string;
|
|
20
22
|
number: number;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
|
-
* The `{owner, repo, number}` a GitHub pull-request URL names, or
|
|
24
|
-
* `url` is not one.
|
|
25
|
+
* The `{hostname, owner, repo, number}` a GitHub pull-request URL names, or
|
|
26
|
+
* null when `url` is not one.
|
|
25
27
|
*
|
|
26
|
-
* Accepts `https
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Accepts `https://<host>/<owner>/<repo>/pull/<n>` with or without a scheme,
|
|
29
|
+
* a trailing slash, a sub-page (`/files`, `/commits`), a query string or a
|
|
30
|
+
* fragment — every shape a browser's address bar hands over. The host is
|
|
31
|
+
* `github.com` (a `www.` prefix is dropped) or a GitHub Enterprise host such
|
|
32
|
+
* as `github.acme.com` or `acme.ghe.com`; without a scheme the host must
|
|
33
|
+
* contain a dot. `parseGitUrl` is the wrong tool here: it reads `/pull/<n>`
|
|
30
34
|
* as part of the project path.
|
|
31
|
-
*
|
|
32
|
-
* GitHub.com only. A GitHub Enterprise host is a different coordinate space
|
|
33
|
-
* (the `gh` CLI needs `--hostname`), so it is not silently accepted.
|
|
34
35
|
*/
|
|
35
36
|
export declare function parsePrUrl(url: string): PullRequestRef | null;
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* What a person typed to name a pull request, read by {@link parsePrRef}.
|
|
39
|
+
*
|
|
40
|
+
* - `number`: `42`, `#42`, or `owner/name#42` (then `repo` is set). For bare
|
|
41
|
+
* digits, `orBranch` holds the same text, since a branch can be named `42`.
|
|
42
|
+
* - `url`: a pull request URL, with what {@link parsePrUrl} read from it.
|
|
43
|
+
* - `branch`: any other text that is a valid branch name.
|
|
44
|
+
*/
|
|
45
|
+
export type PrRef = {
|
|
46
|
+
kind: 'number';
|
|
47
|
+
number: number;
|
|
48
|
+
repo: OwnerRepo | null;
|
|
49
|
+
orBranch: string | null;
|
|
50
|
+
} | ({
|
|
51
|
+
kind: 'url';
|
|
52
|
+
url: string;
|
|
53
|
+
} & PullRequestRef) | {
|
|
54
|
+
kind: 'branch';
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Read what a person typed to name a pull request: a number, `#42`,
|
|
59
|
+
* `owner/name#42`, a URL, or a branch name. Null when it is none of these.
|
|
60
|
+
*
|
|
61
|
+
* Pure: it starts no process. Digits are read as a number first, the way
|
|
62
|
+
* `gh pr view 42` reads them; `orBranch` keeps the branch reading. The
|
|
63
|
+
* clients' `prFind` tries both.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parsePrRef(input: string): PrRef | null;
|
|
66
|
+
/** True for `owner/name` as GitHub spells it. */
|
|
67
|
+
export declare function isRepoSlug(text: string): boolean;
|
package/dist/repo.js
CHANGED
|
@@ -26,24 +26,88 @@ export function ownerRepoSlug(r) {
|
|
|
26
26
|
return `${r.owner}/${r.repo}`;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* The `{owner, repo, number}` a GitHub pull-request URL names, or
|
|
30
|
-
* `url` is not one.
|
|
29
|
+
* The `{hostname, owner, repo, number}` a GitHub pull-request URL names, or
|
|
30
|
+
* null when `url` is not one.
|
|
31
31
|
*
|
|
32
|
-
* Accepts `https
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* Accepts `https://<host>/<owner>/<repo>/pull/<n>` with or without a scheme,
|
|
33
|
+
* a trailing slash, a sub-page (`/files`, `/commits`), a query string or a
|
|
34
|
+
* fragment — every shape a browser's address bar hands over. The host is
|
|
35
|
+
* `github.com` (a `www.` prefix is dropped) or a GitHub Enterprise host such
|
|
36
|
+
* as `github.acme.com` or `acme.ghe.com`; without a scheme the host must
|
|
37
|
+
* contain a dot. `parseGitUrl` is the wrong tool here: it reads `/pull/<n>`
|
|
36
38
|
* as part of the project path.
|
|
37
|
-
*
|
|
38
|
-
* GitHub.com only. A GitHub Enterprise host is a different coordinate space
|
|
39
|
-
* (the `gh` CLI needs `--hostname`), so it is not silently accepted.
|
|
40
39
|
*/
|
|
41
40
|
export function parsePrUrl(url) {
|
|
42
|
-
const m = /^(?:https
|
|
41
|
+
const m = /^(?:(https?:)\/\/)?([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::\d+)?)\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i.exec(url.trim());
|
|
43
42
|
if (m === null)
|
|
44
43
|
return null;
|
|
45
|
-
const
|
|
44
|
+
const host = m[2].toLowerCase();
|
|
45
|
+
if (m[1] === undefined && !host.includes('.'))
|
|
46
|
+
return null;
|
|
47
|
+
const number = Number(m[5]);
|
|
46
48
|
if (!Number.isSafeInteger(number) || number <= 0)
|
|
47
49
|
return null;
|
|
48
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
hostname: host === 'www.github.com' ? 'github.com' : host,
|
|
52
|
+
owner: m[3],
|
|
53
|
+
repo: m[4].replace(/\.git$/, ''),
|
|
54
|
+
number,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read what a person typed to name a pull request: a number, `#42`,
|
|
59
|
+
* `owner/name#42`, a URL, or a branch name. Null when it is none of these.
|
|
60
|
+
*
|
|
61
|
+
* Pure: it starts no process. Digits are read as a number first, the way
|
|
62
|
+
* `gh pr view 42` reads them; `orBranch` keeps the branch reading. The
|
|
63
|
+
* clients' `prFind` tries both.
|
|
64
|
+
*/
|
|
65
|
+
export function parsePrRef(input) {
|
|
66
|
+
const text = input.trim();
|
|
67
|
+
const digits = /^(#?)(\d+)$/.exec(text);
|
|
68
|
+
if (digits !== null) {
|
|
69
|
+
const number = Number(digits[2]);
|
|
70
|
+
if (Number.isSafeInteger(number) && number > 0) {
|
|
71
|
+
return { kind: 'number', number, repo: null, orBranch: digits[1] === '' ? text : null };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const qualified = /^([^/\s#]+)\/([^/\s#]+)#(\d+)$/.exec(text);
|
|
75
|
+
if (qualified !== null) {
|
|
76
|
+
const number = Number(qualified[3]);
|
|
77
|
+
if (!Number.isSafeInteger(number) || number <= 0)
|
|
78
|
+
return null;
|
|
79
|
+
if (!isRepoSlug(`${qualified[1]}/${qualified[2]}`))
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
kind: 'number',
|
|
83
|
+
number,
|
|
84
|
+
repo: { owner: qualified[1], repo: qualified[2] },
|
|
85
|
+
orBranch: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const url = parsePrUrl(text);
|
|
89
|
+
if (url !== null)
|
|
90
|
+
return { kind: 'url', url: text, ...url };
|
|
91
|
+
return isBranchName(text) ? { kind: 'branch', name: text } : null;
|
|
92
|
+
}
|
|
93
|
+
/** True for `owner/name` as GitHub spells it. */
|
|
94
|
+
export function isRepoSlug(text) {
|
|
95
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9_.][A-Za-z0-9_.-]*$/.test(text);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* True when `name` is a branch name git would accept (`git check-ref-format
|
|
99
|
+
* --branch`), and does not start with `-`, so it cannot read as a flag.
|
|
100
|
+
*/
|
|
101
|
+
function isBranchName(name) {
|
|
102
|
+
if (name === '' || name === '@' || name.startsWith('-') || name.startsWith('/'))
|
|
103
|
+
return false;
|
|
104
|
+
if (name.endsWith('/') || name.endsWith('.'))
|
|
105
|
+
return false;
|
|
106
|
+
// Control characters, space, and the characters git reserves.
|
|
107
|
+
// oxlint-disable-next-line no-control-regex
|
|
108
|
+
if (/[\x00-\x20\x7f~^:?*[\\]/.test(name))
|
|
109
|
+
return false;
|
|
110
|
+
if (name.includes('..') || name.includes('@{') || name.includes('//'))
|
|
111
|
+
return false;
|
|
112
|
+
return name.split('/').every((part) => !part.startsWith('.') && !part.endsWith('.lock'));
|
|
49
113
|
}
|