@greatwait/cli 0.1.0-alpha.4 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @greatwait/cli
2
2
 
3
- The Greatwait command line. Install it, sign in, and reach a first callback
3
+ The GreatWait command line. Install it, sign in, and reach a first callback
4
4
  against a receiver you control.
5
5
 
6
6
  ```sh
package/dist/catalog.js CHANGED
@@ -96,7 +96,7 @@ const PAGE_OPTIONS = [
96
96
  },
97
97
  {
98
98
  flag: "--all",
99
- summary: "Follow cursors until the collection ends, up to a published ceiling.",
99
+ summary: "Read up to 50 pages from the start or --cursor; return a cursor if more remain.",
100
100
  },
101
101
  ];
102
102
  const CONFIRM = {
@@ -151,7 +151,7 @@ export const COMMAND_CATALOG = [
151
151
  },
152
152
  ],
153
153
  positionals: [],
154
- summary: "Sign in to a Greatwait service and remember the credential.",
154
+ summary: "Sign in to a GreatWait service and remember the credential.",
155
155
  words: "login",
156
156
  },
157
157
  {
@@ -226,7 +226,7 @@ export const COMMAND_CATALOG = [
226
226
  API_URL,
227
227
  {
228
228
  flag: "--url",
229
- summary: "The https receiver Greatwait will deliver to.",
229
+ summary: "The https receiver GreatWait will deliver to.",
230
230
  value: "<https url>",
231
231
  },
232
232
  ],
@@ -615,6 +615,14 @@ export const COMMAND_CATALOG = [
615
615
  summary: "Show what is due in a window, and how fresh the answer is.",
616
616
  words: "forecast",
617
617
  },
618
+ {
619
+ group: "Planning",
620
+ operations: ["limits.read"],
621
+ options: [API_URL],
622
+ positionals: [],
623
+ summary: "Show the verified organization's effective allowances and policy revisions.",
624
+ words: "limits show",
625
+ },
618
626
  {
619
627
  group: "Planning",
620
628
  operations: ["usage.read"],
@@ -667,8 +675,8 @@ export const COMMAND_CATALOG = [
667
675
  API_URL,
668
676
  {
669
677
  flag: "--low-credit",
670
- summary: "Send the low-credit notice below this exact USD amount.",
671
- value: "<dollars>",
678
+ summary: "Send the low-credit notice below this exact USD amount, or auto for a fifth of the latest grant; omit to keep it.",
679
+ value: "<dollars|auto>",
672
680
  },
673
681
  {
674
682
  flag: "--noncritical-email",
@@ -814,14 +822,14 @@ export const COMMAND_CATALOG = [
814
822
  },
815
823
  {
816
824
  flag: "--reason",
817
- summary: "Why an operator should review the request.",
825
+ summary: "Why the credit is reserved for refund.",
818
826
  value: "<text>",
819
827
  },
820
828
  IDEMPOTENCY,
821
829
  CONFIRM,
822
830
  ],
823
831
  positionals: [],
824
- summary: "Record a request for operator-reviewed unused-credit refund.",
832
+ summary: "Reserve unused purchased credit against a payment for refund.",
825
833
  words: "billing refund-request",
826
834
  },
827
835
  {
@@ -832,12 +840,12 @@ export const COMMAND_CATALOG = [
832
840
  API_URL,
833
841
  {
834
842
  flag: "--refund-request",
835
- summary: "The pending refund request an external operator reviewed.",
843
+ summary: "The pending refund reservation to complete or decline.",
836
844
  value: "<request>",
837
845
  },
838
846
  {
839
847
  flag: "--decision",
840
- summary: "Record the external review outcome.",
848
+ summary: "Complete with an existing Stripe refund, or release the reservation.",
841
849
  value: "<completed|declined>",
842
850
  },
843
851
  {
@@ -950,6 +958,56 @@ export const COMMAND_CATALOG = [
950
958
  summary: "Create a hosted payment-method and invoice recovery link.",
951
959
  words: "billing portal",
952
960
  },
961
+ {
962
+ group: "Planning",
963
+ operations: ["billing.paymentmethod"],
964
+ options: [API_URL],
965
+ positionals: [],
966
+ summary: "Show the saved payment method (a card, Link or another) that automatic top-ups and project subscriptions charge.",
967
+ words: "billing payment-method",
968
+ },
969
+ {
970
+ group: "Planning",
971
+ operations: ["billing.invoices"],
972
+ options: [API_URL],
973
+ positionals: [],
974
+ summary: "List project subscription charges, most recent first, with their receipts.",
975
+ words: "billing invoices",
976
+ },
977
+ {
978
+ group: "Planning",
979
+ operations: ["billing.paymenthistory"],
980
+ options: [API_URL, ...PAGE_OPTIONS],
981
+ positionals: [],
982
+ summary: "Read execution-credit payments with continuation to older history.",
983
+ words: "billing payment-history",
984
+ },
985
+ {
986
+ group: "Planning",
987
+ operations: ["billing.invoicehistory"],
988
+ options: [API_URL, ...PAGE_OPTIONS],
989
+ positionals: [],
990
+ summary: "Read project subscription charges with continuation to older history.",
991
+ words: "billing invoice-history",
992
+ },
993
+ {
994
+ confirms: true,
995
+ group: "Planning",
996
+ operations: ["billing.paymentmethodsetup"],
997
+ options: [API_URL, CONFIRM],
998
+ positionals: [],
999
+ summary: "Create a hosted link for adding or replacing the saved payment method.",
1000
+ words: "billing add-card",
1001
+ },
1002
+ {
1003
+ confirms: true,
1004
+ group: "Planning",
1005
+ operations: ["billing.paymentmethodremove"],
1006
+ options: [API_URL, CONFIRM],
1007
+ positionals: [],
1008
+ summary: "Remove the saved payment method: project subscriptions stop renewing at the end of the paid period, and automatic top-up turns off.",
1009
+ words: "billing remove-card",
1010
+ },
953
1011
  {
954
1012
  group: "Planning",
955
1013
  operations: ["billing.ledger"],
@@ -1015,7 +1073,7 @@ export const COMMAND_CATALOG = [
1015
1073
  },
1016
1074
  ],
1017
1075
  positionals: [],
1018
- summary: "Create a receiver Greatwait hosts, for a test environment only.",
1076
+ summary: "Create a receiver GreatWait hosts, for a test environment only.",
1019
1077
  words: "sinks create",
1020
1078
  },
1021
1079
  {
@@ -1110,6 +1168,16 @@ export const OPERATION_OMISSIONS = [
1110
1168
  ground: "declared",
1111
1169
  operation: "billing.reconcile",
1112
1170
  },
1171
+ {
1172
+ because: "Confirmation answers a browser returning from hosted Checkout: the dashboard calls it on the customer's return so the credit shows at once. A purchase started from the command line has no return to answer; billing funding reports it once the provider notification or the scheduled repair settles it, and the MCP tool provides the automation path.",
1173
+ ground: "declared",
1174
+ operation: "billing.fundingconfirm",
1175
+ },
1176
+ {
1177
+ because: "Confirmation answers a browser returning from the hosted payment-method form: the dashboard calls it on the customer's return so the method shows at once. A method added from a link the command line printed has no return to answer; the provider notification makes it the saved method, billing payment-method reports it, and the MCP tool provides the automation path.",
1178
+ ground: "declared",
1179
+ operation: "billing.paymentmethodconfirm",
1180
+ },
1113
1181
  {
1114
1182
  because: "The endpoint page is the accepted place to mark this human-observed server update done; adding a second interactive CLI design is outside this remediation.",
1115
1183
  ground: "declared",
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import { CliError, InterruptedError } from "./runtime.js";
27
27
  * that exists appears here, and one that does not, cannot.
28
28
  */
29
29
  function usage(programName) {
30
- return `${programName} — the Greatwait command line
30
+ return `${programName} — the GreatWait command line
31
31
 
32
32
  ${overview()}
33
33
 
@@ -102,7 +102,9 @@ function classify(error, programName) {
102
102
  return {
103
103
  exit: classifyStatus(error.status, problem?.code),
104
104
  human: problem === undefined
105
- ? `The service answered ${String(error.status)}.`
105
+ ? error.status === 0
106
+ ? "The service could not be reached."
107
+ : `The service answered ${String(error.status)}.`
106
108
  : `${problem.title} (${problem.code})`,
107
109
  // **Every published code, not the two that were spelled out here.** This
108
110
  // was a two-branch conditional naming `authentication_required` and
package/dist/commands.js CHANGED
@@ -203,7 +203,9 @@ async function describeThrough(client, origin, programName) {
203
203
  };
204
204
  }
205
205
  catch (error) {
206
- if (!(error instanceof GreatwaitClientError)) {
206
+ if (!(error instanceof GreatwaitClientError) || error.status === 0) {
207
+ // The SDK uses status 0 when its transport retries are exhausted; older
208
+ // adapters may still throw the original network error.
207
209
  // **A transport failure is not an answer about the credential.** The
208
210
  // request never reached the service — no DNS, no TLS, a reset — so it
209
211
  // says nothing about what was just issued, and it does not carry a
@@ -395,7 +397,7 @@ const doctor = async (runtime, args) => {
395
397
  checks.push({
396
398
  detail: error instanceof Error
397
399
  ? error.message
398
- : "the origin did not answer as a Greatwait API",
400
+ : "the origin did not answer as a GreatWait API",
399
401
  name: "reachable",
400
402
  outcome: "failed",
401
403
  });
@@ -583,9 +585,11 @@ const destinationsCreate = async (runtime, args) => {
583
585
  ``,
584
586
  `Signing key ${created.secret}`,
585
587
  ``,
586
- `This key is shown once and is not recoverable. Store it now — your receiver`,
588
+ `This is the only call that returns the key. Store it now — your receiver`,
587
589
  `verifies every callback with it, and reading this destination back will show`,
588
- `${redactSecret(created.secret)} instead.`,
590
+ `${redactSecret(created.secret)} instead. If you lose it, a signed-in member`,
591
+ `can read it again on this destination's page in the dashboard; no command`,
592
+ `here can, because the reveal accepts a member session and not a grant.`,
589
593
  ].join("\n"),
590
594
  nextMove: `Next: ${runtime.programName} schedules create --endpoint ${created.id} --in 30s`,
591
595
  };
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
  *
@@ -60,8 +72,9 @@ export declare const EXIT_CLASS_MEANINGS: Readonly<Record<ExitClass, string>>;
60
72
  * A refusal from an edge proxy carries no problem document at all — there is no
61
73
  * code to read — and a client that branched on the code first would classify
62
74
  * every one of those as internal. The code refines the answer where it
63
- * disagrees with the status, which is exactly once: `scope_missing` is a `403`
64
- * 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
65
78
  * not authentication is the caller's request to fix.
66
79
  */
67
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
  *
@@ -59,6 +71,7 @@ export const EXIT_CLASS_MEANINGS = {
59
71
  ok: "It did what it was asked.",
60
72
  retryable: "The service could not answer now. The identical request may work later.",
61
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.",
62
75
  usage: "The command line was wrong.",
63
76
  validation: "The request was refused on its merits: invalid, missing, or conflicting.",
64
77
  };
@@ -69,13 +82,23 @@ export const EXIT_CLASS_MEANINGS = {
69
82
  * A refusal from an edge proxy carries no problem document at all — there is no
70
83
  * code to read — and a client that branched on the code first would classify
71
84
  * every one of those as internal. The code refines the answer where it
72
- * disagrees with the status, which is exactly once: `scope_missing` is a `403`
73
- * 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
74
88
  * not authentication is the caller's request to fix.
75
89
  */
76
90
  export function classifyStatus(status, problemCode) {
77
91
  if (problemCode === "scope_missing")
78
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";
79
102
  if (status === 401)
80
103
  return "authentication";
81
104
  if (status === 403)
@@ -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) => {
package/dist/oauth.js CHANGED
@@ -35,7 +35,7 @@ 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 ${runtime.programName} 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)
@@ -75,7 +75,7 @@ export async function registerClient(runtime, server, options) {
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],
package/dist/operate.js CHANGED
@@ -226,9 +226,11 @@ const destinationsRotateKey = async (runtime, args) => {
226
226
  ``,
227
227
  `Signing key ${rotated.secret}`,
228
228
  ``,
229
- `This key is shown once and is not recoverable. Add it to your receiver's`,
229
+ `This is the only call that returns the key. Add it to your receiver's`,
230
230
  `accepted set now, alongside the current one; reading this destination back`,
231
- `will show ${redactSecret(rotated.secret)} instead.`,
231
+ `will show ${redactSecret(rotated.secret)} instead. If you lose it, a`,
232
+ `signed-in member can read it again on this destination's page in the`,
233
+ `dashboard; no command here can.`,
232
234
  ].join("\n"),
233
235
  };
234
236
  };
@@ -445,7 +447,7 @@ const attemptsRead = async (runtime, args) => {
445
447
  ].join("\n"),
446
448
  ...(found.remediation === "review_before_redelivery"
447
449
  ? {
448
- nextMove: "This attempt was ambiguous: the receiver may have processed it even though Greatwait never saw an answer. Check the receiver before redelivering.",
450
+ nextMove: "This attempt was ambiguous: the receiver may have processed it even though GreatWait never saw an answer. Check the receiver before redelivering.",
449
451
  }
450
452
  : {}),
451
453
  };
@@ -462,6 +464,46 @@ function window(args) {
462
464
  ...(to === undefined ? {} : { to: parseInstant(to, "--to") }),
463
465
  };
464
466
  }
467
+ const limitsShow = async (runtime, args) => {
468
+ positionals(args, [], `${runtime.programName} limits show`);
469
+ const active = await session(runtime, args);
470
+ const view = parseResponse("limits.read", await active.client.request("limits_read"));
471
+ const rows = [
472
+ [
473
+ "Inline payload",
474
+ String(view.limits.inlinePayloadBytes),
475
+ "bytes",
476
+ view.provenance.inlinePayloadBytes,
477
+ ],
478
+ [
479
+ "Partition keys",
480
+ view.limits.partitionKeyCeiling === undefined
481
+ ? "No configured ceiling"
482
+ : String(view.limits.partitionKeyCeiling),
483
+ "keys",
484
+ view.provenance.partitionKeyCeiling ?? "default",
485
+ ],
486
+ [
487
+ "Forecast horizon",
488
+ String(view.limits.forecastHorizonSeconds),
489
+ "seconds",
490
+ view.provenance.forecastHorizonSeconds,
491
+ ],
492
+ [
493
+ "Usage window",
494
+ String(view.limits.usageWindowDays),
495
+ "days",
496
+ view.provenance.usageWindowDays,
497
+ ],
498
+ ];
499
+ return {
500
+ data: view,
501
+ human: [
502
+ table(["SETTING", "VALUE", "UNIT", "ORIGIN"], rows, ""),
503
+ `Policy revisions: global ${String(view.revision.global)}, organization ${String(view.revision.organization)}`,
504
+ ].join("\n"),
505
+ };
506
+ };
465
507
  const forecast = async (runtime, args) => {
466
508
  const active = await session(runtime, args);
467
509
  const bucket = args.options["--bucket"];
@@ -678,18 +720,38 @@ const billingNotificationSettings = async (runtime, args) => {
678
720
  };
679
721
  const billingNotificationSettingsUpdate = async (runtime, args) => {
680
722
  const noncritical = args.options["--noncritical-email"];
723
+ // Three meanings, matching the contract: omitted leaves the threshold as it
724
+ // is, chosen or not; `auto` sends null, which returns it to a fifth of the
725
+ // latest credit grant; an amount chooses it. `off` is how other commands
726
+ // spell "no amount", and there is no "no threshold" here, so it is refused
727
+ // along with zero, negatives and fractions of a cent.
728
+ const lowCreditRaw = args.options["--low-credit"];
729
+ const lowCredit = lowCreditRaw === undefined
730
+ ? undefined
731
+ : lowCreditRaw === "auto"
732
+ ? null
733
+ : budgetMoney(args, "--low-credit");
734
+ const refusal = "--low-credit must be a positive exact-cent USD amount or auto, and --noncritical-email must be on or off";
735
+ if (lowCreditRaw !== undefined &&
736
+ lowCreditRaw !== "auto" &&
737
+ lowCredit === null)
738
+ throw new CliError(refusal, "usage");
681
739
  const parsed = billingNotificationSettingsUpdateRequestSchema.safeParse({
682
740
  expectedRevision: expectedRevision(args),
683
- lowCreditThreshold: budgetMoney(args, "--low-credit"),
741
+ ...(lowCredit === undefined ? {} : { lowCreditThreshold: lowCredit }),
684
742
  noncriticalEmail: noncritical === "on" ? true : noncritical === "off" ? false : undefined,
685
743
  });
686
744
  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");
745
+ throw new CliError(refusal, "usage");
688
746
  }
689
747
  const confirmation = await confirmChange(runtime, args, {
690
748
  action: "Replace this organization's billing notification settings.",
691
749
  effect: [
692
- `low-credit threshold ${money(parsed.data.lowCreditThreshold)}`,
750
+ lowCredit === undefined
751
+ ? "low-credit threshold unchanged"
752
+ : lowCredit === null
753
+ ? "low-credit threshold automatic (a fifth of the latest credit grant)"
754
+ : `low-credit threshold ${money(lowCredit)}`,
693
755
  `noncritical email ${parsed.data.noncriticalEmail ? "on" : "off"}`,
694
756
  "Critical email remains mandatory for owners and active billing-role members.",
695
757
  ],
@@ -877,12 +939,12 @@ const billingRefundRequest = async (runtime, args) => {
877
939
  throw new CliError("Provide a positive exact-cent --amount, --payment-operation and --reason", "usage");
878
940
  }
879
941
  const confirmation = await confirmChange(runtime, args, {
880
- action: "Record this unused-credit refund request for operator review.",
942
+ action: "Reserve this unused purchased credit against the payment for refund.",
881
943
  effect: [
882
944
  `amount ${money(parsed.data.amount)}`,
883
945
  `payment ${parsed.data.paymentOperation}`,
884
946
  `reason ${parsed.data.reason}`,
885
- "This records a request only. It does not submit a provider refund or cash out credit.",
947
+ "This reserves credit only. It does not submit a provider refund or cash out credit.",
886
948
  ],
887
949
  });
888
950
  const active = await session(runtime, args);
@@ -898,7 +960,7 @@ const billingRefundRequest = async (runtime, args) => {
898
960
  `Requested ${view.requestedAt}`,
899
961
  `Retry key ${key}`,
900
962
  ].join("\n"),
901
- nextMove: "An operator must review this request before any provider refund.",
963
+ nextMove: "A Stripe refund of this payment settles or supersedes the reservation; resolve-refund --decision declined releases it.",
902
964
  };
903
965
  };
904
966
  const billingRefundResolution = async (runtime, args) => {
@@ -1020,6 +1082,99 @@ const billingChangeProject = async (runtime, args) => {
1020
1082
  data: { confirmation, idempotencyKey: key, ...view },
1021
1083
  };
1022
1084
  };
1085
+ const billingPaymentMethod = async (runtime, args) => {
1086
+ const active = await session(runtime, args);
1087
+ const view = parseResponse("billing.paymentmethod", await active.client.request("billing_paymentmethod"));
1088
+ return {
1089
+ data: view,
1090
+ human: paymentMethodLine(view),
1091
+ };
1092
+ };
1093
+ /**
1094
+ * The saved payment method in one line (W9-14). The label is the service's,
1095
+ * so a card reads "Visa ending in 4242" here exactly as on the dashboard, and
1096
+ * Link or Cash App Pay is named rather than reported as no card.
1097
+ */
1098
+ function paymentMethodLine(view) {
1099
+ const method = view.method;
1100
+ if (method === null)
1101
+ return "No payment method on file. Add one with billing add-card; automatic top-up and paid projects need it.";
1102
+ return method.type === "card"
1103
+ ? `Payment method on file: ${method.label}, expires ${String(method.expMonth).padStart(2, "0")}/${String(method.expYear)}`
1104
+ : `Payment method on file: ${method.label}`;
1105
+ }
1106
+ const billingInvoices = async (runtime, args) => {
1107
+ const active = await session(runtime, args);
1108
+ const view = parseResponse("billing.invoices", await active.client.request("billing_invoices"));
1109
+ return {
1110
+ data: view,
1111
+ human: table(["CREATED", "AMOUNT", "STATE", "RECEIPT"], view.data.map((invoice) => [
1112
+ invoice.createdAt,
1113
+ money(invoice.amount),
1114
+ invoice.state,
1115
+ invoice.receiptUrl ?? "",
1116
+ ]), "No project subscription charges."),
1117
+ };
1118
+ };
1119
+ const billingPaymentHistory = async (runtime, args) => {
1120
+ const active = await session(runtime, args);
1121
+ return listing(runtime, args, {
1122
+ command: `${runtime.programName} billing payment-history`,
1123
+ columns: ["CREATED", "AMOUNT", "STATE", "RECEIPT"],
1124
+ empty: "No execution-credit payments.",
1125
+ read: (request) => page("billing.paymenthistory", () => active.client.request("billing_paymenthistory", request)),
1126
+ row: (payment) => [
1127
+ payment.createdAt,
1128
+ money(payment.gross ?? payment.credit),
1129
+ payment.state,
1130
+ payment.receiptUrl ?? "",
1131
+ ],
1132
+ });
1133
+ };
1134
+ const billingInvoiceHistory = async (runtime, args) => {
1135
+ const active = await session(runtime, args);
1136
+ return listing(runtime, args, {
1137
+ command: `${runtime.programName} billing invoice-history`,
1138
+ columns: ["CREATED", "AMOUNT", "STATE", "RECEIPT"],
1139
+ empty: "No project subscription charges.",
1140
+ read: (request) => page("billing.invoicehistory", () => active.client.request("billing_invoicehistory", request)),
1141
+ row: (invoice) => [
1142
+ invoice.createdAt,
1143
+ money(invoice.amount),
1144
+ invoice.state,
1145
+ invoice.receiptUrl ?? "",
1146
+ ],
1147
+ });
1148
+ };
1149
+ const billingAddCard = async (runtime, args) => {
1150
+ await confirmChange(runtime, args, {
1151
+ action: "Create a hosted link for adding or replacing this organization's payment method.",
1152
+ effect: [
1153
+ "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.",
1154
+ ],
1155
+ });
1156
+ const active = await session(runtime, args);
1157
+ const view = parseResponse("billing.paymentmethodsetup", await active.client.request("billing_paymentmethodsetup", {}));
1158
+ return { data: view, human: view.url };
1159
+ };
1160
+ const billingRemoveCard = async (runtime, args) => {
1161
+ await confirmChange(runtime, args, {
1162
+ action: "Remove this organization's saved payment method.",
1163
+ effect: [
1164
+ "Project subscriptions stop renewing: paid projects keep running until the end of the period already paid for, then stop. The included project keeps running.",
1165
+ "Automatic top-up is turned off immediately. Nothing more is charged.",
1166
+ "Saving a payment method again before the period ends keeps those projects.",
1167
+ ],
1168
+ });
1169
+ const active = await session(runtime, args);
1170
+ const view = parseResponse("billing.paymentmethodremove", await active.client.request("billing_paymentmethodremove", {
1171
+ confirmed: true,
1172
+ }));
1173
+ return {
1174
+ data: view,
1175
+ human: "Payment method removed. No payment method on file.",
1176
+ };
1177
+ };
1023
1178
  const billingPortal = async (runtime, args) => {
1024
1179
  await confirmChange(runtime, args, {
1025
1180
  action: "Create this organization's hosted billing recovery link.",
@@ -1093,7 +1248,9 @@ const billingLedger = async (runtime, args) => {
1093
1248
  row: (entry) => [
1094
1249
  entry.sequence,
1095
1250
  entry.recordedAt,
1096
- entry.kind,
1251
+ entry.note === "provider-details-attached"
1252
+ ? `${entry.kind} (details attached)`
1253
+ : entry.kind,
1097
1254
  money(entry.amount),
1098
1255
  entry.reference.attempt ??
1099
1256
  entry.reference.creditLot ??
@@ -1281,7 +1438,7 @@ const sinksDelete = async (runtime, args) => {
1281
1438
  action: `Delete test sink ${current.id}.`,
1282
1439
  effect: [
1283
1440
  `received ${String(current.deliveryCount)} deliveries, which go with it`,
1284
- `The recorded bodies are the only copy Greatwait keeps. Read anything you`,
1441
+ `The recorded bodies are the only copy GreatWait keeps. Read anything you`,
1285
1442
  `still need first.`,
1286
1443
  ],
1287
1444
  });
@@ -1345,6 +1502,7 @@ const sinksDelivery = async (runtime, args) => {
1345
1502
  /* -------------------------------------------------------------------------- */
1346
1503
  /** The operate-and-diagnose commands, keyed by the words that select them. */
1347
1504
  export const OPERATE_COMMANDS = {
1505
+ "limits show": limitsShow,
1348
1506
  "attempts read": attemptsRead,
1349
1507
  billing,
1350
1508
  "billing budgets": billingBudgets,
@@ -1364,6 +1522,12 @@ export const OPERATE_COMMANDS = {
1364
1522
  "billing resolve-refund": billingRefundResolution,
1365
1523
  "billing change-project": billingChangeProject,
1366
1524
  "billing portal": billingPortal,
1525
+ "billing payment-method": billingPaymentMethod,
1526
+ "billing invoices": billingInvoices,
1527
+ "billing payment-history": billingPaymentHistory,
1528
+ "billing invoice-history": billingInvoiceHistory,
1529
+ "billing add-card": billingAddCard,
1530
+ "billing remove-card": billingRemoveCard,
1367
1531
  timing,
1368
1532
  "credentials list": credentialsList,
1369
1533
  "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<"invalid_request" | "not_found" | "state_conflict" | "version_conflict" | "authentication_required" | "billing_not_ready" | "capability_not_configured" | "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>>;
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
@@ -60,13 +60,9 @@ export declare function parseCount(value: string, flag: string, bounds: {
60
60
  /**
61
61
  * How many pages `--all` will follow before it stops and says so.
62
62
  *
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.
63
+ * A bounded invocation holds at most fifty response pages in memory. Its
64
+ * cursor can be used with `--all` again, so this limits one invocation's
65
+ * allocation and request burst without preventing access to later records.
70
66
  */
71
67
  export declare const ALL_PAGES_CEILING = 50;
72
68
  export interface PageRequest {
package/dist/support.js CHANGED
@@ -134,24 +134,15 @@ export function parseCount(value, flag, bounds) {
134
134
  /**
135
135
  * How many pages `--all` will follow before it stops and says so.
136
136
  *
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.
137
+ * A bounded invocation holds at most fifty response pages in memory. Its
138
+ * cursor can be used with `--all` again, so this limits one invocation's
139
+ * allocation and request burst without preventing access to later records.
144
140
  */
145
141
  export const ALL_PAGES_CEILING = 50;
146
142
  /** The page arguments, validated against the contract's own bounds. */
147
143
  export function pageRequest(args) {
148
144
  const limit = args.options["--limit"];
149
145
  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
146
  return {
156
147
  ...(cursor === undefined ? {} : { cursor }),
157
148
  ...(limit === undefined
@@ -229,7 +220,7 @@ export function continuationNote(drained, command) {
229
220
  if (drained.nextCursor === undefined)
230
221
  return undefined;
231
222
  return drained.ceilingReached
232
- ? `Stopped after ${String(drained.pages)} pages, which is where --all stops. Continue with: ${command} --cursor ${drained.nextCursor}`
223
+ ? `Stopped after ${String(drained.pages)} pages; this result is incomplete. Continue with: ${command} --cursor ${drained.nextCursor} --all`
233
224
  : `More to read. Continue with: ${command} --cursor ${drained.nextCursor}`;
234
225
  }
235
226
  /* -------------------------------------------------------------------------- */
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.5",
11
+ "@greatwait/contracts": "0.1.0-alpha.5"
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.5"
52
52
  }