@greatwait/cli 0.1.0-alpha.3
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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/args.d.ts +46 -0
- package/dist/args.js +118 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +191 -0
- package/dist/commands.d.ts +14 -0
- package/dist/commands.js +836 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +242 -0
- package/dist/exit.d.ts +58 -0
- package/dist/exit.js +73 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/journey.d.ts +49 -0
- package/dist/journey.js +76 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +24 -0
- package/dist/node-runtime.d.ts +35 -0
- package/dist/node-runtime.js +210 -0
- package/dist/oauth.d.ts +96 -0
- package/dist/oauth.js +205 -0
- package/dist/output.d.ts +66 -0
- package/dist/output.js +76 -0
- package/dist/runtime.d.ts +63 -0
- package/dist/runtime.js +32 -0
- package/dist/session.d.ts +66 -0
- package/dist/session.js +157 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlowXO LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @greatwait/cli
|
|
2
|
+
|
|
3
|
+
The Greatwait command line. Install it, sign in, and reach a first callback
|
|
4
|
+
against a receiver you control.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install --global @greatwait/cli
|
|
8
|
+
greatwait login --api-url https://api.greatwait.dev
|
|
9
|
+
greatwait journey
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What it is
|
|
13
|
+
|
|
14
|
+
A thin executable over the public API — the same operations `@greatwait/api-client`
|
|
15
|
+
exposes and the same contracts the OpenAPI document publishes. It adds no
|
|
16
|
+
endpoint, no retry policy and no behaviour of its own: anything you can do here
|
|
17
|
+
you can do with `curl`, and anything you cannot do with `curl` you cannot do
|
|
18
|
+
here.
|
|
19
|
+
|
|
20
|
+
## Signing in
|
|
21
|
+
|
|
22
|
+
`greatwait login` uses OAuth: it registers itself with the service, opens your
|
|
23
|
+
browser, and receives the answer on a loopback port it has already bound.
|
|
24
|
+
Nothing is configured — the endpoints come from the origin's own published
|
|
25
|
+
metadata.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
greatwait login # opens a browser
|
|
29
|
+
greatwait login --no-browser # prints the address to open yourself
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For automation, use a scoped API credential created in the dashboard. It is
|
|
33
|
+
never taken as a command-line argument, because an argument is visible to every
|
|
34
|
+
other process on the machine and is written to your shell history:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
printf %s "$GREATWAIT_API_KEY" | greatwait login --api-key
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
or export `GREATWAIT_API_KEY` and skip signing in altogether.
|
|
41
|
+
|
|
42
|
+
Credentials are stored per API origin in your platform's configuration
|
|
43
|
+
directory, readable by you and nobody else. A staging credential is never
|
|
44
|
+
presented to production, because the two are separate entries rather than one
|
|
45
|
+
current context.
|
|
46
|
+
|
|
47
|
+
## First value
|
|
48
|
+
|
|
49
|
+
`greatwait journey` prints the published first-value sequence — the same nine
|
|
50
|
+
steps the website and the quickstarts walk — with the command for each one.
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
greatwait destinations create --url https://your-receiver.example.com/greatwait
|
|
54
|
+
greatwait schedules create --endpoint ep_… --in 30s --data '{"greeting":"first value"}'
|
|
55
|
+
greatwait occurrences wait sch_… occ_…
|
|
56
|
+
greatwait attempts list sch_… occ_…
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Output and exit codes
|
|
60
|
+
|
|
61
|
+
`--json` is a versioned machine shape, not a serialization of the human text.
|
|
62
|
+
The exit status is a class rather than a code per failure:
|
|
63
|
+
|
|
64
|
+
| code | meaning |
|
|
65
|
+
| ---: | ---------------------------------------------------------------------- |
|
|
66
|
+
| 0 | it worked |
|
|
67
|
+
| 1 | a defect in this command |
|
|
68
|
+
| 2 | the command line was wrong |
|
|
69
|
+
| 3 | no usable credential — `greatwait login` |
|
|
70
|
+
| 4 | the credential may not do this |
|
|
71
|
+
| 5 | the request was refused on its merits |
|
|
72
|
+
| 6 | the service could not answer now; the identical request may work later |
|
|
73
|
+
| 7 | a deadline passed |
|
|
74
|
+
| 130 | interrupted |
|
|
75
|
+
|
|
76
|
+
The service's own problem code is preserved in `--json` and in the message, so a
|
|
77
|
+
script that already branches on one against the REST surface does not need a
|
|
78
|
+
second vocabulary.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line, parsed.
|
|
3
|
+
*
|
|
4
|
+
* **Deliberately small, and one rule in it is a security control rather than a
|
|
5
|
+
* convenience.** `--api-key` takes no value. Not "should not" — cannot: the
|
|
6
|
+
* parser refuses `--api-key=…` and refuses a bare `--api-key` followed by
|
|
7
|
+
* anything that is not another option. Every other argument on a process's
|
|
8
|
+
* command line is readable by any other process on the machine through `ps`,
|
|
9
|
+
* lands in shell history, and is captured by a crash reporter; a flag that
|
|
10
|
+
* accepted a secret would be a flag somebody uses once in a script and never
|
|
11
|
+
* looks at again.
|
|
12
|
+
*/
|
|
13
|
+
export interface ParsedArguments {
|
|
14
|
+
readonly booleans: ReadonlySet<string>;
|
|
15
|
+
readonly command: readonly string[];
|
|
16
|
+
readonly options: Readonly<Record<string, string>>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What to say to somebody who has just put a credential in their shell history.
|
|
20
|
+
*
|
|
21
|
+
* One sentence about the harm and two ways to avoid it, stated once because
|
|
22
|
+
* both spellings — attached and separated — deserve the same answer. Neither
|
|
23
|
+
* repeats the value back.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SECRET_ARGUMENT_ADVICE = "A credential given as an argument is visible to every process on this machine and is written to your shell history \u2014 treat the one you just typed as exposed and replace it. Run the command with a bare --api-key and paste the secret when asked, or pipe it in: printf %s \"$KEY\" | greatwait login --api-key";
|
|
26
|
+
export declare function parseArguments(argv: readonly string[],
|
|
27
|
+
/**
|
|
28
|
+
* The words that begin a command, so the rule below can tell a subcommand
|
|
29
|
+
* from a leaked secret.
|
|
30
|
+
*
|
|
31
|
+
* **Without it the rule refused `greatwait --api-key login`.** The parser is
|
|
32
|
+
* position-independent by design, so a bare `--api-key` before the command
|
|
33
|
+
* word is a legitimate line that used to work; refusing it says "this flag
|
|
34
|
+
* does not take a value" to somebody who never typed one. Passed in rather
|
|
35
|
+
* than imported, because `commands.ts` reads this module and the cycle would
|
|
36
|
+
* be the wrong way round.
|
|
37
|
+
*/
|
|
38
|
+
commandWords?: ReadonlySet<string>): ParsedArguments;
|
|
39
|
+
/**
|
|
40
|
+
* A duration written the way a person writes one: `30s`, `5m`, `2h`.
|
|
41
|
+
*
|
|
42
|
+
* Bare digits are refused rather than assumed to be seconds. "in 30" is a
|
|
43
|
+
* sentence with a missing word, and guessing at it is how somebody schedules
|
|
44
|
+
* something thirty milliseconds — or thirty hours — from now.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseDuration(value: string): number;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { CliError } from "./runtime.js";
|
|
2
|
+
/** Options that take a value. Anything else is a boolean flag. */
|
|
3
|
+
const VALUED_OPTIONS = new Set([
|
|
4
|
+
"--api-url",
|
|
5
|
+
"--at",
|
|
6
|
+
"--data",
|
|
7
|
+
"--endpoint",
|
|
8
|
+
"--idempotency-key",
|
|
9
|
+
"--in",
|
|
10
|
+
"--scope",
|
|
11
|
+
"--timeout",
|
|
12
|
+
"--url",
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Options that must never take a value, whatever they look like.
|
|
16
|
+
*
|
|
17
|
+
* Separate from "not in `VALUED_OPTIONS`" so the refusal can say *why* — a
|
|
18
|
+
* person who typed `--api-key sk_...` has just put a secret in their shell
|
|
19
|
+
* history and needs to be told that, not told the flag was unrecognised.
|
|
20
|
+
*/
|
|
21
|
+
const SECRET_BEARING_FLAGS = new Set(["--api-key"]);
|
|
22
|
+
/**
|
|
23
|
+
* What to say to somebody who has just put a credential in their shell history.
|
|
24
|
+
*
|
|
25
|
+
* One sentence about the harm and two ways to avoid it, stated once because
|
|
26
|
+
* both spellings — attached and separated — deserve the same answer. Neither
|
|
27
|
+
* repeats the value back.
|
|
28
|
+
*/
|
|
29
|
+
export const SECRET_ARGUMENT_ADVICE = 'A credential given as an argument is visible to every process on this machine and is written to your shell history — treat the one you just typed as exposed and replace it. Run the command with a bare --api-key and paste the secret when asked, or pipe it in: printf %s "$KEY" | greatwait login --api-key';
|
|
30
|
+
export function parseArguments(argv,
|
|
31
|
+
/**
|
|
32
|
+
* The words that begin a command, so the rule below can tell a subcommand
|
|
33
|
+
* from a leaked secret.
|
|
34
|
+
*
|
|
35
|
+
* **Without it the rule refused `greatwait --api-key login`.** The parser is
|
|
36
|
+
* position-independent by design, so a bare `--api-key` before the command
|
|
37
|
+
* word is a legitimate line that used to work; refusing it says "this flag
|
|
38
|
+
* does not take a value" to somebody who never typed one. Passed in rather
|
|
39
|
+
* than imported, because `commands.ts` reads this module and the cycle would
|
|
40
|
+
* be the wrong way round.
|
|
41
|
+
*/
|
|
42
|
+
commandWords = new Set()) {
|
|
43
|
+
const command = [];
|
|
44
|
+
const options = {};
|
|
45
|
+
const booleans = new Set();
|
|
46
|
+
let index = 0;
|
|
47
|
+
while (index < argv.length) {
|
|
48
|
+
const token = argv[index];
|
|
49
|
+
if (!token.startsWith("--")) {
|
|
50
|
+
command.push(token);
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const equals = token.indexOf("=");
|
|
55
|
+
const name = equals === -1 ? token : token.slice(0, equals);
|
|
56
|
+
if (SECRET_BEARING_FLAGS.has(name)) {
|
|
57
|
+
if (equals !== -1) {
|
|
58
|
+
throw new CliError(`${name} does not take a value on the command line`, "usage", SECRET_ARGUMENT_ADVICE);
|
|
59
|
+
}
|
|
60
|
+
// **The next token is refused here, rather than left to a command.**
|
|
61
|
+
// Leaving it to become a positional argument was the first design, and a
|
|
62
|
+
// review round found what that cost: `login` refuses an unexpected
|
|
63
|
+
// argument by *naming* it, so `greatwait login --api-key sk_live_…`
|
|
64
|
+
// printed the credential back — on standard error, and inside the
|
|
65
|
+
// `--json` envelope on standard output, which is the stream a
|
|
66
|
+
// continuous-integration job captures. The flag exists so that a secret
|
|
67
|
+
// never reaches output, and its own refusal was the one place it did.
|
|
68
|
+
//
|
|
69
|
+
// Nothing is echoed. Somebody who has just typed their credential needs
|
|
70
|
+
// to be told so, not shown it again.
|
|
71
|
+
const following = argv[index + 1];
|
|
72
|
+
if (following !== undefined &&
|
|
73
|
+
!following.startsWith("--") &&
|
|
74
|
+
!commandWords.has(following)) {
|
|
75
|
+
throw new CliError(`${name} does not take a value on the command line`, "usage", SECRET_ARGUMENT_ADVICE);
|
|
76
|
+
}
|
|
77
|
+
booleans.add(name);
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (VALUED_OPTIONS.has(name)) {
|
|
82
|
+
if (equals !== -1) {
|
|
83
|
+
options[name] = token.slice(equals + 1);
|
|
84
|
+
index += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const value = argv[index + 1];
|
|
88
|
+
if (value === undefined || value.startsWith("--")) {
|
|
89
|
+
throw new CliError(`${name} needs a value`, "usage");
|
|
90
|
+
}
|
|
91
|
+
options[name] = value;
|
|
92
|
+
index += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (equals !== -1) {
|
|
96
|
+
throw new CliError(`${name} does not take a value`, "usage");
|
|
97
|
+
}
|
|
98
|
+
booleans.add(name);
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
return { booleans, command, options };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A duration written the way a person writes one: `30s`, `5m`, `2h`.
|
|
105
|
+
*
|
|
106
|
+
* Bare digits are refused rather than assumed to be seconds. "in 30" is a
|
|
107
|
+
* sentence with a missing word, and guessing at it is how somebody schedules
|
|
108
|
+
* something thirty milliseconds — or thirty hours — from now.
|
|
109
|
+
*/
|
|
110
|
+
export function parseDuration(value) {
|
|
111
|
+
const match = /^(?<amount>\d+)(?<unit>s|m|h|d)$/u.exec(value);
|
|
112
|
+
if (match?.groups === undefined) {
|
|
113
|
+
throw new CliError(`${value} is not a duration`, "usage", "Write it with a unit: 30s, 5m, 2h, 1d.");
|
|
114
|
+
}
|
|
115
|
+
const amount = Number(match.groups["amount"]);
|
|
116
|
+
const seconds = { d: 86_400, h: 3_600, m: 60, s: 1 }[match.groups["unit"]];
|
|
117
|
+
return amount * seconds;
|
|
118
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CliRuntime } from "./runtime.js";
|
|
2
|
+
/**
|
|
3
|
+
* One run of `greatwait`, from argv to an exit code.
|
|
4
|
+
*
|
|
5
|
+
* **A function of a runtime rather than a process.** Every case criterion 7
|
|
6
|
+
* names — a browser that will not open, a callback that never arrives, a
|
|
7
|
+
* deadline passing, Ctrl-C halfway through a wait, a configuration path on
|
|
8
|
+
* another platform — is a fact about the surroundings, and this shape is what
|
|
9
|
+
* lets a test construct one. `main.ts` is the only file that knows there is a
|
|
10
|
+
* process at all.
|
|
11
|
+
*/
|
|
12
|
+
declare const USAGE = "greatwait \u2014 the Greatwait command line\n\n greatwait login [--api-url <origin>] [--api-key] [--no-browser]\n greatwait logout [--api-url <origin>]\n greatwait whoami\n greatwait doctor\n greatwait context [list | use <origin>]\n greatwait journey\n\n greatwait destinations create --url <https receiver>\n greatwait destinations disable <destination>\n greatwait schedules create --endpoint <destination> (--in 30s | --at <instant>) [--data <json>]\n greatwait schedules read <schedule>\n greatwait occurrences wait <schedule> <occurrence>\n greatwait attempts list <schedule> <occurrence>\n\n --json machine-readable output, versioned\n --help this text\n\nRun `greatwait journey` for the published first-value sequence.";
|
|
13
|
+
export declare function run(runtime: CliRuntime): Promise<number>;
|
|
14
|
+
export { USAGE };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { GreatwaitClientError } from "@greatwait/api-client";
|
|
2
|
+
import { parseArguments } from "./args.js";
|
|
3
|
+
import { COMMANDS } from "./commands.js";
|
|
4
|
+
import { UnsafeConfigError } from "./config.js";
|
|
5
|
+
import { EXIT_CLASSES, classifyStatus } from "./exit.js";
|
|
6
|
+
import { renderFailure, renderResult } from "./output.js";
|
|
7
|
+
import { CliError, InterruptedError } from "./runtime.js";
|
|
8
|
+
/**
|
|
9
|
+
* One run of `greatwait`, from argv to an exit code.
|
|
10
|
+
*
|
|
11
|
+
* **A function of a runtime rather than a process.** Every case criterion 7
|
|
12
|
+
* names — a browser that will not open, a callback that never arrives, a
|
|
13
|
+
* deadline passing, Ctrl-C halfway through a wait, a configuration path on
|
|
14
|
+
* another platform — is a fact about the surroundings, and this shape is what
|
|
15
|
+
* lets a test construct one. `main.ts` is the only file that knows there is a
|
|
16
|
+
* process at all.
|
|
17
|
+
*/
|
|
18
|
+
const USAGE = `greatwait — the Greatwait command line
|
|
19
|
+
|
|
20
|
+
greatwait login [--api-url <origin>] [--api-key] [--no-browser]
|
|
21
|
+
greatwait logout [--api-url <origin>]
|
|
22
|
+
greatwait whoami
|
|
23
|
+
greatwait doctor
|
|
24
|
+
greatwait context [list | use <origin>]
|
|
25
|
+
greatwait journey
|
|
26
|
+
|
|
27
|
+
greatwait destinations create --url <https receiver>
|
|
28
|
+
greatwait destinations disable <destination>
|
|
29
|
+
greatwait schedules create --endpoint <destination> (--in 30s | --at <instant>) [--data <json>]
|
|
30
|
+
greatwait schedules read <schedule>
|
|
31
|
+
greatwait occurrences wait <schedule> <occurrence>
|
|
32
|
+
greatwait attempts list <schedule> <occurrence>
|
|
33
|
+
|
|
34
|
+
--json machine-readable output, versioned
|
|
35
|
+
--help this text
|
|
36
|
+
|
|
37
|
+
Run \`greatwait journey\` for the published first-value sequence.`;
|
|
38
|
+
/**
|
|
39
|
+
* Which command a set of leading words names.
|
|
40
|
+
*
|
|
41
|
+
* Two words before one, so `schedules read` is found before a hypothetical
|
|
42
|
+
* `schedules`. Written as a lookup rather than a switch because the same table
|
|
43
|
+
* is what `--help` and the tests enumerate.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Every word that can begin a command.
|
|
47
|
+
*
|
|
48
|
+
* Handed to the parser so it can tell `greatwait --api-key login` — a
|
|
49
|
+
* legitimate, position-independent line — from a credential typed where a
|
|
50
|
+
* value would go. Derived from the table rather than listed, so a command
|
|
51
|
+
* added later needs no second edit.
|
|
52
|
+
*/
|
|
53
|
+
const COMMAND_WORDS = new Set(Object.keys(COMMANDS).map((key) => key.split(" ")[0]));
|
|
54
|
+
function select(words) {
|
|
55
|
+
for (const length of [2, 1]) {
|
|
56
|
+
const key = words.slice(0, length).join(" ");
|
|
57
|
+
if (Object.hasOwn(COMMANDS, key)) {
|
|
58
|
+
return { key, rest: words.slice(length) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function problemOf(error) {
|
|
64
|
+
return error instanceof GreatwaitClientError ? error.problem : undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The class and the sentence for anything that stopped a command.
|
|
68
|
+
*
|
|
69
|
+
* **The service's own refusal is preferred to this command's paraphrase.** A
|
|
70
|
+
* problem document carries a title and a detail written for the person reading
|
|
71
|
+
* them, and a client that replaced those with its own wording would be
|
|
72
|
+
* maintaining a second copy of every refusal the service can produce.
|
|
73
|
+
*/
|
|
74
|
+
function classify(error) {
|
|
75
|
+
if (error instanceof InterruptedError) {
|
|
76
|
+
return {
|
|
77
|
+
exit: "interrupted",
|
|
78
|
+
human: "Stopped. Nothing was cancelled at the service — anything already created is still there.",
|
|
79
|
+
nextMove: undefined,
|
|
80
|
+
problem: undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof CliError) {
|
|
84
|
+
return {
|
|
85
|
+
exit: error.exit,
|
|
86
|
+
human: error.message,
|
|
87
|
+
nextMove: error.nextMove,
|
|
88
|
+
problem: undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof UnsafeConfigError) {
|
|
92
|
+
return {
|
|
93
|
+
exit: "internal",
|
|
94
|
+
human: error.message,
|
|
95
|
+
nextMove: undefined,
|
|
96
|
+
problem: undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (error instanceof GreatwaitClientError) {
|
|
100
|
+
const problem = problemOf(error);
|
|
101
|
+
return {
|
|
102
|
+
exit: classifyStatus(error.status, problem?.code),
|
|
103
|
+
human: problem === undefined
|
|
104
|
+
? `The service answered ${String(error.status)}.`
|
|
105
|
+
: `${problem.title} (${problem.code})`,
|
|
106
|
+
nextMove: problem?.code === "authentication_required"
|
|
107
|
+
? "Sign in: greatwait login"
|
|
108
|
+
: problem?.code === "scope_missing"
|
|
109
|
+
? "This credential does not carry the permission this call needs. Create one that does, rather than widening this one."
|
|
110
|
+
: error.retryable
|
|
111
|
+
? "The identical request is safe to repeat."
|
|
112
|
+
: undefined,
|
|
113
|
+
problem,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// **`internal`, and the message is kept.** Anything reaching here is a defect
|
|
117
|
+
// in this command rather than a refusal the service published, and swallowing
|
|
118
|
+
// the message would leave a person with an exit code and nothing to report.
|
|
119
|
+
return {
|
|
120
|
+
exit: "internal",
|
|
121
|
+
human: error instanceof Error ? error.message : String(error),
|
|
122
|
+
nextMove: undefined,
|
|
123
|
+
problem: undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export async function run(runtime) {
|
|
127
|
+
let json = false;
|
|
128
|
+
let command = "greatwait";
|
|
129
|
+
try {
|
|
130
|
+
const args = parseArguments(runtime.argv, COMMAND_WORDS);
|
|
131
|
+
json = args.booleans.has("--json");
|
|
132
|
+
if (args.booleans.has("--help") || args.command.length === 0) {
|
|
133
|
+
runtime.stdout(`${USAGE}\n`);
|
|
134
|
+
return EXIT_CLASSES.ok;
|
|
135
|
+
}
|
|
136
|
+
const selected = select(args.command);
|
|
137
|
+
if (selected === undefined) {
|
|
138
|
+
throw new CliError(`unknown command ${args.command.join(" ")}`, "usage", "Run greatwait --help for the list.");
|
|
139
|
+
}
|
|
140
|
+
command = selected.key;
|
|
141
|
+
const result = await COMMANDS[selected.key](runtime, { ...args, command: selected.rest });
|
|
142
|
+
// **A command that answered prints on standard output, whatever the news
|
|
143
|
+
// is.** `doctor` reporting a failed check is an answer with a shape, and
|
|
144
|
+
// routing it to `stderr` — or through the error envelope — would hand a
|
|
145
|
+
// script a rendered table where its data should be.
|
|
146
|
+
(result.exit === undefined || json ? runtime.stdout : runtime.stderr)(renderResult(command, result, json));
|
|
147
|
+
// **The sentence for a person reaches standard error under `--json`,
|
|
148
|
+
// whichever way the answer went.** It used to do so only for a failing
|
|
149
|
+
// one, so a success with something worth saying — a grant the service
|
|
150
|
+
// could not describe, an environment that delivers for real — said it
|
|
151
|
+
// nowhere at all. The document is the machine's and this is the person's,
|
|
152
|
+
// and neither has to filter the other.
|
|
153
|
+
if (json && result.nextMove !== undefined) {
|
|
154
|
+
runtime.stderr(`${result.nextMove}\n`);
|
|
155
|
+
}
|
|
156
|
+
return EXIT_CLASSES[result.exit ?? "ok"];
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const classified = classify(error);
|
|
160
|
+
// **A `--json` failure goes to standard output, and a plain one to standard
|
|
161
|
+
// error.** Found by the staging browser suite, which could not parse a
|
|
162
|
+
// refusal because Node had written a deprecation warning to the same stream
|
|
163
|
+
// first — a machine consumer reading `stderr` is reading a channel it does
|
|
164
|
+
// not own and cannot bound. With `--json` the document is the command's
|
|
165
|
+
// answer whether or not it worked, so it belongs where the answer goes;
|
|
166
|
+
// the exit class is what says which it was. Without `--json` the ordinary
|
|
167
|
+
// shell convention holds and diagnostics stay off `stdout`.
|
|
168
|
+
(json ? runtime.stdout : runtime.stderr)(renderFailure(command, {
|
|
169
|
+
exit: classified.exit,
|
|
170
|
+
human: classified.human,
|
|
171
|
+
...(classified.nextMove === undefined
|
|
172
|
+
? {}
|
|
173
|
+
: { nextMove: classified.nextMove }),
|
|
174
|
+
...(classified.problem === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: { problem: classified.problem }),
|
|
177
|
+
}, json));
|
|
178
|
+
// **The next move still reaches a person under `--json`, on the other
|
|
179
|
+
// stream.** It is a sentence written for a reader and it changes whenever
|
|
180
|
+
// it reads badly, so putting it in the document would make every rewording
|
|
181
|
+
// a change to a versioned contract — the thing `--json` exists not to be.
|
|
182
|
+
// Dropping it instead left somebody scripting with no idea what to do,
|
|
183
|
+
// which is how this was found. The machine reads `stdout`, the person reads
|
|
184
|
+
// `stderr`, and neither has to filter the other.
|
|
185
|
+
if (json && classified.nextMove !== undefined) {
|
|
186
|
+
runtime.stderr(`${classified.nextMove}\n`);
|
|
187
|
+
}
|
|
188
|
+
return EXIT_CLASSES[classified.exit];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export { USAGE };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ParsedArguments } from "./args.js";
|
|
2
|
+
import { type StoredCredential } from "./config.js";
|
|
3
|
+
import { type CommandResult } from "./output.js";
|
|
4
|
+
import { type CliRuntime } from "./runtime.js";
|
|
5
|
+
type Command = (runtime: CliRuntime, args: ParsedArguments) => Promise<CommandResult>;
|
|
6
|
+
interface CredentialContextView {
|
|
7
|
+
readonly environmentKind: string;
|
|
8
|
+
readonly principal: string;
|
|
9
|
+
readonly project: string;
|
|
10
|
+
readonly scopes: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/** Every command, keyed by the words that select it. */
|
|
13
|
+
export declare const COMMANDS: Readonly<Record<string, Command>>;
|
|
14
|
+
export type { Command, CredentialContextView, StoredCredential };
|