@greatwait/cli 0.1.0-alpha.4 → 0.1.0-alpha.6
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/README.md +1 -1
- package/dist/catalog.d.ts +4 -5
- package/dist/catalog.js +551 -168
- package/dist/cli.js +4 -2
- package/dist/commands.js +206 -60
- package/dist/exit.d.ts +15 -2
- package/dist/exit.js +25 -2
- 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/node-runtime.js +1 -1
- package/dist/oauth.js +2 -2
- package/dist/operate.d.ts +21 -0
- package/dist/operate.js +254 -17
- package/dist/remediation.d.ts +1 -1
- package/dist/remediation.js +8 -0
- package/dist/session.js +1 -1
- package/dist/support.d.ts +5 -7
- package/dist/support.js +11 -13
- package/package.json +4 -4
package/dist/operate.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
|
|
1
|
+
import { httpRequestSchema, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
|
|
2
2
|
import { parseDuration } from "./args.js";
|
|
3
3
|
import { confirmChange } from "./confirm.js";
|
|
4
4
|
import { redactSecret } from "./output.js";
|
|
5
5
|
import { CliError } from "./runtime.js";
|
|
6
6
|
import { openSession } from "./session.js";
|
|
7
|
-
import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, table, } from "./support.js";
|
|
7
|
+
import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, readPrivateJsonOption, table, } from "./support.js";
|
|
8
8
|
/* -------------------------------------------------------------------------- */
|
|
9
9
|
/* Shared plumbing */
|
|
10
10
|
/* -------------------------------------------------------------------------- */
|
|
@@ -102,18 +102,78 @@ async function scheduleRequest(runtime, args, usage) {
|
|
|
102
102
|
? { everySeconds: parseDuration(value) }
|
|
103
103
|
: { cron: value };
|
|
104
104
|
const endpoint = args.options["--endpoint"];
|
|
105
|
+
const url = args.options["--url"];
|
|
106
|
+
if (endpoint !== undefined && url !== undefined) {
|
|
107
|
+
// Refused here as well as by the service, because "you gave both" is a
|
|
108
|
+
// sentence about the line somebody typed. The service's own refusal says
|
|
109
|
+
// the same thing a round trip later.
|
|
110
|
+
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.");
|
|
111
|
+
}
|
|
105
112
|
const data = await readJsonOption(runtime, "--data", args.options["--data"]);
|
|
113
|
+
const httpInput = await readPrivateJsonOption(runtime, "--http", args.options["--http"]);
|
|
114
|
+
const http = httpInput === undefined
|
|
115
|
+
? undefined
|
|
116
|
+
: httpRequestSchema.safeParse(httpInput);
|
|
117
|
+
if (http !== undefined && !http.success)
|
|
118
|
+
throw new CliError("--http settings are invalid", "usage", http.error.issues.map((issue) => issue.message).join("; "));
|
|
106
119
|
const latencyClass = args.options["--latency-class"];
|
|
107
120
|
const partitionKey = args.options["--partition-key"];
|
|
108
121
|
return {
|
|
109
122
|
...(data === undefined ? {} : { data }),
|
|
123
|
+
...(http?.success ? { http: http.data } : {}),
|
|
110
124
|
...(endpoint === undefined ? {} : { endpoint }),
|
|
125
|
+
...(url === undefined ? {} : { url }),
|
|
111
126
|
...(latencyClass === undefined ? {} : { latencyClass }),
|
|
112
127
|
...(partitionKey === undefined ? {} : { partitionKey }),
|
|
113
128
|
...(timeZone === undefined ? {} : { timeZone }),
|
|
114
129
|
...recurrence,
|
|
115
130
|
};
|
|
116
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Where a schedule written with `--url` delivers, and where its signing
|
|
134
|
+
* secret is.
|
|
135
|
+
*
|
|
136
|
+
* **Only the API's notice, never a secret.** A schedule write that names a
|
|
137
|
+
* `url` answers with `endpointSigningSecret` — `returned: false` and the
|
|
138
|
+
* dashboard page where a member reveals the secret — and nothing else about
|
|
139
|
+
* it, so there is nothing here that could print one. Absent when the write
|
|
140
|
+
* named `--endpoint`: that caller already holds the destination it chose.
|
|
141
|
+
*/
|
|
142
|
+
export function destinationLines(schedule, notice,
|
|
143
|
+
/**
|
|
144
|
+
* The `--url` that was sent. A schedule write answers without
|
|
145
|
+
* `endpointUrl` (only reads carry it), and the address is the one the
|
|
146
|
+
* destination was found or made at.
|
|
147
|
+
*/
|
|
148
|
+
requestedUrl) {
|
|
149
|
+
if (notice === undefined)
|
|
150
|
+
return [];
|
|
151
|
+
const address = schedule.endpointUrl ?? canonical(requestedUrl);
|
|
152
|
+
return [
|
|
153
|
+
...(schedule.endpoint === undefined
|
|
154
|
+
? []
|
|
155
|
+
: [
|
|
156
|
+
`Destination ${schedule.endpoint}${address === undefined ? "" : ` at ${address}`}`,
|
|
157
|
+
]),
|
|
158
|
+
``,
|
|
159
|
+
`GreatWait signs every call to this address. Your receiver checks the`,
|
|
160
|
+
`signature with the destination's signing secret, which isn't shown here.`,
|
|
161
|
+
notice.revealUrl === undefined
|
|
162
|
+
? `A project member can reveal it on the destination's page in the dashboard.`
|
|
163
|
+
: `Reveal it on the destination's page: ${notice.revealUrl}`,
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
/** An address as the service stores it, or as typed if it will not parse. */
|
|
167
|
+
function canonical(url) {
|
|
168
|
+
if (url === undefined)
|
|
169
|
+
return undefined;
|
|
170
|
+
try {
|
|
171
|
+
return new URL(url).href;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return url;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
117
177
|
/** How a schedule's recurrence reads in one column. */
|
|
118
178
|
function recurrenceOf(schedule) {
|
|
119
179
|
return schedule.recurrence.kind;
|
|
@@ -176,6 +236,12 @@ const destinationsUpdate = async (runtime, args) => {
|
|
|
176
236
|
if (url === undefined) {
|
|
177
237
|
throw new CliError(`${runtime.programName} destinations update needs --url`, "usage", `Usage: ${runtime.programName} destinations update <destination> --url https://your-receiver.example.com/greatwait`);
|
|
178
238
|
}
|
|
239
|
+
const headerInput = await readPrivateJsonOption(runtime, "--headers", args.options["--headers"]);
|
|
240
|
+
const headers = headerInput === undefined
|
|
241
|
+
? undefined
|
|
242
|
+
: endpointHeaderChangesSchema.safeParse(headerInput);
|
|
243
|
+
if (headers !== undefined && !headers.success)
|
|
244
|
+
throw new CliError("--headers settings are invalid", "usage", headers.error.issues.map((issue) => issue.message).join("; "));
|
|
179
245
|
const active = await session(runtime, args);
|
|
180
246
|
// Read first, so the preview is the destination as it stands rather than the
|
|
181
247
|
// arguments read back.
|
|
@@ -188,7 +254,7 @@ const destinationsUpdate = async (runtime, args) => {
|
|
|
188
254
|
`Deliveries in flight keep going to the receiver they started for.`,
|
|
189
255
|
],
|
|
190
256
|
});
|
|
191
|
-
const updated = parseResponse("endpoints.update", await active.client.request("endpoints_update", id, { url }, conditionalHeaders(args)));
|
|
257
|
+
const updated = parseResponse("endpoints.update", await active.client.request("endpoints_update", id, { url, ...(headers?.success ? { headers: headers.data } : {}) }, conditionalHeaders(args)));
|
|
192
258
|
return {
|
|
193
259
|
data: { confirmation, endpoint: updated },
|
|
194
260
|
human: endpointBlock(updated).join("\n"),
|
|
@@ -226,9 +292,11 @@ const destinationsRotateKey = async (runtime, args) => {
|
|
|
226
292
|
``,
|
|
227
293
|
`Signing key ${rotated.secret}`,
|
|
228
294
|
``,
|
|
229
|
-
`This
|
|
295
|
+
`This is the only call that returns the key. Add it to your receiver's`,
|
|
230
296
|
`accepted set now, alongside the current one; reading this destination back`,
|
|
231
|
-
`will show ${redactSecret(rotated.secret)} instead
|
|
297
|
+
`will show ${redactSecret(rotated.secret)} instead. If you lose it, a`,
|
|
298
|
+
`signed-in member can read it again on this destination's page in the`,
|
|
299
|
+
`dashboard; no command here can.`,
|
|
232
300
|
].join("\n"),
|
|
233
301
|
};
|
|
234
302
|
};
|
|
@@ -277,6 +345,9 @@ async function scheduleChange(runtime, args, options) {
|
|
|
277
345
|
return {
|
|
278
346
|
data: {
|
|
279
347
|
...(confirmation === undefined ? {} : { confirmation }),
|
|
348
|
+
...(view.endpointSigningSecret === undefined
|
|
349
|
+
? {}
|
|
350
|
+
: { endpointSigningSecret: view.endpointSigningSecret }),
|
|
280
351
|
...(view.occurrence === undefined ? {} : { occurrence: view.occurrence }),
|
|
281
352
|
schedule: view.schedule,
|
|
282
353
|
},
|
|
@@ -290,12 +361,13 @@ async function scheduleChange(runtime, args, options) {
|
|
|
290
361
|
: [
|
|
291
362
|
`Next ${view.occurrence.id} planned for ${view.occurrence.plannedFor} (${view.occurrence.state})`,
|
|
292
363
|
]),
|
|
364
|
+
...destinationLines(view.schedule, view.endpointSigningSecret, args.options["--url"]),
|
|
293
365
|
].join("\n"),
|
|
294
366
|
...(nextMove === undefined ? {} : { nextMove }),
|
|
295
367
|
};
|
|
296
368
|
}
|
|
297
369
|
const schedulesReplace = async (runtime, args) => {
|
|
298
|
-
const usage = `${runtime.programName} schedules replace <schedule> --endpoint <destination> (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
|
|
370
|
+
const usage = `${runtime.programName} schedules replace <schedule> (--endpoint <destination> | --url <https url>) (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
|
|
299
371
|
const [id] = positionals(args, ["<schedule>"], usage);
|
|
300
372
|
const request = await scheduleRequest(runtime, args, usage);
|
|
301
373
|
return scheduleChange(runtime, args, {
|
|
@@ -304,7 +376,8 @@ const schedulesReplace = async (runtime, args) => {
|
|
|
304
376
|
action: `Replace the definition of schedule ${current.id}.`,
|
|
305
377
|
effect: [
|
|
306
378
|
`now ${recurrenceOf(current)}, ${current.status}, version ${String(current.version)}`,
|
|
307
|
-
`after the
|
|
379
|
+
`after the timing on this command line. Destination, data and HTTP`,
|
|
380
|
+
` settings are kept unless this command line gives new ones.`,
|
|
308
381
|
`Occurrences already planned under the old definition are discarded.`,
|
|
309
382
|
],
|
|
310
383
|
}),
|
|
@@ -445,7 +518,7 @@ const attemptsRead = async (runtime, args) => {
|
|
|
445
518
|
].join("\n"),
|
|
446
519
|
...(found.remediation === "review_before_redelivery"
|
|
447
520
|
? {
|
|
448
|
-
nextMove: "This attempt was ambiguous: the receiver may have processed it even though
|
|
521
|
+
nextMove: "This attempt was ambiguous: the receiver may have processed it even though GreatWait never saw an answer. Check the receiver before redelivering.",
|
|
449
522
|
}
|
|
450
523
|
: {}),
|
|
451
524
|
};
|
|
@@ -462,6 +535,46 @@ function window(args) {
|
|
|
462
535
|
...(to === undefined ? {} : { to: parseInstant(to, "--to") }),
|
|
463
536
|
};
|
|
464
537
|
}
|
|
538
|
+
const limitsShow = async (runtime, args) => {
|
|
539
|
+
positionals(args, [], `${runtime.programName} limits show`);
|
|
540
|
+
const active = await session(runtime, args);
|
|
541
|
+
const view = parseResponse("limits.read", await active.client.request("limits_read"));
|
|
542
|
+
const rows = [
|
|
543
|
+
[
|
|
544
|
+
"Inline payload",
|
|
545
|
+
String(view.limits.inlinePayloadBytes),
|
|
546
|
+
"bytes",
|
|
547
|
+
view.provenance.inlinePayloadBytes,
|
|
548
|
+
],
|
|
549
|
+
[
|
|
550
|
+
"Partition keys",
|
|
551
|
+
view.limits.partitionKeyCeiling === undefined
|
|
552
|
+
? "No configured ceiling"
|
|
553
|
+
: String(view.limits.partitionKeyCeiling),
|
|
554
|
+
"keys",
|
|
555
|
+
view.provenance.partitionKeyCeiling ?? "default",
|
|
556
|
+
],
|
|
557
|
+
[
|
|
558
|
+
"Forecast horizon",
|
|
559
|
+
String(view.limits.forecastHorizonSeconds),
|
|
560
|
+
"seconds",
|
|
561
|
+
view.provenance.forecastHorizonSeconds,
|
|
562
|
+
],
|
|
563
|
+
[
|
|
564
|
+
"Usage window",
|
|
565
|
+
String(view.limits.usageWindowDays),
|
|
566
|
+
"days",
|
|
567
|
+
view.provenance.usageWindowDays,
|
|
568
|
+
],
|
|
569
|
+
];
|
|
570
|
+
return {
|
|
571
|
+
data: view,
|
|
572
|
+
human: [
|
|
573
|
+
table(["SETTING", "VALUE", "UNIT", "ORIGIN"], rows, ""),
|
|
574
|
+
`Policy revisions: global ${String(view.revision.global)}, organization ${String(view.revision.organization)}`,
|
|
575
|
+
].join("\n"),
|
|
576
|
+
};
|
|
577
|
+
};
|
|
465
578
|
const forecast = async (runtime, args) => {
|
|
466
579
|
const active = await session(runtime, args);
|
|
467
580
|
const bucket = args.options["--bucket"];
|
|
@@ -678,18 +791,38 @@ const billingNotificationSettings = async (runtime, args) => {
|
|
|
678
791
|
};
|
|
679
792
|
const billingNotificationSettingsUpdate = async (runtime, args) => {
|
|
680
793
|
const noncritical = args.options["--noncritical-email"];
|
|
794
|
+
// Three meanings, matching the contract: omitted leaves the threshold as it
|
|
795
|
+
// is, chosen or not; `auto` sends null, which returns it to a fifth of the
|
|
796
|
+
// latest credit grant; an amount chooses it. `off` is how other commands
|
|
797
|
+
// spell "no amount", and there is no "no threshold" here, so it is refused
|
|
798
|
+
// along with zero, negatives and fractions of a cent.
|
|
799
|
+
const lowCreditRaw = args.options["--low-credit"];
|
|
800
|
+
const lowCredit = lowCreditRaw === undefined
|
|
801
|
+
? undefined
|
|
802
|
+
: lowCreditRaw === "auto"
|
|
803
|
+
? null
|
|
804
|
+
: budgetMoney(args, "--low-credit");
|
|
805
|
+
const refusal = "--low-credit must be a positive exact-cent USD amount or auto, and --noncritical-email must be on or off";
|
|
806
|
+
if (lowCreditRaw !== undefined &&
|
|
807
|
+
lowCreditRaw !== "auto" &&
|
|
808
|
+
lowCredit === null)
|
|
809
|
+
throw new CliError(refusal, "usage");
|
|
681
810
|
const parsed = billingNotificationSettingsUpdateRequestSchema.safeParse({
|
|
682
811
|
expectedRevision: expectedRevision(args),
|
|
683
|
-
lowCreditThreshold:
|
|
812
|
+
...(lowCredit === undefined ? {} : { lowCreditThreshold: lowCredit }),
|
|
684
813
|
noncriticalEmail: noncritical === "on" ? true : noncritical === "off" ? false : undefined,
|
|
685
814
|
});
|
|
686
815
|
if (!parsed.success) {
|
|
687
|
-
throw new CliError(
|
|
816
|
+
throw new CliError(refusal, "usage");
|
|
688
817
|
}
|
|
689
818
|
const confirmation = await confirmChange(runtime, args, {
|
|
690
819
|
action: "Replace this organization's billing notification settings.",
|
|
691
820
|
effect: [
|
|
692
|
-
|
|
821
|
+
lowCredit === undefined
|
|
822
|
+
? "low-credit threshold unchanged"
|
|
823
|
+
: lowCredit === null
|
|
824
|
+
? "low-credit threshold automatic (a fifth of the latest credit grant)"
|
|
825
|
+
: `low-credit threshold ${money(lowCredit)}`,
|
|
693
826
|
`noncritical email ${parsed.data.noncriticalEmail ? "on" : "off"}`,
|
|
694
827
|
"Critical email remains mandatory for owners and active billing-role members.",
|
|
695
828
|
],
|
|
@@ -877,12 +1010,12 @@ const billingRefundRequest = async (runtime, args) => {
|
|
|
877
1010
|
throw new CliError("Provide a positive exact-cent --amount, --payment-operation and --reason", "usage");
|
|
878
1011
|
}
|
|
879
1012
|
const confirmation = await confirmChange(runtime, args, {
|
|
880
|
-
action: "
|
|
1013
|
+
action: "Reserve this unused purchased credit against the payment for refund.",
|
|
881
1014
|
effect: [
|
|
882
1015
|
`amount ${money(parsed.data.amount)}`,
|
|
883
1016
|
`payment ${parsed.data.paymentOperation}`,
|
|
884
1017
|
`reason ${parsed.data.reason}`,
|
|
885
|
-
"This
|
|
1018
|
+
"This reserves credit only. It does not submit a provider refund or cash out credit.",
|
|
886
1019
|
],
|
|
887
1020
|
});
|
|
888
1021
|
const active = await session(runtime, args);
|
|
@@ -898,7 +1031,7 @@ const billingRefundRequest = async (runtime, args) => {
|
|
|
898
1031
|
`Requested ${view.requestedAt}`,
|
|
899
1032
|
`Retry key ${key}`,
|
|
900
1033
|
].join("\n"),
|
|
901
|
-
nextMove: "
|
|
1034
|
+
nextMove: "A Stripe refund of this payment settles or supersedes the reservation; resolve-refund --decision declined releases it.",
|
|
902
1035
|
};
|
|
903
1036
|
};
|
|
904
1037
|
const billingRefundResolution = async (runtime, args) => {
|
|
@@ -1020,6 +1153,99 @@ const billingChangeProject = async (runtime, args) => {
|
|
|
1020
1153
|
data: { confirmation, idempotencyKey: key, ...view },
|
|
1021
1154
|
};
|
|
1022
1155
|
};
|
|
1156
|
+
const billingPaymentMethod = async (runtime, args) => {
|
|
1157
|
+
const active = await session(runtime, args);
|
|
1158
|
+
const view = parseResponse("billing.paymentmethod", await active.client.request("billing_paymentmethod"));
|
|
1159
|
+
return {
|
|
1160
|
+
data: view,
|
|
1161
|
+
human: paymentMethodLine(view),
|
|
1162
|
+
};
|
|
1163
|
+
};
|
|
1164
|
+
/**
|
|
1165
|
+
* The saved payment method in one line (W9-14). The label is the service's,
|
|
1166
|
+
* so a card reads "Visa ending in 4242" here exactly as on the dashboard, and
|
|
1167
|
+
* Link or Cash App Pay is named rather than reported as no card.
|
|
1168
|
+
*/
|
|
1169
|
+
function paymentMethodLine(view) {
|
|
1170
|
+
const method = view.method;
|
|
1171
|
+
if (method === null)
|
|
1172
|
+
return "No payment method on file. Add one with billing add-card; automatic top-up and paid projects need it.";
|
|
1173
|
+
return method.type === "card"
|
|
1174
|
+
? `Payment method on file: ${method.label}, expires ${String(method.expMonth).padStart(2, "0")}/${String(method.expYear)}`
|
|
1175
|
+
: `Payment method on file: ${method.label}`;
|
|
1176
|
+
}
|
|
1177
|
+
const billingInvoices = async (runtime, args) => {
|
|
1178
|
+
const active = await session(runtime, args);
|
|
1179
|
+
const view = parseResponse("billing.invoices", await active.client.request("billing_invoices"));
|
|
1180
|
+
return {
|
|
1181
|
+
data: view,
|
|
1182
|
+
human: table(["CREATED", "AMOUNT", "STATE", "RECEIPT"], view.data.map((invoice) => [
|
|
1183
|
+
invoice.createdAt,
|
|
1184
|
+
money(invoice.amount),
|
|
1185
|
+
invoice.state,
|
|
1186
|
+
invoice.receiptUrl ?? "",
|
|
1187
|
+
]), "No project subscription charges."),
|
|
1188
|
+
};
|
|
1189
|
+
};
|
|
1190
|
+
const billingPaymentHistory = async (runtime, args) => {
|
|
1191
|
+
const active = await session(runtime, args);
|
|
1192
|
+
return listing(runtime, args, {
|
|
1193
|
+
command: `${runtime.programName} billing payment-history`,
|
|
1194
|
+
columns: ["CREATED", "AMOUNT", "STATE", "RECEIPT"],
|
|
1195
|
+
empty: "No execution-credit payments.",
|
|
1196
|
+
read: (request) => page("billing.paymenthistory", () => active.client.request("billing_paymenthistory", request)),
|
|
1197
|
+
row: (payment) => [
|
|
1198
|
+
payment.createdAt,
|
|
1199
|
+
money(payment.gross ?? payment.credit),
|
|
1200
|
+
payment.state,
|
|
1201
|
+
payment.receiptUrl ?? "",
|
|
1202
|
+
],
|
|
1203
|
+
});
|
|
1204
|
+
};
|
|
1205
|
+
const billingInvoiceHistory = async (runtime, args) => {
|
|
1206
|
+
const active = await session(runtime, args);
|
|
1207
|
+
return listing(runtime, args, {
|
|
1208
|
+
command: `${runtime.programName} billing invoice-history`,
|
|
1209
|
+
columns: ["CREATED", "AMOUNT", "STATE", "RECEIPT"],
|
|
1210
|
+
empty: "No project subscription charges.",
|
|
1211
|
+
read: (request) => page("billing.invoicehistory", () => active.client.request("billing_invoicehistory", request)),
|
|
1212
|
+
row: (invoice) => [
|
|
1213
|
+
invoice.createdAt,
|
|
1214
|
+
money(invoice.amount),
|
|
1215
|
+
invoice.state,
|
|
1216
|
+
invoice.receiptUrl ?? "",
|
|
1217
|
+
],
|
|
1218
|
+
});
|
|
1219
|
+
};
|
|
1220
|
+
const billingAddCard = async (runtime, args) => {
|
|
1221
|
+
await confirmChange(runtime, args, {
|
|
1222
|
+
action: "Create a hosted link for adding or replacing this organization's payment method.",
|
|
1223
|
+
effect: [
|
|
1224
|
+
"The page offers a card, Link and any other method the account accepts. Saving one there charges nothing. It becomes what automatic top-ups and project subscriptions charge.",
|
|
1225
|
+
],
|
|
1226
|
+
});
|
|
1227
|
+
const active = await session(runtime, args);
|
|
1228
|
+
const view = parseResponse("billing.paymentmethodsetup", await active.client.request("billing_paymentmethodsetup", {}));
|
|
1229
|
+
return { data: view, human: view.url };
|
|
1230
|
+
};
|
|
1231
|
+
const billingRemoveCard = async (runtime, args) => {
|
|
1232
|
+
await confirmChange(runtime, args, {
|
|
1233
|
+
action: "Remove this organization's saved payment method.",
|
|
1234
|
+
effect: [
|
|
1235
|
+
"Project subscriptions stop renewing: paid projects keep running until the end of the period already paid for, then stop. The included project keeps running.",
|
|
1236
|
+
"Automatic top-up is turned off immediately. Nothing more is charged.",
|
|
1237
|
+
"Saving a payment method again before the period ends keeps those projects.",
|
|
1238
|
+
],
|
|
1239
|
+
});
|
|
1240
|
+
const active = await session(runtime, args);
|
|
1241
|
+
const view = parseResponse("billing.paymentmethodremove", await active.client.request("billing_paymentmethodremove", {
|
|
1242
|
+
confirmed: true,
|
|
1243
|
+
}));
|
|
1244
|
+
return {
|
|
1245
|
+
data: view,
|
|
1246
|
+
human: "Payment method removed. No payment method on file.",
|
|
1247
|
+
};
|
|
1248
|
+
};
|
|
1023
1249
|
const billingPortal = async (runtime, args) => {
|
|
1024
1250
|
await confirmChange(runtime, args, {
|
|
1025
1251
|
action: "Create this organization's hosted billing recovery link.",
|
|
@@ -1093,7 +1319,9 @@ const billingLedger = async (runtime, args) => {
|
|
|
1093
1319
|
row: (entry) => [
|
|
1094
1320
|
entry.sequence,
|
|
1095
1321
|
entry.recordedAt,
|
|
1096
|
-
entry.
|
|
1322
|
+
entry.note === "provider-details-attached"
|
|
1323
|
+
? `${entry.kind} (details attached)`
|
|
1324
|
+
: entry.kind,
|
|
1097
1325
|
money(entry.amount),
|
|
1098
1326
|
entry.reference.attempt ??
|
|
1099
1327
|
entry.reference.creditLot ??
|
|
@@ -1230,7 +1458,9 @@ const sinksCreate = async (runtime, args) => {
|
|
|
1230
1458
|
const program = await readJsonOption(runtime, "--program", args.options["--program"]);
|
|
1231
1459
|
const sink = parseResponse("testsinks.create", await active.client.request("testsinks_create", {
|
|
1232
1460
|
endpoint,
|
|
1233
|
-
|
|
1461
|
+
// The program is passed as typed and validated by the service, which
|
|
1462
|
+
// names the field it refuses; the client's type cannot know it yet.
|
|
1463
|
+
...(program === undefined ? {} : { program: program }),
|
|
1234
1464
|
}));
|
|
1235
1465
|
return {
|
|
1236
1466
|
data: { sink },
|
|
@@ -1281,7 +1511,7 @@ const sinksDelete = async (runtime, args) => {
|
|
|
1281
1511
|
action: `Delete test sink ${current.id}.`,
|
|
1282
1512
|
effect: [
|
|
1283
1513
|
`received ${String(current.deliveryCount)} deliveries, which go with it`,
|
|
1284
|
-
`The recorded bodies are the only copy
|
|
1514
|
+
`The recorded bodies are the only copy GreatWait keeps. Read anything you`,
|
|
1285
1515
|
`still need first.`,
|
|
1286
1516
|
],
|
|
1287
1517
|
});
|
|
@@ -1345,6 +1575,7 @@ const sinksDelivery = async (runtime, args) => {
|
|
|
1345
1575
|
/* -------------------------------------------------------------------------- */
|
|
1346
1576
|
/** The operate-and-diagnose commands, keyed by the words that select them. */
|
|
1347
1577
|
export const OPERATE_COMMANDS = {
|
|
1578
|
+
"limits show": limitsShow,
|
|
1348
1579
|
"attempts read": attemptsRead,
|
|
1349
1580
|
billing,
|
|
1350
1581
|
"billing budgets": billingBudgets,
|
|
@@ -1364,6 +1595,12 @@ export const OPERATE_COMMANDS = {
|
|
|
1364
1595
|
"billing resolve-refund": billingRefundResolution,
|
|
1365
1596
|
"billing change-project": billingChangeProject,
|
|
1366
1597
|
"billing portal": billingPortal,
|
|
1598
|
+
"billing payment-method": billingPaymentMethod,
|
|
1599
|
+
"billing invoices": billingInvoices,
|
|
1600
|
+
"billing payment-history": billingPaymentHistory,
|
|
1601
|
+
"billing invoice-history": billingInvoiceHistory,
|
|
1602
|
+
"billing add-card": billingAddCard,
|
|
1603
|
+
"billing remove-card": billingRemoveCard,
|
|
1367
1604
|
timing,
|
|
1368
1605
|
"credentials list": credentialsList,
|
|
1369
1606
|
"credentials revoke": credentialsRevoke,
|
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/remediation.js
CHANGED
|
@@ -19,6 +19,14 @@ import { CANONICAL_PROGRAM_NAME } from "./catalog.js";
|
|
|
19
19
|
function remediationTable(programName) {
|
|
20
20
|
return {
|
|
21
21
|
authentication_required: `Sign in: ${programName} login`,
|
|
22
|
+
// Nothing to fix and nothing to do but wait a moment — so the advice says
|
|
23
|
+
// that, rather than sending somebody to look for a fault that is not there.
|
|
24
|
+
billing_not_ready: "Billing for this organization is still being set up. It finishes on its own within a minute; run the command again then. Nothing has been charged and no schedule is affected.",
|
|
25
|
+
// **Not "try again", because trying again is exactly what does not work.**
|
|
26
|
+
// The distinction from `service_unavailable` below is the whole of
|
|
27
|
+
// FXO-2497's second limb: both are 503, and only one of them clears by
|
|
28
|
+
// itself.
|
|
29
|
+
capability_not_configured: "This service is not configured for that. Repeating the command will not change it — somebody with access to the deployment has to turn the capability on. The detail above names which one.",
|
|
22
30
|
credential_expired: `This credential has passed its expiry. Sign in again — ${programName} login — or rotate the one your automation presents: ${programName} credentials rotate <credential>`,
|
|
23
31
|
credential_revoked: `This credential has been revoked and will not work again. Sign in again: ${programName} login`,
|
|
24
32
|
idempotency_key_reused: "This key was used for a different request. Use a new --idempotency-key, or repeat the original request exactly to get the original answer back.",
|
package/dist/session.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function resolveOrigin(runtime, store, argumentValue) {
|
|
|
42
42
|
return { origin: known[0], source: "sole" };
|
|
43
43
|
}
|
|
44
44
|
if (known.length === 0) {
|
|
45
|
-
throw new CliError("no
|
|
45
|
+
throw new CliError("no GreatWait service has been chosen", "usage", `Sign in first — ${runtime.programName} login --api-url https://api.greatwait.dev — or set ${ENVIRONMENT_NAMES.apiUrl}.`);
|
|
46
46
|
}
|
|
47
47
|
throw new CliError(`${String(known.length)} services are signed in and none is selected`, "usage", `Choose one — ${runtime.programName} context use ${known[0]} — or pass --api-url on this command.`);
|
|
48
48
|
}
|
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
|
/**
|
|
@@ -60,13 +62,9 @@ export declare function parseCount(value: string, flag: string, bounds: {
|
|
|
60
62
|
/**
|
|
61
63
|
* How many pages `--all` will follow before it stops and says so.
|
|
62
64
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* collection that has ever grown. Fifty pages at the contract's maximum size is
|
|
67
|
-
* five thousand rows; past that, this stops, says how far it got, and hands
|
|
68
|
-
* back the cursor to carry on from — which is the difference between a bound
|
|
69
|
-
* and a lie.
|
|
65
|
+
* A bounded invocation holds at most fifty response pages in memory. Its
|
|
66
|
+
* cursor can be used with `--all` again, so this limits one invocation's
|
|
67
|
+
* allocation and request burst without preventing access to later records.
|
|
70
68
|
*/
|
|
71
69
|
export declare const ALL_PAGES_CEILING = 50;
|
|
72
70
|
export interface PageRequest {
|
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)
|
|
@@ -134,24 +141,15 @@ export function parseCount(value, flag, bounds) {
|
|
|
134
141
|
/**
|
|
135
142
|
* How many pages `--all` will follow before it stops and says so.
|
|
136
143
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* collection that has ever grown. Fifty pages at the contract's maximum size is
|
|
141
|
-
* five thousand rows; past that, this stops, says how far it got, and hands
|
|
142
|
-
* back the cursor to carry on from — which is the difference between a bound
|
|
143
|
-
* and a lie.
|
|
144
|
+
* A bounded invocation holds at most fifty response pages in memory. Its
|
|
145
|
+
* cursor can be used with `--all` again, so this limits one invocation's
|
|
146
|
+
* allocation and request burst without preventing access to later records.
|
|
144
147
|
*/
|
|
145
148
|
export const ALL_PAGES_CEILING = 50;
|
|
146
149
|
/** The page arguments, validated against the contract's own bounds. */
|
|
147
150
|
export function pageRequest(args) {
|
|
148
151
|
const limit = args.options["--limit"];
|
|
149
152
|
const cursor = args.options["--cursor"];
|
|
150
|
-
if (cursor !== undefined && args.booleans.has("--all")) {
|
|
151
|
-
// Not a contradiction the service can catch: it would follow the cursor and
|
|
152
|
-
// report a partial collection as the whole of it.
|
|
153
|
-
throw new CliError("--cursor continues one listing and --all reads every page, so only one of them can be meant", "usage", "Drop --cursor to read from the start, or drop --all to read one page from where you were.");
|
|
154
|
-
}
|
|
155
153
|
return {
|
|
156
154
|
...(cursor === undefined ? {} : { cursor }),
|
|
157
155
|
...(limit === undefined
|
|
@@ -229,7 +227,7 @@ export function continuationNote(drained, command) {
|
|
|
229
227
|
if (drained.nextCursor === undefined)
|
|
230
228
|
return undefined;
|
|
231
229
|
return drained.ceilingReached
|
|
232
|
-
? `Stopped after ${String(drained.pages)} pages
|
|
230
|
+
? `Stopped after ${String(drained.pages)} pages; this result is incomplete. Continue with: ${command} --cursor ${drained.nextCursor} --all`
|
|
233
231
|
: `More to read. Continue with: ${command} --cursor ${drained.nextCursor}`;
|
|
234
232
|
}
|
|
235
233
|
/* -------------------------------------------------------------------------- */
|
package/package.json
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
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.6",
|
|
11
|
+
"@greatwait/contracts": "0.1.0-alpha.6"
|
|
12
12
|
},
|
|
13
|
-
"description": "The
|
|
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": {
|
|
15
15
|
"node": ">=22.13.0"
|
|
16
16
|
},
|
|
@@ -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.6"
|
|
52
52
|
}
|