@greatwait/cli 0.1.0-alpha.5 → 0.1.0-alpha.7
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/catalog.d.ts +4 -5
- package/dist/catalog.js +507 -175
- package/dist/commands.js +208 -56
- package/dist/member-catalog.d.ts +1 -0
- package/dist/member-catalog.js +1 -0
- package/dist/member-commands.d.ts +6 -0
- package/dist/member-commands.js +192 -0
- package/dist/operate.d.ts +21 -0
- package/dist/operate.js +130 -6
- package/dist/remediation.d.ts +1 -1
- package/dist/support.d.ts +2 -0
- package/dist/support.js +7 -0
- package/package.json +3 -3
package/dist/operate.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { autoPauseAfterFailuresSchema, httpRequestSchema, sanitizedDiagnosticText, diagnosticBodyText, responseBodySentence, requestCaptureSentence, attemptStatusGuidance, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
|
|
2
3
|
import { parseDuration } from "./args.js";
|
|
3
4
|
import { confirmChange } from "./confirm.js";
|
|
4
5
|
import { redactSecret } from "./output.js";
|
|
5
6
|
import { CliError } from "./runtime.js";
|
|
6
7
|
import { openSession } from "./session.js";
|
|
7
|
-
import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, table, } from "./support.js";
|
|
8
|
+
import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, readPrivateJsonOption, table, } from "./support.js";
|
|
8
9
|
/* -------------------------------------------------------------------------- */
|
|
9
10
|
/* Shared plumbing */
|
|
10
11
|
/* -------------------------------------------------------------------------- */
|
|
@@ -102,18 +103,85 @@ async function scheduleRequest(runtime, args, usage) {
|
|
|
102
103
|
? { everySeconds: parseDuration(value) }
|
|
103
104
|
: { cron: value };
|
|
104
105
|
const endpoint = args.options["--endpoint"];
|
|
106
|
+
const url = args.options["--url"];
|
|
107
|
+
if (endpoint !== undefined && url !== undefined) {
|
|
108
|
+
// Refused here as well as by the service, because "you gave both" is a
|
|
109
|
+
// sentence about the line somebody typed. The service's own refusal says
|
|
110
|
+
// the same thing a round trip later.
|
|
111
|
+
throw new CliError("give --endpoint or --url, not both", "usage", "--url uses this project's destination at that address, or creates one. --endpoint names a destination that already exists.");
|
|
112
|
+
}
|
|
105
113
|
const data = await readJsonOption(runtime, "--data", args.options["--data"]);
|
|
114
|
+
const httpInput = await readPrivateJsonOption(runtime, "--http", args.options["--http"]);
|
|
115
|
+
const http = httpInput === undefined
|
|
116
|
+
? undefined
|
|
117
|
+
: httpRequestSchema.safeParse(httpInput);
|
|
118
|
+
if (http !== undefined && !http.success)
|
|
119
|
+
throw new CliError("--http settings are invalid", "usage", http.error.issues.map((issue) => issue.message).join("; "));
|
|
120
|
+
const pauseInput = args.options["--auto-pause-after-failures"];
|
|
121
|
+
const pause = pauseInput === undefined
|
|
122
|
+
? undefined
|
|
123
|
+
: autoPauseAfterFailuresSchema.safeParse(pauseInput === "never" ? pauseInput : Number(pauseInput));
|
|
124
|
+
if (pause !== undefined && !pause.success)
|
|
125
|
+
throw new CliError('--auto-pause-after-failures must be a whole number from 1 to 100, or "never"', "usage");
|
|
106
126
|
const latencyClass = args.options["--latency-class"];
|
|
107
127
|
const partitionKey = args.options["--partition-key"];
|
|
108
128
|
return {
|
|
109
129
|
...(data === undefined ? {} : { data }),
|
|
130
|
+
...(http?.success ? { http: http.data } : {}),
|
|
110
131
|
...(endpoint === undefined ? {} : { endpoint }),
|
|
132
|
+
...(url === undefined ? {} : { url }),
|
|
111
133
|
...(latencyClass === undefined ? {} : { latencyClass }),
|
|
134
|
+
...(pause?.success ? { autoPauseAfterFailures: pause.data } : {}),
|
|
112
135
|
...(partitionKey === undefined ? {} : { partitionKey }),
|
|
113
136
|
...(timeZone === undefined ? {} : { timeZone }),
|
|
114
137
|
...recurrence,
|
|
115
138
|
};
|
|
116
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Where a schedule written with `--url` delivers, and where its signing
|
|
142
|
+
* secret is.
|
|
143
|
+
*
|
|
144
|
+
* **Only the API's notice, never a secret.** A schedule write that names a
|
|
145
|
+
* `url` answers with `endpointSigningSecret` — `returned: false` and the
|
|
146
|
+
* dashboard page where a member reveals the secret — and nothing else about
|
|
147
|
+
* it, so there is nothing here that could print one. Absent when the write
|
|
148
|
+
* named `--endpoint`: that caller already holds the destination it chose.
|
|
149
|
+
*/
|
|
150
|
+
export function destinationLines(schedule, notice,
|
|
151
|
+
/**
|
|
152
|
+
* The `--url` that was sent. A schedule write answers without
|
|
153
|
+
* `endpointUrl` (only reads carry it), and the address is the one the
|
|
154
|
+
* destination was found or made at.
|
|
155
|
+
*/
|
|
156
|
+
requestedUrl) {
|
|
157
|
+
if (notice === undefined)
|
|
158
|
+
return [];
|
|
159
|
+
const address = schedule.endpointUrl ?? canonical(requestedUrl);
|
|
160
|
+
return [
|
|
161
|
+
...(schedule.endpoint === undefined
|
|
162
|
+
? []
|
|
163
|
+
: [
|
|
164
|
+
`Destination ${schedule.endpoint}${address === undefined ? "" : ` at ${address}`}`,
|
|
165
|
+
]),
|
|
166
|
+
``,
|
|
167
|
+
`GreatWait signs every call to this address. Your receiver checks the`,
|
|
168
|
+
`signature with the destination's signing secret, which isn't shown here.`,
|
|
169
|
+
notice.revealUrl === undefined
|
|
170
|
+
? `A project member can reveal it on the destination's page in the dashboard.`
|
|
171
|
+
: `Reveal it on the destination's page: ${notice.revealUrl}`,
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
/** An address as the service stores it, or as typed if it will not parse. */
|
|
175
|
+
function canonical(url) {
|
|
176
|
+
if (url === undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
try {
|
|
179
|
+
return new URL(url).href;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return url;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
117
185
|
/** How a schedule's recurrence reads in one column. */
|
|
118
186
|
function recurrenceOf(schedule) {
|
|
119
187
|
return schedule.recurrence.kind;
|
|
@@ -176,6 +244,12 @@ const destinationsUpdate = async (runtime, args) => {
|
|
|
176
244
|
if (url === undefined) {
|
|
177
245
|
throw new CliError(`${runtime.programName} destinations update needs --url`, "usage", `Usage: ${runtime.programName} destinations update <destination> --url https://your-receiver.example.com/greatwait`);
|
|
178
246
|
}
|
|
247
|
+
const headerInput = await readPrivateJsonOption(runtime, "--headers", args.options["--headers"]);
|
|
248
|
+
const headers = headerInput === undefined
|
|
249
|
+
? undefined
|
|
250
|
+
: endpointHeaderChangesSchema.safeParse(headerInput);
|
|
251
|
+
if (headers !== undefined && !headers.success)
|
|
252
|
+
throw new CliError("--headers settings are invalid", "usage", headers.error.issues.map((issue) => issue.message).join("; "));
|
|
179
253
|
const active = await session(runtime, args);
|
|
180
254
|
// Read first, so the preview is the destination as it stands rather than the
|
|
181
255
|
// arguments read back.
|
|
@@ -188,7 +262,7 @@ const destinationsUpdate = async (runtime, args) => {
|
|
|
188
262
|
`Deliveries in flight keep going to the receiver they started for.`,
|
|
189
263
|
],
|
|
190
264
|
});
|
|
191
|
-
const updated = parseResponse("endpoints.update", await active.client.request("endpoints_update", id, { url }, conditionalHeaders(args)));
|
|
265
|
+
const updated = parseResponse("endpoints.update", await active.client.request("endpoints_update", id, { url, ...(headers?.success ? { headers: headers.data } : {}) }, conditionalHeaders(args)));
|
|
192
266
|
return {
|
|
193
267
|
data: { confirmation, endpoint: updated },
|
|
194
268
|
human: endpointBlock(updated).join("\n"),
|
|
@@ -279,6 +353,9 @@ async function scheduleChange(runtime, args, options) {
|
|
|
279
353
|
return {
|
|
280
354
|
data: {
|
|
281
355
|
...(confirmation === undefined ? {} : { confirmation }),
|
|
356
|
+
...(view.endpointSigningSecret === undefined
|
|
357
|
+
? {}
|
|
358
|
+
: { endpointSigningSecret: view.endpointSigningSecret }),
|
|
282
359
|
...(view.occurrence === undefined ? {} : { occurrence: view.occurrence }),
|
|
283
360
|
schedule: view.schedule,
|
|
284
361
|
},
|
|
@@ -292,12 +369,13 @@ async function scheduleChange(runtime, args, options) {
|
|
|
292
369
|
: [
|
|
293
370
|
`Next ${view.occurrence.id} planned for ${view.occurrence.plannedFor} (${view.occurrence.state})`,
|
|
294
371
|
]),
|
|
372
|
+
...destinationLines(view.schedule, view.endpointSigningSecret, args.options["--url"]),
|
|
295
373
|
].join("\n"),
|
|
296
374
|
...(nextMove === undefined ? {} : { nextMove }),
|
|
297
375
|
};
|
|
298
376
|
}
|
|
299
377
|
const schedulesReplace = async (runtime, args) => {
|
|
300
|
-
const usage = `${runtime.programName} schedules replace <schedule> --endpoint <destination> (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
|
|
378
|
+
const usage = `${runtime.programName} schedules replace <schedule> (--endpoint <destination> | --url <https url>) (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
|
|
301
379
|
const [id] = positionals(args, ["<schedule>"], usage);
|
|
302
380
|
const request = await scheduleRequest(runtime, args, usage);
|
|
303
381
|
return scheduleChange(runtime, args, {
|
|
@@ -306,7 +384,8 @@ const schedulesReplace = async (runtime, args) => {
|
|
|
306
384
|
action: `Replace the definition of schedule ${current.id}.`,
|
|
307
385
|
effect: [
|
|
308
386
|
`now ${recurrenceOf(current)}, ${current.status}, version ${String(current.version)}`,
|
|
309
|
-
`after the
|
|
387
|
+
`after the timing on this command line. Destination, data and HTTP`,
|
|
388
|
+
` settings are kept unless this command line gives new ones.`,
|
|
310
389
|
`Occurrences already planned under the old definition are discarded.`,
|
|
311
390
|
],
|
|
312
391
|
}),
|
|
@@ -423,9 +502,52 @@ const attemptsRead = async (runtime, args) => {
|
|
|
423
502
|
const active = await session(runtime, args);
|
|
424
503
|
const view = parseResponse("attempts.read", await active.client.request("attempts_read", schedule, occurrence, attempt));
|
|
425
504
|
const found = view.attempt;
|
|
505
|
+
const responseFile = args.options["--response-file"];
|
|
506
|
+
if (responseFile !== undefined) {
|
|
507
|
+
if (found.response?.body === undefined)
|
|
508
|
+
throw new CliError(responseBodySentence(found.response?.bodyState ?? "not_captured", found.responseStatus === undefined), "validation");
|
|
509
|
+
const body = found.response.body;
|
|
510
|
+
try {
|
|
511
|
+
await writeFile(responseFile, body.encoding === "base64"
|
|
512
|
+
? Buffer.from(body.content, "base64")
|
|
513
|
+
: Buffer.from(body.content, "utf8"), { mode: 0o600, flag: "wx" });
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
throw new CliError("The response file could not be created. Choose a path that does not already exist.", "usage");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const diagnosticLines = [];
|
|
520
|
+
if (found.response !== undefined) {
|
|
521
|
+
const response = found.response;
|
|
522
|
+
diagnosticLines.push(`Server response ${String(found.responseStatus ?? "")} ${response.statusText}`, ...response.headers.map((h) => `${h.name}: ${h.value}`));
|
|
523
|
+
if (response.body === undefined)
|
|
524
|
+
diagnosticLines.push(responseBodySentence(response.bodyState, found.responseStatus === undefined));
|
|
525
|
+
else {
|
|
526
|
+
const body = response.body;
|
|
527
|
+
if (body.incomplete)
|
|
528
|
+
diagnosticLines.push("The response stream stopped during capture; this is a partial answer.");
|
|
529
|
+
if (body.truncated)
|
|
530
|
+
diagnosticLines.push(`Response cut at 10 KiB (10,240 bytes)${body.originalBytes === undefined ? "" : `; original length ${String(body.originalBytes)} bytes`}.`);
|
|
531
|
+
diagnosticLines.push(diagnosticBodyText(body) !== undefined
|
|
532
|
+
? diagnosticBodyText(body)
|
|
533
|
+
: `Binary response (${String(body.capturedBytes)} bytes). Download with --response-file <path>.`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
diagnosticLines.push("What we sent");
|
|
537
|
+
if (found.request === undefined)
|
|
538
|
+
diagnosticLines.push(requestCaptureSentence(found.requestState, found.state === undefined));
|
|
539
|
+
else
|
|
540
|
+
diagnosticLines.push(`${found.request.method} ${found.request.url}`, ...found.request.headers.map((h) => `${h.name}: ${h.value}`), found.request.body === "" ? "We sent no body." : found.request.body);
|
|
541
|
+
const guidance = attemptStatusGuidance(found.responseStatus);
|
|
542
|
+
if (guidance !== undefined)
|
|
543
|
+
diagnosticLines.push(guidance);
|
|
426
544
|
return {
|
|
427
545
|
data: { attempt: found, freshness: freshnessOf(view) },
|
|
428
546
|
human: [
|
|
547
|
+
...diagnosticLines.map(sanitizedDiagnosticText),
|
|
548
|
+
...(responseFile === undefined
|
|
549
|
+
? []
|
|
550
|
+
: [`Response saved to ${sanitizedDiagnosticText(responseFile)}`]),
|
|
429
551
|
`Attempt ${found.id}`,
|
|
430
552
|
`Number ${String(found.attemptNumber)}`,
|
|
431
553
|
...(found.state === undefined ? [] : [`State ${found.state}`]),
|
|
@@ -1387,7 +1509,9 @@ const sinksCreate = async (runtime, args) => {
|
|
|
1387
1509
|
const program = await readJsonOption(runtime, "--program", args.options["--program"]);
|
|
1388
1510
|
const sink = parseResponse("testsinks.create", await active.client.request("testsinks_create", {
|
|
1389
1511
|
endpoint,
|
|
1390
|
-
|
|
1512
|
+
// The program is passed as typed and validated by the service, which
|
|
1513
|
+
// names the field it refuses; the client's type cannot know it yet.
|
|
1514
|
+
...(program === undefined ? {} : { program: program }),
|
|
1391
1515
|
}));
|
|
1392
1516
|
return {
|
|
1393
1517
|
data: { sink },
|
package/dist/remediation.d.ts
CHANGED
|
@@ -30,6 +30,6 @@ export declare function remediationFor(problem: Problem | undefined, retryable:
|
|
|
30
30
|
* A case asserting what the advice *says* wants one spelling; a case asserting
|
|
31
31
|
* that it follows the invoked name passes its own.
|
|
32
32
|
*/
|
|
33
|
-
export declare const REMEDIATION: Readonly<Record<"
|
|
33
|
+
export declare const REMEDIATION: Readonly<Record<"authentication_required" | "billing_not_ready" | "capability_not_configured" | "credential_expired" | "credential_revoked" | "idempotency_key_reused" | "invalid_request" | "not_found" | "payload_too_large" | "plan_limit_reached" | "project_archived" | "rate_limited" | "revision_conflict" | "scope_missing" | "service_unavailable" | "state_conflict" | "test_to_live_escalation" | "version_conflict", string>>;
|
|
34
34
|
/** Exposed so a case can assert the records cover the published vocabularies. */
|
|
35
35
|
export { isRetryable, remediationTable };
|
package/dist/support.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export declare function expectNoExtraArguments(args: ParsedArguments, used: numb
|
|
|
31
31
|
* "unexpected argument" rule is applied from the same count.
|
|
32
32
|
*/
|
|
33
33
|
export declare function positionals(args: ParsedArguments, names: readonly string[], usage: string): readonly string[];
|
|
34
|
+
/** Secret-bearing option documents are accepted through files/stdin only. */
|
|
35
|
+
export declare function readPrivateJsonOption(runtime: CliRuntime, flag: string, value: string | undefined): Promise<unknown>;
|
|
34
36
|
/** The payload, from an argument, a file, or standard input. */
|
|
35
37
|
export declare function readJsonOption(runtime: CliRuntime, flag: string, value: string | undefined): Promise<unknown>;
|
|
36
38
|
/**
|
package/dist/support.js
CHANGED
|
@@ -76,6 +76,13 @@ export function positionals(args, names, usage) {
|
|
|
76
76
|
expectNoExtraArguments(args, names.length, usage);
|
|
77
77
|
return found;
|
|
78
78
|
}
|
|
79
|
+
/** Secret-bearing option documents are accepted through files/stdin only. */
|
|
80
|
+
export async function readPrivateJsonOption(runtime, flag, value) {
|
|
81
|
+
if (value !== undefined && value !== "-" && !value.startsWith("@")) {
|
|
82
|
+
throw new CliError(`${flag} must be @file.json or -`, "usage", "Read secret-bearing settings from a private file or standard input, never a command-line argument.");
|
|
83
|
+
}
|
|
84
|
+
return readJsonOption(runtime, flag, value);
|
|
85
|
+
}
|
|
79
86
|
/** The payload, from an argument, a file, or standard input. */
|
|
80
87
|
export async function readJsonOption(runtime, flag, value) {
|
|
81
88
|
if (value === undefined)
|
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"url": "https://github.com/flowxo/greatwait/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@greatwait/api-client": "0.1.0-alpha.
|
|
11
|
-
"@greatwait/contracts": "0.1.0-alpha.
|
|
10
|
+
"@greatwait/api-client": "0.1.0-alpha.7",
|
|
11
|
+
"@greatwait/contracts": "0.1.0-alpha.7"
|
|
12
12
|
},
|
|
13
13
|
"description": "The GreatWait command line: sign in, create a destination and a one-shot schedule, and wait for the callback, over the public API.",
|
|
14
14
|
"engines": {
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
},
|
|
49
49
|
"sideEffects": false,
|
|
50
50
|
"type": "module",
|
|
51
|
-
"version": "0.1.0-alpha.
|
|
51
|
+
"version": "0.1.0-alpha.7"
|
|
52
52
|
}
|