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

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/catalog.js CHANGED
@@ -529,6 +529,11 @@ export const COMMAND_CATALOG = [
529
529
  summary: "Your own label, such as a customer id. Each label is paced on its own, so one busy label can't hold up the rest. Can't be changed later.",
530
530
  value: "<key>",
531
531
  },
532
+ {
533
+ flag: "--auto-pause-after-failures",
534
+ summary: 'Pause a recurring schedule after 1–100 consecutive failed runs, or "never". Defaults to 5 on create; omission on replace keeps the setting. Resume resets the count.',
535
+ value: "<count|never>",
536
+ },
532
537
  {
533
538
  flag: "--latency-class",
534
539
  summary: "realtime or bulk. realtime goes first when both are due. By default a call due within two minutes is realtime, and a repeating one is bulk.",
@@ -605,6 +610,11 @@ export const COMMAND_CATALOG = [
605
610
  summary: "Your own label, such as a customer id. Each label is paced on its own, so one busy label can't hold up the rest. Can't be changed later.",
606
611
  value: "<key>",
607
612
  },
613
+ {
614
+ flag: "--auto-pause-after-failures",
615
+ summary: 'Pause a recurring schedule after 1–100 consecutive failed runs, or "never". Defaults to 5 on create; omission on replace keeps the setting. Resume resets the count.',
616
+ value: "<count|never>",
617
+ },
608
618
  {
609
619
  flag: "--latency-class",
610
620
  summary: "realtime or bulk. realtime goes first when both are due. By default a call due within two minutes is realtime, and a repeating one is bulk.",
@@ -834,7 +844,14 @@ export const COMMAND_CATALOG = [
834
844
  {
835
845
  group: "Occurrences and attempts",
836
846
  operations: ["attempts.read"],
837
- options: [API_URL],
847
+ options: [
848
+ API_URL,
849
+ {
850
+ flag: "--response-file",
851
+ value: "path",
852
+ summary: "Save the captured response body in a new private file.",
853
+ },
854
+ ],
838
855
  positionals: [
839
856
  {
840
857
  name: "<schedule>",
package/dist/commands.js CHANGED
@@ -805,11 +805,21 @@ const schedulesRead = async (runtime, args) => {
805
805
  recurrence: view.schedule.recurrence.kind,
806
806
  schedule: view.schedule.id,
807
807
  status: view.schedule.status,
808
+ autoPauseAfterFailures: view.schedule.autoPauseAfterFailures,
809
+ consecutiveFailedRuns: view.schedule.consecutiveFailedRuns,
810
+ autoPauseReason: view.schedule.autoPauseReason,
808
811
  version: view.schedule.version,
809
812
  },
810
813
  human: [
811
814
  `Schedule ${view.schedule.id}`,
812
815
  `Status ${view.schedule.status}`,
816
+ `Auto-pause ${String(view.schedule.autoPauseAfterFailures ?? 5)}`,
817
+ `Failed runs ${String(view.schedule.consecutiveFailedRuns ?? 0)}`,
818
+ ...(view.schedule.autoPauseReason == null
819
+ ? []
820
+ : [
821
+ `Paused ${String(view.schedule.autoPauseReason.failedRuns)} failed runs; ${view.schedule.autoPauseReason.status === undefined ? "no HTTP response" : `HTTP ${String(view.schedule.autoPauseReason.status)}`} (${view.schedule.autoPauseReason.diagnostic}) at ${view.schedule.autoPauseReason.pausedAt}`,
822
+ ]),
813
823
  `Recurrence ${view.schedule.recurrence.kind}`,
814
824
  `Version ${String(view.schedule.version)}`,
815
825
  ...(etag === undefined ? [] : [`ETag ${etag}`]),
package/dist/operate.js CHANGED
@@ -1,4 +1,5 @@
1
- import { httpRequestSchema, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
1
+ import { writeFile } from "node:fs/promises";
2
+ import { autoPauseAfterFailuresSchema, httpRequestSchema, sanitizedDiagnosticText, diagnosticBodyText, responseBodySentence, requestCaptureSentence, attemptStatusGuidance, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
2
3
  import { parseDuration } from "./args.js";
3
4
  import { confirmChange } from "./confirm.js";
4
5
  import { redactSecret } from "./output.js";
@@ -116,6 +117,12 @@ async function scheduleRequest(runtime, args, usage) {
116
117
  : httpRequestSchema.safeParse(httpInput);
117
118
  if (http !== undefined && !http.success)
118
119
  throw new CliError("--http settings are invalid", "usage", http.error.issues.map((issue) => issue.message).join("; "));
120
+ const pauseInput = args.options["--auto-pause-after-failures"];
121
+ const pause = pauseInput === undefined
122
+ ? undefined
123
+ : autoPauseAfterFailuresSchema.safeParse(pauseInput === "never" ? pauseInput : Number(pauseInput));
124
+ if (pause !== undefined && !pause.success)
125
+ throw new CliError('--auto-pause-after-failures must be a whole number from 1 to 100, or "never"', "usage");
119
126
  const latencyClass = args.options["--latency-class"];
120
127
  const partitionKey = args.options["--partition-key"];
121
128
  return {
@@ -124,6 +131,7 @@ async function scheduleRequest(runtime, args, usage) {
124
131
  ...(endpoint === undefined ? {} : { endpoint }),
125
132
  ...(url === undefined ? {} : { url }),
126
133
  ...(latencyClass === undefined ? {} : { latencyClass }),
134
+ ...(pause?.success ? { autoPauseAfterFailures: pause.data } : {}),
127
135
  ...(partitionKey === undefined ? {} : { partitionKey }),
128
136
  ...(timeZone === undefined ? {} : { timeZone }),
129
137
  ...recurrence,
@@ -494,9 +502,52 @@ const attemptsRead = async (runtime, args) => {
494
502
  const active = await session(runtime, args);
495
503
  const view = parseResponse("attempts.read", await active.client.request("attempts_read", schedule, occurrence, attempt));
496
504
  const found = view.attempt;
505
+ const responseFile = args.options["--response-file"];
506
+ if (responseFile !== undefined) {
507
+ if (found.response?.body === undefined)
508
+ throw new CliError(responseBodySentence(found.response?.bodyState ?? "not_captured", found.responseStatus === undefined), "validation");
509
+ const body = found.response.body;
510
+ try {
511
+ await writeFile(responseFile, body.encoding === "base64"
512
+ ? Buffer.from(body.content, "base64")
513
+ : Buffer.from(body.content, "utf8"), { mode: 0o600, flag: "wx" });
514
+ }
515
+ catch {
516
+ throw new CliError("The response file could not be created. Choose a path that does not already exist.", "usage");
517
+ }
518
+ }
519
+ const diagnosticLines = [];
520
+ if (found.response !== undefined) {
521
+ const response = found.response;
522
+ diagnosticLines.push(`Server response ${String(found.responseStatus ?? "")} ${response.statusText}`, ...response.headers.map((h) => `${h.name}: ${h.value}`));
523
+ if (response.body === undefined)
524
+ diagnosticLines.push(responseBodySentence(response.bodyState, found.responseStatus === undefined));
525
+ else {
526
+ const body = response.body;
527
+ if (body.incomplete)
528
+ diagnosticLines.push("The response stream stopped during capture; this is a partial answer.");
529
+ if (body.truncated)
530
+ diagnosticLines.push(`Response cut at 10 KiB (10,240 bytes)${body.originalBytes === undefined ? "" : `; original length ${String(body.originalBytes)} bytes`}.`);
531
+ diagnosticLines.push(diagnosticBodyText(body) !== undefined
532
+ ? diagnosticBodyText(body)
533
+ : `Binary response (${String(body.capturedBytes)} bytes). Download with --response-file <path>.`);
534
+ }
535
+ }
536
+ diagnosticLines.push("What we sent");
537
+ if (found.request === undefined)
538
+ diagnosticLines.push(requestCaptureSentence(found.requestState, found.state === undefined));
539
+ else
540
+ diagnosticLines.push(`${found.request.method} ${found.request.url}`, ...found.request.headers.map((h) => `${h.name}: ${h.value}`), found.request.body === "" ? "We sent no body." : found.request.body);
541
+ const guidance = attemptStatusGuidance(found.responseStatus);
542
+ if (guidance !== undefined)
543
+ diagnosticLines.push(guidance);
497
544
  return {
498
545
  data: { attempt: found, freshness: freshnessOf(view) },
499
546
  human: [
547
+ ...diagnosticLines.map(sanitizedDiagnosticText),
548
+ ...(responseFile === undefined
549
+ ? []
550
+ : [`Response saved to ${sanitizedDiagnosticText(responseFile)}`]),
500
551
  `Attempt ${found.id}`,
501
552
  `Number ${String(found.attemptNumber)}`,
502
553
  ...(found.state === undefined ? [] : [`State ${found.state}`]),
package/package.json CHANGED
@@ -7,8 +7,8 @@
7
7
  "url": "https://github.com/flowxo/greatwait/issues"
8
8
  },
9
9
  "dependencies": {
10
- "@greatwait/api-client": "0.1.0-alpha.6",
11
- "@greatwait/contracts": "0.1.0-alpha.6"
10
+ "@greatwait/api-client": "0.1.0-alpha.7",
11
+ "@greatwait/contracts": "0.1.0-alpha.7"
12
12
  },
13
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": {
@@ -48,5 +48,5 @@
48
48
  },
49
49
  "sideEffects": false,
50
50
  "type": "module",
51
- "version": "0.1.0-alpha.6"
51
+ "version": "0.1.0-alpha.7"
52
52
  }