@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.
@@ -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;
@@ -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`, tri-modal like `Git`: queries return parsed values
15
- * and never throw, mutations throw `CommandFailed`, `raw` is the hatch.
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`, tri-modal like `Git`: queries return parsed values
15
- * and never throw, mutations throw `CommandFailed`, `raw` is the hatch.
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
@@ -65,4 +65,3 @@ export type PrRef = {
65
65
  export declare function parsePrRef(input: string): PrRef | null;
66
66
  /** True for `owner/name` as GitHub spells it. */
67
67
  export declare function isRepoSlug(text: string): boolean;
68
- //# sourceMappingURL=repo.d.ts.map
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
- * Tri-modal, same as the git client: queries return parsed values and never
11
- * throw, mutations throw `CommandFailed`, and `raw`/`rawChecked` are the
12
- * escape hatches.
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 `CommandFailed` on a non-zero exit. */
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
- /** Pull requests matching `opts`, or null when gh failed. */
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> | null;
93
+ }): ListResult<PullRequest>;
69
94
  /**
70
- * One PR by number, branch or URL, with the heavy fields; null when gh
71
- * failed or there is no such PR.
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, or null when gh failed. */
147
+ /** A PR's body text. Throws when gh fails. */
123
148
  prBody(number: number, opts?: {
124
149
  timeoutMs?: number | undefined;
125
- }): string | null;
150
+ }): string;
126
151
  /**
127
- * A REST GET through `gh api`. Never throws: failure is `ok: false` on the
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`, or null on a failed or partial
135
- * result. See the `graphql` verb.
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[] | null;
142
- /** Every label on the repository (up to `opts.limit`, default 1000), or null when gh failed. */
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> | null;
147
- /** Issues matching `opts` (open by default, up to 200), or null when gh failed. */
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> | null;
151
- /** One issue with its labels, or null when gh failed. */
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), or null when gh failed. */
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> | null;
159
- /** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when gh failed. */
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, or null when gh failed. */
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> | null;
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 `CommandFailed` when gh cannot produce it. */
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, or null when they could not be read completely. */
227
- prReviewThreads(number: number): v.ReviewThread[] | null;
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