@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/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 key is shown once and is not recoverable. Add it to your receiver's`,
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 definition on this command line, and nothing of the old one`,
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 Greatwait never saw an answer. Check the receiver before redelivering.",
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: budgetMoney(args, "--low-credit"),
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("--low-credit must be a positive exact-cent USD amount and --noncritical-email must be on or off", "usage");
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
- `low-credit threshold ${money(parsed.data.lowCreditThreshold)}`,
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: "Record this unused-credit refund request for operator review.",
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 records a request only. It does not submit a provider refund or cash out credit.",
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: "An operator must review this request before any provider refund.",
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.kind,
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
- ...(program === undefined ? {} : { program }),
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 Greatwait keeps. Read anything you`,
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,
@@ -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<"invalid_request" | "not_found" | "state_conflict" | "version_conflict" | "authentication_required" | "credential_expired" | "credential_revoked" | "idempotency_key_reused" | "payload_too_large" | "plan_limit_reached" | "project_archived" | "rate_limited" | "revision_conflict" | "scope_missing" | "service_unavailable" | "test_to_live_escalation", string>>;
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 };
@@ -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 Greatwait service has been chosen", "usage", `Sign in first — ${runtime.programName} login --api-url https://api.greatwait.dev — or set ${ENVIRONMENT_NAMES.apiUrl}.`);
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
- * **Published, and generous rather than equal to a page.** A ceiling nobody can
64
- * see is a truncation somebody reports as data loss, and one set at the
65
- * contract's page maximum would make `--all` mean "one page" for every
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
- * **Published, and generous rather than equal to a page.** A ceiling nobody can
138
- * see is a truncation somebody reports as data loss, and one set at the
139
- * contract's page maximum would make `--all` mean "one page" for every
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, which is where --all stops. Continue with: ${command} --cursor ${drained.nextCursor}`
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.4",
11
- "@greatwait/contracts": "0.1.0-alpha.4"
10
+ "@greatwait/api-client": "0.1.0-alpha.6",
11
+ "@greatwait/contracts": "0.1.0-alpha.6"
12
12
  },
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.",
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.4"
51
+ "version": "0.1.0-alpha.6"
52
52
  }