@greatwait/cli 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
@@ -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 Greatwait API",
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", { url }));
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 key is shown once and is not recoverable. Store it now — your receiver`,
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
- throw new CliError(`${runtime.programName} schedules create needs --endpoint`, "usage", `Usage: ${usage}`);
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
- idempotencyKey,
650
- occurrence: created.occurrence.id,
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
- `Schedule ${created.schedule.id}`,
657
- `Occurrence ${created.occurrence.id}`,
658
- `Planned for ${created.occurrence.plannedFor}`,
659
- `Retry key ${idempotencyKey}`,
660
- ...(replayed
661
- ? [
662
- ``,
663
- `This key had already been used for this exact request, so nothing was`,
664
- `created a second time and no second callback will be delivered.`,
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
- nextMove: `Next: ${runtime.programName} occurrences wait ${created.schedule.id} ${created.occurrence.id}`,
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
- const occurrencesWait = async (runtime, args) => {
767
- const schedule = args.command[0];
768
- const occurrence = args.command[1];
769
- if (schedule === undefined || occurrence === undefined) {
770
- throw new CliError(`${runtime.programName} occurrences wait needs a schedule and an occurrence`, "usage", `Usage: ${runtime.programName} occurrences wait <schedule> <occurrence>`);
771
- }
772
- expectNoExtraArguments(args, 2, `${runtime.programName} occurrences wait <schedule> <occurrence>`);
773
- const session = await openSession(runtime, { apiUrl: apiUrlOption(args) });
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
- const deadline = runtime.now() +
780
- (override === undefined ? timeoutMs : parseDuration(override) * 1000);
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
- if (runtime.now() >= deadline) {
804
- throw new CliError(`occurrence ${occurrence} was still ${state} when the deadline passed`, "timeout", recoveryFor("await-occurrence", "not-settled"));
920
+ const remaining = deadline - runtime.now();
921
+ if (remaining <= 0) {
922
+ return { settled: false, state, states: seen };
805
923
  }
806
- await sleepOrAbort(runtime, intervalMs);
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 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)
@@ -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
+ };