@greatwait/cli 0.1.0-alpha.4 → 0.1.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/catalog.d.ts +4 -5
- package/dist/catalog.js +551 -168
- package/dist/cli.js +4 -2
- package/dist/commands.js +206 -60
- package/dist/exit.d.ts +15 -2
- package/dist/exit.js +25 -2
- package/dist/member-catalog.d.ts +1 -0
- package/dist/member-catalog.js +1 -0
- package/dist/member-commands.d.ts +6 -0
- package/dist/member-commands.js +192 -0
- package/dist/node-runtime.js +1 -1
- package/dist/oauth.js +2 -2
- package/dist/operate.d.ts +21 -0
- package/dist/operate.js +254 -17
- package/dist/remediation.d.ts +1 -1
- package/dist/remediation.js +8 -0
- package/dist/session.js +1 -1
- package/dist/support.d.ts +5 -7
- package/dist/support.js +11 -13
- package/package.json +4 -4
package/dist/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
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { readPrivateJsonOption } from "./support.js";
|
|
2
|
+
import { MEMBER_IMPLEMENTATIONS } from "./member-commands.js";
|
|
1
3
|
import { GreatwaitClientError, createGreatwaitClient, } from "@greatwait/api-client";
|
|
2
|
-
import { isTerminalOccurrenceState, responseSchemaFor, } from "@greatwait/contracts";
|
|
4
|
+
import { endpointHeaderChangesSchema, isTerminalOccurrenceState, responseSchemaFor, } from "@greatwait/contracts";
|
|
3
5
|
import { parseDuration } from "./args.js";
|
|
4
6
|
import { COMMAND_CATALOG, REQUESTED_SCOPES } from "./catalog.js";
|
|
5
7
|
import { confirmChange } from "./confirm.js";
|
|
@@ -9,7 +11,7 @@ import { redactSecret } from "./output.js";
|
|
|
9
11
|
import { CliError, sleepOrAbort } from "./runtime.js";
|
|
10
12
|
import { publishedWait, recoveryFor, renderJourney, STEP_COMMANDS, } from "./journey.js";
|
|
11
13
|
import { anonymousClient, ENVIRONMENT_NAMES, openSession, resolveOrigin, storeFor, } from "./session.js";
|
|
12
|
-
import { OPERATE_COMMANDS, scheduleRequest } from "./operate.js";
|
|
14
|
+
import { OPERATE_COMMANDS, destinationLines, scheduleRequest } from "./operate.js";
|
|
13
15
|
import { apiUrlOption, expectNoExtraArguments, parseResponse, positionals, } from "./support.js";
|
|
14
16
|
/* -------------------------------------------------------------------------- */
|
|
15
17
|
/* Signing in */
|
|
@@ -203,7 +205,9 @@ async function describeThrough(client, origin, programName) {
|
|
|
203
205
|
};
|
|
204
206
|
}
|
|
205
207
|
catch (error) {
|
|
206
|
-
if (!(error instanceof GreatwaitClientError)) {
|
|
208
|
+
if (!(error instanceof GreatwaitClientError) || error.status === 0) {
|
|
209
|
+
// The SDK uses status 0 when its transport retries are exhausted; older
|
|
210
|
+
// adapters may still throw the original network error.
|
|
207
211
|
// **A transport failure is not an answer about the credential.** The
|
|
208
212
|
// request never reached the service — no DNS, no TLS, a reset — so it
|
|
209
213
|
// says nothing about what was just issued, and it does not carry a
|
|
@@ -395,7 +399,7 @@ const doctor = async (runtime, args) => {
|
|
|
395
399
|
checks.push({
|
|
396
400
|
detail: error instanceof Error
|
|
397
401
|
? error.message
|
|
398
|
-
: "the origin did not answer as a
|
|
402
|
+
: "the origin did not answer as a GreatWait API",
|
|
399
403
|
name: "reachable",
|
|
400
404
|
outcome: "failed",
|
|
401
405
|
});
|
|
@@ -569,8 +573,17 @@ const destinationsCreate = async (runtime, args) => {
|
|
|
569
573
|
if (url === undefined) {
|
|
570
574
|
throw new CliError(`${runtime.programName} destinations create needs --url`, "usage", `Usage: ${runtime.programName} destinations create --url https://your-receiver.example.com/greatwait`);
|
|
571
575
|
}
|
|
576
|
+
const headerInput = await readPrivateJsonOption(runtime, "--headers", args.options["--headers"]);
|
|
577
|
+
const headers = headerInput === undefined
|
|
578
|
+
? undefined
|
|
579
|
+
: endpointHeaderChangesSchema.safeParse(headerInput);
|
|
580
|
+
if (headers !== undefined && !headers.success)
|
|
581
|
+
throw new CliError("--headers settings are invalid", "usage", headers.error.issues.map((issue) => issue.message).join("; "));
|
|
572
582
|
const session = await openSession(runtime, { apiUrl: apiUrlOption(args) });
|
|
573
|
-
const created = responseSchemaFor("endpoints.create")?.parse(await session.client.request("endpoints_create", {
|
|
583
|
+
const created = responseSchemaFor("endpoints.create")?.parse(await session.client.request("endpoints_create", {
|
|
584
|
+
url,
|
|
585
|
+
...(headers?.success ? { headers: headers.data } : {}),
|
|
586
|
+
}));
|
|
574
587
|
return {
|
|
575
588
|
// **The secret is here and in no later output.** The service returns it
|
|
576
589
|
// from this call and from no other, which is the product behaviour rather
|
|
@@ -583,9 +596,11 @@ const destinationsCreate = async (runtime, args) => {
|
|
|
583
596
|
``,
|
|
584
597
|
`Signing key ${created.secret}`,
|
|
585
598
|
``,
|
|
586
|
-
`This
|
|
599
|
+
`This is the only call that returns the key. Store it now — your receiver`,
|
|
587
600
|
`verifies every callback with it, and reading this destination back will show`,
|
|
588
|
-
`${redactSecret(created.secret)} instead
|
|
601
|
+
`${redactSecret(created.secret)} instead. If you lose it, a signed-in member`,
|
|
602
|
+
`can read it again on this destination's page in the dashboard; no command`,
|
|
603
|
+
`here can, because the reveal accepts a member session and not a grant.`,
|
|
589
604
|
].join("\n"),
|
|
590
605
|
nextMove: `Next: ${runtime.programName} schedules create --endpoint ${created.id} --in 30s`,
|
|
591
606
|
};
|
|
@@ -624,16 +639,27 @@ const destinationsDisable = async (runtime, args) => {
|
|
|
624
639
|
/* Schedules */
|
|
625
640
|
/* -------------------------------------------------------------------------- */
|
|
626
641
|
const schedulesCreate = async (runtime, args) => {
|
|
627
|
-
const usage = `${runtime.programName} schedules create --endpoint <destination> (--in <duration> | --at <instant> | --every <duration> | --cron <expression>)`;
|
|
628
|
-
if (args.options["--endpoint"] === undefined
|
|
629
|
-
|
|
642
|
+
const usage = `${runtime.programName} schedules create (--endpoint <destination> | --url <https url>) (--in <duration> | --at <instant> | --every <duration> | --cron <expression>) [--wait]`;
|
|
643
|
+
if (args.options["--endpoint"] === undefined &&
|
|
644
|
+
args.options["--url"] === undefined) {
|
|
645
|
+
throw new CliError(`${runtime.programName} schedules create needs --url or --endpoint`, "usage", `Usage: ${usage}`);
|
|
646
|
+
}
|
|
647
|
+
const wait = args.booleans.has("--wait");
|
|
648
|
+
// Parsed before anything is sent: a typo in --timeout found after the create
|
|
649
|
+
// would exit without the schedule's id or retry key, and the corrected rerun
|
|
650
|
+
// would make a second schedule.
|
|
651
|
+
const timeoutMs = wait ? waitTimeoutMs(args) : undefined;
|
|
652
|
+
if (!wait && args.options["--timeout"] !== undefined) {
|
|
653
|
+
// Refused rather than ignored: somebody who passed a timeout believes the
|
|
654
|
+
// command will wait for something.
|
|
655
|
+
throw new CliError("--timeout only means something with --wait", "usage", `Usage: ${usage} [--timeout <duration>]`);
|
|
630
656
|
}
|
|
631
657
|
expectNoExtraArguments(args, 0, usage);
|
|
632
658
|
const session = await openSession(runtime, { apiUrl: apiUrlOption(args) });
|
|
633
659
|
// **The same builder `schedules replace` uses.** The two send the identical
|
|
634
660
|
// definition — a replace is a create with an identifier — and writing the
|
|
635
661
|
// recurrence rules out twice is how one of them comes to accept a cron
|
|
636
|
-
// expression the other does not.
|
|
662
|
+
// expression the other does not. It also refuses `--url` with `--endpoint`.
|
|
637
663
|
const request = await scheduleRequest(runtime, args, usage);
|
|
638
664
|
// **Generated when it is not given, and printed either way.** A create that
|
|
639
665
|
// silently invented a fresh key on every run would make a retry a second
|
|
@@ -644,30 +670,125 @@ const schedulesCreate = async (runtime, args) => {
|
|
|
644
670
|
const answer = await session.client.requestDetailed("schedules_create", request, { "Idempotency-Key": idempotencyKey });
|
|
645
671
|
const created = parseResponse("schedules.create", answer.body);
|
|
646
672
|
const replayed = answer.headers["Idempotency-Replayed"] === "true";
|
|
673
|
+
const schedule = created.schedule.id;
|
|
674
|
+
const occurrence = created.occurrence.id;
|
|
675
|
+
const data = {
|
|
676
|
+
...(created.endpointSigningSecret === undefined
|
|
677
|
+
? {}
|
|
678
|
+
: {
|
|
679
|
+
endpoint: created.schedule.endpoint,
|
|
680
|
+
endpointSigningSecret: created.endpointSigningSecret,
|
|
681
|
+
}),
|
|
682
|
+
idempotencyKey,
|
|
683
|
+
occurrence,
|
|
684
|
+
plannedFor: created.occurrence.plannedFor,
|
|
685
|
+
replayed,
|
|
686
|
+
schedule,
|
|
687
|
+
};
|
|
688
|
+
const human = [
|
|
689
|
+
`Schedule ${schedule}`,
|
|
690
|
+
`Occurrence ${occurrence}`,
|
|
691
|
+
`Planned for ${created.occurrence.plannedFor}`,
|
|
692
|
+
`Retry key ${idempotencyKey}`,
|
|
693
|
+
...destinationLines(created.schedule, created.endpointSigningSecret, args.options["--url"]),
|
|
694
|
+
...(replayed
|
|
695
|
+
? [
|
|
696
|
+
``,
|
|
697
|
+
`This key had already been used for this exact request, so nothing was`,
|
|
698
|
+
`created a second time and no second callback will be delivered.`,
|
|
699
|
+
]
|
|
700
|
+
: []),
|
|
701
|
+
];
|
|
702
|
+
const watchLater = `${runtime.programName} occurrences wait ${schedule} ${occurrence}`;
|
|
703
|
+
if (!wait) {
|
|
704
|
+
return {
|
|
705
|
+
data,
|
|
706
|
+
human: human.join("\n"),
|
|
707
|
+
nextMove: `Next: ${watchLater}`,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const repeats = created.schedule.recurrence.kind !== "at";
|
|
711
|
+
if (timeoutMs === undefined)
|
|
712
|
+
throw new CliError("--wait lost its timeout", "internal");
|
|
713
|
+
const dueInMs = Date.parse(created.occurrence.plannedFor) - runtime.now();
|
|
714
|
+
if (dueInMs > timeoutMs) {
|
|
715
|
+
// **Said at once rather than discovered at the deadline.** Polling for
|
|
716
|
+
// three minutes towards a call due tomorrow would end in the same timeout
|
|
717
|
+
// with nothing learned; the schedule exists either way.
|
|
718
|
+
return {
|
|
719
|
+
data: {
|
|
720
|
+
...data,
|
|
721
|
+
wait: { settled: false, state: created.occurrence.state },
|
|
722
|
+
},
|
|
723
|
+
exit: "timeout",
|
|
724
|
+
human: [
|
|
725
|
+
...human,
|
|
726
|
+
``,
|
|
727
|
+
`Not waiting: the first call is due at ${created.occurrence.plannedFor}, after the wait would end.`,
|
|
728
|
+
].join("\n"),
|
|
729
|
+
nextMove: `Watch it nearer the time: ${watchLater}`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
// The schedule's identifiers before the wait, so a person who interrupts it
|
|
733
|
+
// still has them. A machine reads them from the JSON at the end.
|
|
734
|
+
if (!args.booleans.has("--json")) {
|
|
735
|
+
runtime.stderr(`${human.join("\n")}\n\n`);
|
|
736
|
+
}
|
|
737
|
+
const watched = await watchOccurrence(runtime, session, schedule, occurrence, timeoutMs);
|
|
738
|
+
const repeating = repeats
|
|
739
|
+
? [
|
|
740
|
+
``,
|
|
741
|
+
`This schedule repeats. Only its first call was waited for, and it keeps running.`,
|
|
742
|
+
]
|
|
743
|
+
: [];
|
|
744
|
+
if (!watched.settled) {
|
|
745
|
+
return {
|
|
746
|
+
data: { ...data, wait: { settled: false, state: watched.state } },
|
|
747
|
+
exit: "timeout",
|
|
748
|
+
human: [
|
|
749
|
+
`The first call was still ${watched.state} when the wait ended.`,
|
|
750
|
+
...repeating,
|
|
751
|
+
].join("\n"),
|
|
752
|
+
...(() => {
|
|
753
|
+
const nextMove = recoveryFor("await-occurrence", "not-settled");
|
|
754
|
+
return nextMove === undefined ? {} : { nextMove };
|
|
755
|
+
})(),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const attempts = parseResponse("attempts.list", await session.client.request("attempts_list", schedule, occurrence)).data;
|
|
759
|
+
const delivered = watched.occurrence.state === "succeeded";
|
|
647
760
|
return {
|
|
648
761
|
data: {
|
|
649
|
-
|
|
650
|
-
occurrence:
|
|
651
|
-
plannedFor: created.occurrence.plannedFor,
|
|
652
|
-
replayed,
|
|
653
|
-
schedule: created.schedule.id,
|
|
762
|
+
...data,
|
|
763
|
+
wait: { attempts, occurrence: watched.occurrence, settled: true },
|
|
654
764
|
},
|
|
655
765
|
human: [
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
]
|
|
666
|
-
: []),
|
|
766
|
+
delivered
|
|
767
|
+
? `Delivered. ${outcomeOf(attempts.find((attempt) => attempt.state === "succeeded"))}`
|
|
768
|
+
: `Not delivered: the first call settled ${watched.occurrence.state}.`,
|
|
769
|
+
// The attempt rows only when they explain something: a delivered call
|
|
770
|
+
// has said all it needs to in the line above.
|
|
771
|
+
...(delivered || attempts.length === 0
|
|
772
|
+
? []
|
|
773
|
+
: [``, ...attempts.map(attemptLine)]),
|
|
774
|
+
...repeating,
|
|
667
775
|
].join("\n"),
|
|
668
|
-
|
|
776
|
+
...(delivered
|
|
777
|
+
? {}
|
|
778
|
+
: (() => {
|
|
779
|
+
const nextMove = recoveryFor("read-attempt", "attempt-failed");
|
|
780
|
+
return nextMove === undefined ? {} : { nextMove };
|
|
781
|
+
})()),
|
|
669
782
|
};
|
|
670
783
|
};
|
|
784
|
+
function attemptLine(attempt) {
|
|
785
|
+
return `${attempt.state.padEnd(20)}${attempt.responseStatus === undefined ? "no response" : `answered ${String(attempt.responseStatus)}`}${attempt.diagnostic === undefined ? "" : ` ${attempt.diagnostic}`}${attempt.remediation === undefined ? "" : ` / ${attempt.remediation}`}`;
|
|
786
|
+
}
|
|
787
|
+
function outcomeOf(attempt) {
|
|
788
|
+
return attempt?.responseStatus === undefined
|
|
789
|
+
? "Your receiver accepted the call."
|
|
790
|
+
: `Your receiver answered ${String(attempt.responseStatus)}.`;
|
|
791
|
+
}
|
|
671
792
|
const schedulesRead = async (runtime, args) => {
|
|
672
793
|
const id = args.command[0];
|
|
673
794
|
if (id === undefined) {
|
|
@@ -763,21 +884,28 @@ const exportsCancel = async (runtime, args) => {
|
|
|
763
884
|
/* -------------------------------------------------------------------------- */
|
|
764
885
|
/* Waiting */
|
|
765
886
|
/* -------------------------------------------------------------------------- */
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
// The interval and the deadline come from the published journey. A number
|
|
775
|
-
// here would be a fourth answer to a question the website, the curl page and
|
|
776
|
-
// the JavaScript page already answer identically.
|
|
777
|
-
const { intervalMs, timeoutMs } = publishedWait();
|
|
887
|
+
/**
|
|
888
|
+
* How long a wait lasts: `--timeout`, or the published journey's deadline.
|
|
889
|
+
*
|
|
890
|
+
* The interval and the deadline come from the published journey. A number
|
|
891
|
+
* here would be a fourth answer to a question the website, the curl page and
|
|
892
|
+
* the JavaScript page already answer identically.
|
|
893
|
+
*/
|
|
894
|
+
function waitTimeoutMs(args) {
|
|
778
895
|
const override = args.options["--timeout"];
|
|
779
|
-
|
|
780
|
-
|
|
896
|
+
return override === undefined
|
|
897
|
+
? publishedWait().timeoutMs
|
|
898
|
+
: parseDuration(override) * 1000;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Reads one occurrence until it settles or the deadline passes, reporting each
|
|
902
|
+
* new state on standard error. Shared by `occurrences wait` and `schedules
|
|
903
|
+
* create --wait`, so the two cannot come to disagree about what settled means
|
|
904
|
+
* or how long the wait lasts.
|
|
905
|
+
*/
|
|
906
|
+
async function watchOccurrence(runtime, session, schedule, occurrence, timeoutMs) {
|
|
907
|
+
const { intervalMs } = publishedWait();
|
|
908
|
+
const deadline = runtime.now() + timeoutMs;
|
|
781
909
|
const seen = [];
|
|
782
910
|
for (;;) {
|
|
783
911
|
const view = responseSchemaFor("occurrences.read")?.parse(await session.client.request("occurrences_read", schedule, occurrence));
|
|
@@ -787,24 +915,43 @@ const occurrencesWait = async (runtime, args) => {
|
|
|
787
915
|
runtime.stderr(` ${state}\n`);
|
|
788
916
|
}
|
|
789
917
|
if (isTerminalOccurrenceState(state)) {
|
|
790
|
-
return {
|
|
791
|
-
data: {
|
|
792
|
-
attempts: view.occurrence.attemptCount,
|
|
793
|
-
occurrence,
|
|
794
|
-
schedule,
|
|
795
|
-
settled: true,
|
|
796
|
-
state,
|
|
797
|
-
states: seen,
|
|
798
|
-
},
|
|
799
|
-
human: `Occurrence ${occurrence} settled ${state} after ${String(view.occurrence.attemptCount)} ${view.occurrence.attemptCount === 1 ? "attempt" : "attempts"}.`,
|
|
800
|
-
nextMove: `Next: ${runtime.programName} attempts list ${schedule} ${occurrence}`,
|
|
801
|
-
};
|
|
918
|
+
return { occurrence: view.occurrence, settled: true, states: seen };
|
|
802
919
|
}
|
|
803
|
-
|
|
804
|
-
|
|
920
|
+
const remaining = deadline - runtime.now();
|
|
921
|
+
if (remaining <= 0) {
|
|
922
|
+
return { settled: false, state, states: seen };
|
|
805
923
|
}
|
|
806
|
-
|
|
924
|
+
// Cut to the time left, so the last read happens at the deadline rather
|
|
925
|
+
// than an interval past it — a --timeout that is not a multiple of the
|
|
926
|
+
// interval means what it says.
|
|
927
|
+
await sleepOrAbort(runtime, Math.min(intervalMs, remaining));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const occurrencesWait = async (runtime, args) => {
|
|
931
|
+
const schedule = args.command[0];
|
|
932
|
+
const occurrence = args.command[1];
|
|
933
|
+
if (schedule === undefined || occurrence === undefined) {
|
|
934
|
+
throw new CliError(`${runtime.programName} occurrences wait needs a schedule and an occurrence`, "usage", `Usage: ${runtime.programName} occurrences wait <schedule> <occurrence>`);
|
|
807
935
|
}
|
|
936
|
+
expectNoExtraArguments(args, 2, `${runtime.programName} occurrences wait <schedule> <occurrence>`);
|
|
937
|
+
const session = await openSession(runtime, { apiUrl: apiUrlOption(args) });
|
|
938
|
+
const watched = await watchOccurrence(runtime, session, schedule, occurrence, waitTimeoutMs(args));
|
|
939
|
+
if (!watched.settled) {
|
|
940
|
+
throw new CliError(`occurrence ${occurrence} was still ${watched.state} when the deadline passed`, "timeout", recoveryFor("await-occurrence", "not-settled"));
|
|
941
|
+
}
|
|
942
|
+
const { attemptCount, state } = watched.occurrence;
|
|
943
|
+
return {
|
|
944
|
+
data: {
|
|
945
|
+
attempts: attemptCount,
|
|
946
|
+
occurrence,
|
|
947
|
+
schedule,
|
|
948
|
+
settled: true,
|
|
949
|
+
state,
|
|
950
|
+
states: watched.states,
|
|
951
|
+
},
|
|
952
|
+
human: `Occurrence ${occurrence} settled ${state} after ${String(attemptCount)} ${attemptCount === 1 ? "attempt" : "attempts"}.`,
|
|
953
|
+
nextMove: `Next: ${runtime.programName} attempts list ${schedule} ${occurrence}`,
|
|
954
|
+
};
|
|
808
955
|
};
|
|
809
956
|
const attemptsList = async (runtime, args) => {
|
|
810
957
|
const schedule = args.command[0];
|
|
@@ -820,9 +967,7 @@ const attemptsList = async (runtime, args) => {
|
|
|
820
967
|
data: { attempts: page.data, occurrence, schedule },
|
|
821
968
|
human: page.data.length === 0
|
|
822
969
|
? "No attempt has been made yet."
|
|
823
|
-
: page.data
|
|
824
|
-
.map((attempt) => `${attempt.state.padEnd(20)}${attempt.responseStatus === undefined ? "no response" : `answered ${String(attempt.responseStatus)}`}${attempt.diagnostic === undefined ? "" : ` ${attempt.diagnostic}`}${attempt.remediation === undefined ? "" : ` / ${attempt.remediation}`}`)
|
|
825
|
-
.join("\n"),
|
|
970
|
+
: page.data.map(attemptLine).join("\n"),
|
|
826
971
|
// **The journey's own sentence, not a paraphrase of the code.**
|
|
827
972
|
// `diagnostic` and `remediation` are closed vocabularies rather than prose
|
|
828
973
|
// — the step's `observe` says to read them, and its recovery entry is the
|
|
@@ -850,6 +995,7 @@ const attemptsList = async (runtime, args) => {
|
|
|
850
995
|
*/
|
|
851
996
|
const IMPLEMENTATIONS = {
|
|
852
997
|
...OPERATE_COMMANDS,
|
|
998
|
+
...MEMBER_IMPLEMENTATIONS,
|
|
853
999
|
"attempts list": attemptsList,
|
|
854
1000
|
context,
|
|
855
1001
|
"destinations create": destinationsCreate,
|
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)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MEMBER_COMMANDS } from "./catalog.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MEMBER_COMMANDS } from "./catalog.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ParsedArguments } from "./args.js";
|
|
2
|
+
import type { CommandResult } from "./output.js";
|
|
3
|
+
import { type CliRuntime } from "./runtime.js";
|
|
4
|
+
export declare const MEMBER_IMPLEMENTATIONS: {
|
|
5
|
+
[k: string]: (runtime: CliRuntime, args: ParsedArguments) => Promise<CommandResult>;
|
|
6
|
+
};
|