@greatwait/cli 0.1.0-alpha.3 → 0.1.0-alpha.5

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/dist/exit.d.ts CHANGED
@@ -33,6 +33,18 @@ export declare const EXIT_CLASSES: {
33
33
  readonly retryable: 6;
34
34
  /** A deadline this command was given passed before the answer arrived. */
35
35
  readonly timeout: 7;
36
+ /**
37
+ * The service is not configured to do this, and repeating will not change
38
+ * that.
39
+ *
40
+ * **A separate number from `retryable` because a script must not loop on
41
+ * it.** Both are 5xx and both mean "the service, not you" — but one clears
42
+ * on its own and the other waits on somebody changing the deployment. Until
43
+ * FXO-2497 these shared class 6, so a client told "the identical request is
44
+ * safe to repeat" would retry a condition that is permanent, and a customer
45
+ * was offered a "Try again" control that could never work.
46
+ */
47
+ readonly unconfigured: 8;
36
48
  /**
37
49
  * Interrupted.
38
50
  *
@@ -44,6 +56,15 @@ export declare const EXIT_CLASSES: {
44
56
  readonly interrupted: 130;
45
57
  };
46
58
  export type ExitClass = keyof typeof EXIT_CLASSES;
59
+ /**
60
+ * What each class means, in the words the reference publishes.
61
+ *
62
+ * **Beside the numbers rather than in the page**, because a table of exit codes
63
+ * written out in documentation is a table that describes last month's classes.
64
+ * The reference renders this, so adding a class without describing it does not
65
+ * compile.
66
+ */
67
+ export declare const EXIT_CLASS_MEANINGS: Readonly<Record<ExitClass, string>>;
47
68
  /**
48
69
  * The class an HTTP status falls in.
49
70
  *
@@ -51,8 +72,9 @@ export type ExitClass = keyof typeof EXIT_CLASSES;
51
72
  * A refusal from an edge proxy carries no problem document at all — there is no
52
73
  * code to read — and a client that branched on the code first would classify
53
74
  * every one of those as internal. The code refines the answer where it
54
- * disagrees with the status, which is exactly once: `scope_missing` is a `403`
55
- * and belongs in `authorization`, and everything else in the 4xx range that is
75
+ * disagrees with the status, which is exactly twice: `scope_missing` is a `403`
76
+ * and belongs in `authorization`, `capability_not_configured` is a `503` that
77
+ * must not be called retryable, and everything else in the 4xx range that is
56
78
  * not authentication is the caller's request to fix.
57
79
  */
58
80
  export declare function classifyStatus(status: number, problemCode?: string): ExitClass;
package/dist/exit.js CHANGED
@@ -33,6 +33,18 @@ export const EXIT_CLASSES = {
33
33
  retryable: 6,
34
34
  /** A deadline this command was given passed before the answer arrived. */
35
35
  timeout: 7,
36
+ /**
37
+ * The service is not configured to do this, and repeating will not change
38
+ * that.
39
+ *
40
+ * **A separate number from `retryable` because a script must not loop on
41
+ * it.** Both are 5xx and both mean "the service, not you" — but one clears
42
+ * on its own and the other waits on somebody changing the deployment. Until
43
+ * FXO-2497 these shared class 6, so a client told "the identical request is
44
+ * safe to repeat" would retry a condition that is permanent, and a customer
45
+ * was offered a "Try again" control that could never work.
46
+ */
47
+ unconfigured: 8,
36
48
  /**
37
49
  * Interrupted.
38
50
  *
@@ -43,6 +55,26 @@ export const EXIT_CLASSES = {
43
55
  */
44
56
  interrupted: 130,
45
57
  };
58
+ /**
59
+ * What each class means, in the words the reference publishes.
60
+ *
61
+ * **Beside the numbers rather than in the page**, because a table of exit codes
62
+ * written out in documentation is a table that describes last month's classes.
63
+ * The reference renders this, so adding a class without describing it does not
64
+ * compile.
65
+ */
66
+ export const EXIT_CLASS_MEANINGS = {
67
+ authentication: "No usable credential. Sign in.",
68
+ authorization: "A credential that authenticated and may not do this.",
69
+ internal: "A defect in this program.",
70
+ interrupted: "Stopped — Ctrl-C, or a confirmation declined.",
71
+ ok: "It did what it was asked.",
72
+ retryable: "The service could not answer now. The identical request may work later.",
73
+ timeout: "A deadline this command was given passed before the answer arrived.",
74
+ unconfigured: "The service is not configured to do this. Repeating will not help; it needs somebody to change the service.",
75
+ usage: "The command line was wrong.",
76
+ validation: "The request was refused on its merits: invalid, missing, or conflicting.",
77
+ };
46
78
  /**
47
79
  * The class an HTTP status falls in.
48
80
  *
@@ -50,13 +82,23 @@ export const EXIT_CLASSES = {
50
82
  * A refusal from an edge proxy carries no problem document at all — there is no
51
83
  * code to read — and a client that branched on the code first would classify
52
84
  * every one of those as internal. The code refines the answer where it
53
- * disagrees with the status, which is exactly once: `scope_missing` is a `403`
54
- * and belongs in `authorization`, and everything else in the 4xx range that is
85
+ * disagrees with the status, which is exactly twice: `scope_missing` is a `403`
86
+ * and belongs in `authorization`, `capability_not_configured` is a `503` that
87
+ * must not be called retryable, and everything else in the 4xx range that is
55
88
  * not authentication is the caller's request to fix.
56
89
  */
57
90
  export function classifyStatus(status, problemCode) {
58
91
  if (problemCode === "scope_missing")
59
92
  return "authorization";
93
+ // Without this the status alone decides, and a 503 falls through to
94
+ // `retryable` below — which is the defect: a permanent configuration refusal
95
+ // telling a script that repeating it is safe.
96
+ if (problemCode === "capability_not_configured")
97
+ return "unconfigured";
98
+ // The SDK uses zero when no HTTP answer survives its transport retries.
99
+ // This is a failed exchange, not a successful command.
100
+ if (status === 0)
101
+ return "retryable";
60
102
  if (status === 401)
61
103
  return "authentication";
62
104
  if (status === 403)
package/dist/gw.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/gw.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { start } from "./start.js";
3
+ /**
4
+ * The same command, under the short name.
5
+ *
6
+ * Everything it prints names `gw`, because the whole point of a two-letter
7
+ * command is not typing the long one — and a tool that answers `gw schedules
8
+ * read` with "Next: greatwait attempts list …" hands back the thing it was
9
+ * installed to avoid.
10
+ *
11
+ * `greatwait` stays the canonical name: it is what the published reference, the
12
+ * quickstarts and the first-value journey say, and what one name has to be for
13
+ * a written instruction to be followable by somebody who installed either.
14
+ */
15
+ await start("gw");
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { run, USAGE } from "./cli.js";
1
+ export { run, usage } from "./cli.js";
2
2
  export { COMMANDS } from "./commands.js";
3
3
  export { configDirectory, configLocations, createCredentialStore, originKey, UnsafeConfigError, type CredentialStore, type StoredCredential, } from "./config.js";
4
4
  export { EXIT_CLASSES, classifyStatus, type ExitClass } from "./exit.js";
@@ -6,4 +6,6 @@ export { nodeRuntime, openLoopback } from "./node-runtime.js";
6
6
  export { OUTPUT_VERSION, redactSecret, renderFailure, renderResult, } from "./output.js";
7
7
  export { parseArguments, parseDuration } from "./args.js";
8
8
  export { publishedWait, renderJourney, STEP_COMMANDS } from "./journey.js";
9
+ export { CANONICAL_PROGRAM_NAME } from "./catalog.js";
10
+ export { start } from "./start.js";
9
11
  export { CliError, InterruptedError, type CliRuntime, type LoopbackListener, } from "./runtime.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { run, USAGE } from "./cli.js";
1
+ export { run, usage } from "./cli.js";
2
2
  export { COMMANDS } from "./commands.js";
3
3
  export { configDirectory, configLocations, createCredentialStore, originKey, UnsafeConfigError, } from "./config.js";
4
4
  export { EXIT_CLASSES, classifyStatus } from "./exit.js";
@@ -6,4 +6,6 @@ export { nodeRuntime, openLoopback } from "./node-runtime.js";
6
6
  export { OUTPUT_VERSION, redactSecret, renderFailure, renderResult, } from "./output.js";
7
7
  export { parseArguments, parseDuration } from "./args.js";
8
8
  export { publishedWait, renderJourney, STEP_COMMANDS } from "./journey.js";
9
+ export { CANONICAL_PROGRAM_NAME } from "./catalog.js";
10
+ export { start } from "./start.js";
9
11
  export { CliError, InterruptedError, } from "./runtime.js";
package/dist/journey.d.ts CHANGED
@@ -23,8 +23,8 @@ export interface RenderedStep {
23
23
  readonly purpose: string;
24
24
  readonly title: string;
25
25
  }
26
- export declare function renderStep(step: JourneyStep): RenderedStep;
27
- export declare function renderJourney(): {
26
+ export declare function renderStep(step: JourneyStep, programName?: string): RenderedStep;
27
+ export declare function renderJourney(programName?: string): {
28
28
  readonly prerequisites: readonly {
29
29
  readonly action: string;
30
30
  readonly because: string;
package/dist/journey.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { FIRST_VALUE_JOURNEY, journeyStepVariables, } from "@greatwait/contracts";
2
+ import { CANONICAL_PROGRAM_NAME } from "./catalog.js";
2
3
  /**
3
4
  * The published first-value sequence, rendered for a terminal.
4
5
  *
@@ -25,9 +26,24 @@ export const STEP_COMMANDS = {
25
26
  "read-schedule": "greatwait schedules read <schedule>",
26
27
  "replay-one-shot": "greatwait schedules create --endpoint <destination> --in 30s --idempotency-key <the same key>",
27
28
  };
28
- export function renderStep(step) {
29
+ /**
30
+ * A step's command, under the name this executable was invoked as.
31
+ *
32
+ * **`STEP_COMMANDS` stays canonical and this rewrites the first word.** That
33
+ * table is re-exported into `first-value-projection.ts`, which renders it into
34
+ * the website's journey, the curl page and the JavaScript page — surfaces with
35
+ * no invocation to speak of. So the published text says `greatwait` and the
36
+ * terminal says whichever of the two names is on the person's PATH.
37
+ */
38
+ function stepCommand(key, programName) {
39
+ const command = STEP_COMMANDS[key];
40
+ return command === undefined
41
+ ? undefined
42
+ : command.replace(/^greatwait\b/u, programName);
43
+ }
44
+ export function renderStep(step, programName = CANONICAL_PROGRAM_NAME) {
29
45
  return {
30
- command: STEP_COMMANDS[step.key],
46
+ command: stepCommand(step.key, programName),
31
47
  key: step.key,
32
48
  // What the step must be given, as the journey names it — the path
33
49
  // parameters an earlier step captured, and the references in its body and
@@ -38,14 +54,14 @@ export function renderStep(step) {
38
54
  title: step.title,
39
55
  };
40
56
  }
41
- export function renderJourney() {
57
+ export function renderJourney(programName = CANONICAL_PROGRAM_NAME) {
42
58
  return {
43
59
  prerequisites: FIRST_VALUE_JOURNEY.prerequisites.map((prerequisite) => ({
44
60
  action: prerequisite.action,
45
61
  because: prerequisite.because,
46
62
  title: prerequisite.title,
47
63
  })),
48
- steps: FIRST_VALUE_JOURNEY.steps.map(renderStep),
64
+ steps: FIRST_VALUE_JOURNEY.steps.map((step) => renderStep(step, programName)),
49
65
  };
50
66
  }
51
67
  /**
package/dist/main.js CHANGED
@@ -1,24 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "./cli.js";
3
- import { nodeRuntime } from "./node-runtime.js";
2
+ import { start } from "./start.js";
4
3
  /**
5
- * The installed executable.
4
+ * The installed executable, under the name the package is called.
6
5
  *
7
6
  * **The whole file is the part the repository's own test suite cannot reach**,
8
7
  * which is why criterion 7 asks for a regression against a `bin` that does
9
- * nothing: every case here drives `run` directly, so a package whose source is
8
+ * nothing: every case drives `run` directly, so a package whose source is
10
9
  * perfect and whose `bin` entry points at a file that does not exist passes all
11
10
  * of them. `scripts/verify-clean-install.ts` installs the packed tarball and
12
- * executes this path.
13
- *
14
- * `process.exitCode` rather than `process.exit`: the second truncates whatever
15
- * is still buffered on stdout, and this command's last act is to print.
11
+ * executes this path — and `gw.ts` beside it, because a second `bin` is a
12
+ * second thing that can be wired to nothing.
16
13
  */
17
- const controller = new AbortController();
18
- process.on("SIGINT", () => {
19
- controller.abort();
20
- });
21
- process.on("SIGTERM", () => {
22
- controller.abort();
23
- });
24
- process.exitCode = await run(nodeRuntime(controller.signal));
14
+ await start("greatwait");
@@ -32,4 +32,18 @@ export declare function promptSecret(question: string, streams?: {
32
32
  readonly input: NodeJS.ReadStream;
33
33
  readonly output: NodeJS.WriteStream;
34
34
  }): Promise<string>;
35
- export declare function nodeRuntime(signal: AbortSignal): CliRuntime;
35
+ /**
36
+ * Asks a question on the terminal and reads the answer back, echoed.
37
+ *
38
+ * **The question goes to standard error and the answer is read from standard
39
+ * input**, so a command whose result is being piped somewhere still asks a
40
+ * person who can see it. Ctrl-C at the prompt is an interruption rather than an
41
+ * empty answer, for the same reason it is at the secret prompt: a shell reports
42
+ * 130 for a process the person stopped, and treating it as "no" would be
43
+ * indistinguishable from a deliberate decline in a log.
44
+ */
45
+ export declare function promptLine(question: string, streams?: {
46
+ readonly input: NodeJS.ReadStream;
47
+ readonly output: NodeJS.WriteStream;
48
+ }): Promise<string>;
49
+ export declare function nodeRuntime(signal: AbortSignal, programName: string): CliRuntime;
@@ -49,7 +49,7 @@ export async function openLoopback() {
49
49
  return;
50
50
  }
51
51
  response.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
52
- response.end("Greatwait received the answer. You can close this tab.\n");
52
+ response.end("GreatWait received the answer. You can close this tab.\n");
53
53
  deliver?.(url.searchParams);
54
54
  });
55
55
  await new Promise((resolve, reject) => {
@@ -148,6 +148,34 @@ export async function promptSecret(question, streams = { input: process.stdin, o
148
148
  readline.close();
149
149
  }
150
150
  }
151
+ /**
152
+ * Asks a question on the terminal and reads the answer back, echoed.
153
+ *
154
+ * **The question goes to standard error and the answer is read from standard
155
+ * input**, so a command whose result is being piped somewhere still asks a
156
+ * person who can see it. Ctrl-C at the prompt is an interruption rather than an
157
+ * empty answer, for the same reason it is at the secret prompt: a shell reports
158
+ * 130 for a process the person stopped, and treating it as "no" would be
159
+ * indistinguishable from a deliberate decline in a log.
160
+ */
161
+ export async function promptLine(question, streams = { input: process.stdin, output: process.stderr }) {
162
+ const readline = createInterface({
163
+ input: streams.input,
164
+ output: streams.output,
165
+ });
166
+ try {
167
+ return await new Promise((resolve, reject) => {
168
+ readline.once("SIGINT", () => {
169
+ streams.output.write("\n");
170
+ reject(new InterruptedError());
171
+ });
172
+ readline.question(question, resolve);
173
+ });
174
+ }
175
+ finally {
176
+ readline.close();
177
+ }
178
+ }
151
179
  async function readStdin() {
152
180
  if (process.stdin.isTTY)
153
181
  return "";
@@ -177,7 +205,7 @@ function sleep(milliseconds, signal) {
177
205
  signal.addEventListener("abort", onAbort, { once: true });
178
206
  });
179
207
  }
180
- export function nodeRuntime(signal) {
208
+ export function nodeRuntime(signal, programName) {
181
209
  return {
182
210
  argv: process.argv.slice(2),
183
211
  env: process.env,
@@ -188,6 +216,8 @@ export function nodeRuntime(signal) {
188
216
  now: () => Date.now(),
189
217
  openBrowser,
190
218
  platform: process.platform,
219
+ programName,
220
+ promptLine: (question) => promptLine(question),
191
221
  promptSecret: (question) => promptSecret(question),
192
222
  randomBytes: (length) => crypto.getRandomValues(new Uint8Array(length)),
193
223
  randomUuid: () => crypto.randomUUID(),
package/dist/oauth.d.ts CHANGED
@@ -66,7 +66,7 @@ export declare function buildAuthorization(runtime: CliRuntime, server: Authoriz
66
66
  * else's state is not a code to exchange — it is a request this process did not
67
67
  * make — and reading the code first would exchange it anyway.
68
68
  */
69
- export declare function codeFrom(returned: URLSearchParams, state: string): string;
69
+ export declare function codeFrom(returned: URLSearchParams, state: string, programName: string): string;
70
70
  export declare function exchangeCode(runtime: CliRuntime, server: AuthorizationServer, options: {
71
71
  readonly clientId: string;
72
72
  readonly code: string;
package/dist/oauth.js CHANGED
@@ -17,9 +17,9 @@ async function readJson(runtime, url, init) {
17
17
  }
18
18
  return { body, status: response.status };
19
19
  }
20
- function requireString(value, field, source) {
20
+ function requireString(value, field, source, programName) {
21
21
  if (typeof value !== "string" || value === "") {
22
- throw new CliError(`${source} does not name a ${field}`, "internal", "This origin publishes authorization metadata this command cannot use. Sign in with an API key instead: greatwait login --api-key");
22
+ throw new CliError(`${source} does not name a ${field}`, "internal", `This origin publishes authorization metadata this command cannot use. Sign in with an API key instead: ${programName} login --api-key`);
23
23
  }
24
24
  return value;
25
25
  }
@@ -35,13 +35,13 @@ export async function discover(runtime, origin) {
35
35
  const resourceUrl = `${origin}/.well-known/oauth-protected-resource`;
36
36
  const resource = await readJson(runtime, resourceUrl);
37
37
  if (resource.status !== 200) {
38
- throw new CliError(`${origin} does not publish protected-resource metadata (${String(resource.status)})`, "validation", "Check the origin. A Greatwait API answers GET /__greatwait/version with no credential; try greatwait doctor --api-url <origin>.");
38
+ throw new CliError(`${origin} does not publish protected-resource metadata (${String(resource.status)})`, "validation", `Check the origin. A GreatWait API answers GET /__greatwait/version with no credential; try ${runtime.programName} doctor --api-url <origin>.`);
39
39
  }
40
40
  const resourceBody = resource.body;
41
41
  const servers = Array.isArray(resourceBody.authorization_servers)
42
42
  ? resourceBody.authorization_servers
43
43
  : [];
44
- const issuer = requireString(servers[0], "authorization server", resourceUrl);
44
+ const issuer = requireString(servers[0], "authorization server", resourceUrl, runtime.programName);
45
45
  const metadataUrl = `${issuer}/.well-known/oauth-authorization-server`;
46
46
  const metadata = await readJson(runtime, metadataUrl);
47
47
  if (metadata.status !== 200) {
@@ -50,7 +50,7 @@ export async function discover(runtime, origin) {
50
50
  const body = metadata.body;
51
51
  const scopes = body["scopes_supported"];
52
52
  return {
53
- authorizationEndpoint: requireString(body["authorization_endpoint"], "authorization endpoint", metadataUrl),
53
+ authorizationEndpoint: requireString(body["authorization_endpoint"], "authorization endpoint", metadataUrl, runtime.programName),
54
54
  registrationEndpoint: typeof body["registration_endpoint"] === "string"
55
55
  ? body["registration_endpoint"]
56
56
  : undefined,
@@ -59,23 +59,23 @@ export async function discover(runtime, origin) {
59
59
  // minted for one audience must not be accepted by another, and a client
60
60
  // that names an audience it invented is asserting something it did not
61
61
  // check.
62
- resource: requireString(resourceBody.resource, "resource", resourceUrl),
62
+ resource: requireString(resourceBody.resource, "resource", resourceUrl, runtime.programName),
63
63
  revocationEndpoint: typeof body["revocation_endpoint"] === "string"
64
64
  ? body["revocation_endpoint"]
65
65
  : undefined,
66
66
  scopesSupported: Array.isArray(scopes)
67
67
  ? scopes.filter((scope) => typeof scope === "string")
68
68
  : [],
69
- tokenEndpoint: requireString(body["token_endpoint"], "token endpoint", metadataUrl),
69
+ tokenEndpoint: requireString(body["token_endpoint"], "token endpoint", metadataUrl, runtime.programName),
70
70
  };
71
71
  }
72
72
  export async function registerClient(runtime, server, options) {
73
73
  if (server.registrationEndpoint === undefined) {
74
- throw new CliError("this authorization server does not offer dynamic client registration", "validation", "Sign in with an API key instead: greatwait login --api-key");
74
+ throw new CliError("this authorization server does not offer dynamic client registration", "validation", `Sign in with an API key instead: ${runtime.programName} login --api-key`);
75
75
  }
76
76
  const registered = await readJson(runtime, server.registrationEndpoint, {
77
77
  body: JSON.stringify({
78
- client_name: "Greatwait CLI",
78
+ client_name: "GreatWait CLI",
79
79
  client_uri: "https://greatwait.dev",
80
80
  grant_types: ["authorization_code", "refresh_token"],
81
81
  redirect_uris: [options.redirectUri],
@@ -92,7 +92,7 @@ export async function registerClient(runtime, server, options) {
92
92
  throw new CliError(`dynamic client registration was refused (${String(registered.status)}: ${registered.body?.error ??
93
93
  "no reason given"})`, "validation");
94
94
  }
95
- return requireString(registered.body.client_id, "client_id", server.registrationEndpoint);
95
+ return requireString(registered.body.client_id, "client_id", server.registrationEndpoint, runtime.programName);
96
96
  }
97
97
  export async function buildAuthorization(runtime, server, options) {
98
98
  const state = base64Url(runtime.randomBytes(24));
@@ -116,16 +116,16 @@ export async function buildAuthorization(runtime, server, options) {
116
116
  * else's state is not a code to exchange — it is a request this process did not
117
117
  * make — and reading the code first would exchange it anyway.
118
118
  */
119
- export function codeFrom(returned, state) {
119
+ export function codeFrom(returned, state, programName) {
120
120
  const error = returned.get("error");
121
121
  if (error !== null) {
122
122
  throw new CliError(`authorization was refused (${error})`, "authentication", error === "access_denied"
123
- ? "Nothing was granted. Run greatwait login again and approve the request, or use an API key."
123
+ ? `Nothing was granted. Run ${programName} login again and approve the request, or use an API key.`
124
124
  : undefined);
125
125
  }
126
126
  const returnedState = returned.get("state");
127
127
  if (returnedState !== state) {
128
- throw new CliError(`the redirect carried ${returnedState === null ? "no state" : "a state this run did not issue"}`, "authentication", "A browser tab left open from an earlier sign-in is the usual cause. Close it and run greatwait login again.");
128
+ throw new CliError(`the redirect carried ${returnedState === null ? "no state" : "a state this run did not issue"}`, "authentication", `A browser tab left open from an earlier sign-in is the usual cause. Close it and run ${programName} login again.`);
129
129
  }
130
130
  const code = returned.get("code");
131
131
  if (code === null) {
@@ -142,14 +142,14 @@ async function exchange(runtime, server, form) {
142
142
  if (response.status !== 200) {
143
143
  const error = response.body?.error;
144
144
  throw new CliError(`the authorization server refused the token request (${error ?? String(response.status)})`, "authentication", error === "invalid_grant"
145
- ? "The grant is no longer usable — it expired, was revoked, or was already spent. Run greatwait login again."
145
+ ? `The grant is no longer usable — it expired, was revoked, or was already spent. Run ${runtime.programName} login again.`
146
146
  : undefined);
147
147
  }
148
148
  const body = response.body;
149
149
  return {
150
- accessToken: requireString(body["access_token"], "access_token", server.tokenEndpoint),
150
+ accessToken: requireString(body["access_token"], "access_token", server.tokenEndpoint, runtime.programName),
151
151
  expiresInSeconds: typeof body["expires_in"] === "number" ? body["expires_in"] : 0,
152
- refreshToken: requireString(body["refresh_token"], "refresh_token", server.tokenEndpoint),
152
+ refreshToken: requireString(body["refresh_token"], "refresh_token", server.tokenEndpoint, runtime.programName),
153
153
  scope: typeof body["scope"] === "string" ? body["scope"] : "",
154
154
  };
155
155
  }
@@ -0,0 +1,39 @@
1
+ import { type ParsedArguments } from "./args.js";
2
+ import { type CommandResult } from "./output.js";
3
+ import { type CliRuntime } from "./runtime.js";
4
+ /**
5
+ * Operating and diagnosing, after first value has already happened.
6
+ *
7
+ * W10-01 built the shortest path from an empty machine to an observed callback.
8
+ * This is the rest of the lifecycle: listing what exists, changing it, watching
9
+ * it, and recovering from it. The split is by *when somebody runs it* rather
10
+ * than by resource, which is why `schedules create` is still next to `login`
11
+ * and `schedules cancel` is here.
12
+ *
13
+ * Every command in this file follows the same three rules. It reads before it
14
+ * changes, so the confirmation shows the object as it is now rather than
15
+ * repeating the arguments back. It sends the conditional header when one was
16
+ * given, so a change to something somebody else has moved is refused rather
17
+ * than applied. And it renders identifiers in a column somebody can select,
18
+ * because the next command needs them.
19
+ */
20
+ type Command = (runtime: CliRuntime, args: ParsedArguments) => Promise<CommandResult>;
21
+ /**
22
+ * A schedule definition from the command line.
23
+ *
24
+ * **One recurrence, named explicitly.** The four spellings are the four the
25
+ * contract publishes, and giving none or giving two is refused here rather than
26
+ * sent: "you gave --in and --cron" is a sentence about the line somebody typed,
27
+ * and `invalid_request` a round trip later is not.
28
+ *
29
+ * `--time-zone` is what keeps daylight saving honest. A cron expression without
30
+ * one fires on a fixed offset, so "every weekday at 09:00" drifts an hour twice
31
+ * a year; with one, the service moves the instant and the local time stays put.
32
+ * The command line does not choose a default zone, because guessing the
33
+ * machine's would make the same expression mean different things on a laptop
34
+ * and on a build runner.
35
+ */
36
+ declare function scheduleRequest(runtime: CliRuntime, args: ParsedArguments, usage: string): Promise<Readonly<Record<string, unknown>>>;
37
+ /** The operate-and-diagnose commands, keyed by the words that select them. */
38
+ export declare const OPERATE_COMMANDS: Readonly<Record<string, Command>>;
39
+ export { scheduleRequest, type Command };