@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 +1 -1
- package/dist/catalog.js +78 -10
- package/dist/cli.js +4 -2
- package/dist/commands.js +8 -4
- package/dist/exit.d.ts +15 -2
- package/dist/exit.js +25 -2
- package/dist/node-runtime.js +1 -1
- package/dist/oauth.js +2 -2
- package/dist/operate.js +175 -11
- package/dist/remediation.d.ts +1 -1
- package/dist/remediation.js +8 -0
- package/dist/session.js +1 -1
- package/dist/support.d.ts +3 -7
- package/dist/support.js +4 -13
- package/package.json +4 -4
package/README.md
CHANGED
package/dist/catalog.js
CHANGED
|
@@ -96,7 +96,7 @@ const PAGE_OPTIONS = [
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
flag: "--all",
|
|
99
|
-
summary: "
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
843
|
+
summary: "The pending refund reservation to complete or decline.",
|
|
836
844
|
value: "<request>",
|
|
837
845
|
},
|
|
838
846
|
{
|
|
839
847
|
flag: "--decision",
|
|
840
|
-
summary: "
|
|
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
|
|
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
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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
|
|
64
|
-
* and belongs in `authorization`,
|
|
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
|
|
73
|
-
* and belongs in `authorization`,
|
|
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)
|
package/dist/node-runtime.js
CHANGED
|
@@ -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("
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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: "
|
|
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.
|
|
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
|
|
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,
|
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<"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 };
|
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
|
@@ -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
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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.
|
|
11
|
-
"@greatwait/contracts": "0.1.0-alpha.
|
|
10
|
+
"@greatwait/api-client": "0.1.0-alpha.5",
|
|
11
|
+
"@greatwait/contracts": "0.1.0-alpha.5"
|
|
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.5"
|
|
52
52
|
}
|