@greatwait/cli 0.3.1 → 0.3.3
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/args.js +1 -1
- package/dist/bulk.js +4 -4
- package/dist/catalog.js +52 -10
- package/dist/commands.js +77 -32
- package/dist/operate.js +109 -7
- package/dist/support.d.ts +16 -0
- package/dist/support.js +24 -0
- package/package.json +3 -3
package/dist/args.js
CHANGED
|
@@ -29,7 +29,7 @@ const SECRET_BEARING_FLAGS = new Set(["--api-key"]);
|
|
|
29
29
|
* repeats the value back.
|
|
30
30
|
*/
|
|
31
31
|
export function secretArgumentAdvice(programName) {
|
|
32
|
-
return `A
|
|
32
|
+
return `A key passed as an argument is visible to every process on this machine and lands in your shell history, so treat it as exposed and replace it. Next time, run the command with a bare --api-key and paste the key when asked, or pipe it in: printf %s "$KEY" | ${programName} login --api-key`;
|
|
33
33
|
}
|
|
34
34
|
export function parseArguments(argv,
|
|
35
35
|
/**
|
package/dist/bulk.js
CHANGED
|
@@ -71,10 +71,10 @@ const confirm = async (runtime, args) => {
|
|
|
71
71
|
const active = await session(runtime, args);
|
|
72
72
|
const current = parseResponse("bulk.read", await active.client.request("bulk_read", id));
|
|
73
73
|
if (!current.complete || current.intentDigest !== parsed.data.intentDigest)
|
|
74
|
-
throw new CliError("The job
|
|
74
|
+
throw new CliError("The job isn't finished, or the preview changed. Make a new preview", "validation");
|
|
75
75
|
if (current.intent.operation === "retry" &&
|
|
76
76
|
!parsed.data.acknowledgeDuplicateEffects)
|
|
77
|
-
throw new CliError("Retry requires --acknowledge-duplicate-effects", "usage", "
|
|
77
|
+
throw new CliError("Retry requires --acknowledge-duplicate-effects", "usage", "Your server may already have acted. Redelivering can make it act twice.");
|
|
78
78
|
const confirmation = await confirmChange(runtime, args, {
|
|
79
79
|
action: `Confirm ${current.intent.operation} for frozen bulk job ${id}.`,
|
|
80
80
|
effect: [
|
|
@@ -84,7 +84,7 @@ const confirm = async (runtime, args) => {
|
|
|
84
84
|
`parameters ${JSON.stringify(current.intent.parameters)}`,
|
|
85
85
|
`matched ${String(current.counts.matched)}; eligible ${String(current.counts.eligible)}; ineligible ${String(current.counts.ineligible)}`,
|
|
86
86
|
...current.warnings,
|
|
87
|
-
"
|
|
87
|
+
"Requests already sent can't be recalled, and each attempt is billed as usual.",
|
|
88
88
|
],
|
|
89
89
|
});
|
|
90
90
|
const key = args.options["--idempotency-key"] ??
|
|
@@ -114,7 +114,7 @@ function control(action) {
|
|
|
114
114
|
action: `${action} remaining work in bulk job ${id}.`,
|
|
115
115
|
effect: [
|
|
116
116
|
`operation ${current.intent.operation}; state ${current.state}; expected version ${String(parsed.data.expectedVersion)}`,
|
|
117
|
-
"This controls the job, not
|
|
117
|
+
"This controls the job, not the schedules in it. Items already started may finish.",
|
|
118
118
|
],
|
|
119
119
|
});
|
|
120
120
|
const job = parseResponse(`bulk.${action}`, await active.client.request(`bulk_${action}`, id, parsed.data));
|
package/dist/catalog.js
CHANGED
|
@@ -282,7 +282,7 @@ export const COMMAND_CATALOG = [
|
|
|
282
282
|
{
|
|
283
283
|
group: "Bulk operations",
|
|
284
284
|
words: "bulk preview",
|
|
285
|
-
summary: "
|
|
285
|
+
summary: "Preview a retry, cancel, reschedule, pause or resume across matching or chosen items.",
|
|
286
286
|
operations: ["bulk.preview"],
|
|
287
287
|
positionals: [],
|
|
288
288
|
options: [
|
|
@@ -290,14 +290,14 @@ export const COMMAND_CATALOG = [
|
|
|
290
290
|
{
|
|
291
291
|
flag: "--intent",
|
|
292
292
|
value: "<json|@file|->",
|
|
293
|
-
summary: "
|
|
293
|
+
summary: "The bulk request: operation, target, selection and parameters. A preview never sends anything or changes schedules.",
|
|
294
294
|
},
|
|
295
295
|
],
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
group: "Bulk operations",
|
|
299
299
|
words: "bulk confirm",
|
|
300
|
-
summary: "Confirm
|
|
300
|
+
summary: "Confirm a preview and start its job.",
|
|
301
301
|
operations: ["bulk.read", "bulk.confirm"],
|
|
302
302
|
confirms: true,
|
|
303
303
|
positionals: [
|
|
@@ -314,7 +314,7 @@ export const COMMAND_CATALOG = [
|
|
|
314
314
|
},
|
|
315
315
|
{
|
|
316
316
|
flag: "--acknowledge-duplicate-effects",
|
|
317
|
-
summary: "
|
|
317
|
+
summary: "Confirm you understand a retry may reach your server twice. Required for retry.",
|
|
318
318
|
},
|
|
319
319
|
],
|
|
320
320
|
},
|
|
@@ -331,7 +331,7 @@ export const COMMAND_CATALOG = [
|
|
|
331
331
|
{
|
|
332
332
|
group: "Bulk operations",
|
|
333
333
|
words: "bulk list",
|
|
334
|
-
summary: "List
|
|
334
|
+
summary: "List bulk jobs in this project and environment.",
|
|
335
335
|
operations: ["bulk.list"],
|
|
336
336
|
positionals: [],
|
|
337
337
|
options: [
|
|
@@ -703,7 +703,7 @@ export const COMMAND_CATALOG = [
|
|
|
703
703
|
{
|
|
704
704
|
flag: "--diagnostic",
|
|
705
705
|
value: "<category>",
|
|
706
|
-
summary: "
|
|
706
|
+
summary: "Why an attempt failed, including network failures with no response.",
|
|
707
707
|
},
|
|
708
708
|
{
|
|
709
709
|
flag: "--endpoint",
|
|
@@ -754,12 +754,42 @@ export const COMMAND_CATALOG = [
|
|
|
754
754
|
value: "<instant>",
|
|
755
755
|
summary: "Only schedules with an unsettled run before this RFC 3339 instant.",
|
|
756
756
|
},
|
|
757
|
+
{
|
|
758
|
+
flag: "--due-after",
|
|
759
|
+
value: "<instant>",
|
|
760
|
+
summary: "Only schedules whose next run is at or after this RFC 3339 instant.",
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
flag: "--order",
|
|
764
|
+
value: "<order>",
|
|
765
|
+
summary: "created (the default) or due. due lists what runs next first, and leaves out schedules with no next run.",
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
flag: "--direction",
|
|
769
|
+
value: "<direction>",
|
|
770
|
+
summary: "asc (the default) or desc: newest created, or latest due, first. Keep it when you carry on with --cursor.",
|
|
771
|
+
},
|
|
757
772
|
...PAGE_OPTIONS,
|
|
758
773
|
],
|
|
759
774
|
positionals: [],
|
|
760
775
|
summary: "List this project's schedules.",
|
|
761
776
|
words: "schedules list",
|
|
762
777
|
},
|
|
778
|
+
{
|
|
779
|
+
group: "Schedules",
|
|
780
|
+
operations: ["schedules.summary"],
|
|
781
|
+
options: [
|
|
782
|
+
API_URL,
|
|
783
|
+
{
|
|
784
|
+
flag: "--time-zone",
|
|
785
|
+
value: "<zone>",
|
|
786
|
+
summary: "IANA time zone the daily counts are taken in, such as Europe/London. Defaults to UTC.",
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
positionals: [],
|
|
790
|
+
summary: "Count this project's schedules by status, and those created and finished each day for 30 days.",
|
|
791
|
+
words: "schedules summary",
|
|
792
|
+
},
|
|
763
793
|
{
|
|
764
794
|
group: "Schedules",
|
|
765
795
|
operations: ["schedules.read"],
|
|
@@ -826,7 +856,7 @@ export const COMMAND_CATALOG = [
|
|
|
826
856
|
},
|
|
827
857
|
{
|
|
828
858
|
flag: "--latency-class",
|
|
829
|
-
summary: "realtime or bulk. realtime goes first when both are due.
|
|
859
|
+
summary: "auto, realtime or bulk. realtime goes first when both are due. auto, the default, makes a call due within a minute realtime and everything else bulk.",
|
|
830
860
|
value: "<class>",
|
|
831
861
|
},
|
|
832
862
|
{
|
|
@@ -923,7 +953,7 @@ export const COMMAND_CATALOG = [
|
|
|
923
953
|
},
|
|
924
954
|
{
|
|
925
955
|
flag: "--latency-class",
|
|
926
|
-
summary: "realtime or bulk. realtime goes first when both are due.
|
|
956
|
+
summary: "auto, realtime or bulk. realtime goes first when both are due. auto, the default, makes a call due within a minute realtime and everything else bulk.",
|
|
927
957
|
value: "<class>",
|
|
928
958
|
},
|
|
929
959
|
{
|
|
@@ -960,7 +990,7 @@ export const COMMAND_CATALOG = [
|
|
|
960
990
|
summary: "The schedule's id, starting sch_, from schedules create or schedules list.",
|
|
961
991
|
},
|
|
962
992
|
],
|
|
963
|
-
summary: "Change when a schedule runs. Destination, data, HTTP and delivery settings stay unless you give new ones.
|
|
993
|
+
summary: "Change when a schedule runs. Destination, data, HTTP and delivery settings stay unless you give new ones. Priority and late-run policy are worked out again, and pending runs are dropped. Refused if someone else changed it first.",
|
|
964
994
|
words: "schedules replace",
|
|
965
995
|
},
|
|
966
996
|
{
|
|
@@ -1041,7 +1071,9 @@ export const COMMAND_CATALOG = [
|
|
|
1041
1071
|
{
|
|
1042
1072
|
group: "Data exports",
|
|
1043
1073
|
operations: ["exports.part"],
|
|
1044
|
-
|
|
1074
|
+
// The cursor, not the page size: a part is one bounded file, and the
|
|
1075
|
+
// command carries on from the cursor the previous part printed.
|
|
1076
|
+
options: [API_URL, PAGE_OPTIONS[1]],
|
|
1045
1077
|
positionals: [
|
|
1046
1078
|
{
|
|
1047
1079
|
name: "<export>",
|
|
@@ -1824,6 +1856,11 @@ export const COMMAND_CATALOG = [
|
|
|
1824
1856
|
* control plane refuses on purpose.
|
|
1825
1857
|
*/
|
|
1826
1858
|
export const OPERATION_OMISSIONS = [
|
|
1859
|
+
{
|
|
1860
|
+
because: "Dismiss answers the delivery-failure line a member sees in the workspace chrome (W9-79); a terminal has no such line to clear. `billing notices` lists the notices, and the MCP tool provides the automation path.",
|
|
1861
|
+
ground: "declared",
|
|
1862
|
+
operation: "billing.noticesdismiss",
|
|
1863
|
+
},
|
|
1827
1864
|
{
|
|
1828
1865
|
because: "Organization lifecycle authority belongs to a signed-in organization administrator in a browser session. A CLI credential is deliberately project-bound and cannot acquire or widen itself to organization authority.",
|
|
1829
1866
|
ground: "principal-unreachable",
|
|
@@ -1889,6 +1926,11 @@ export const OPERATION_OMISSIONS = [
|
|
|
1889
1926
|
ground: "principal-unreachable",
|
|
1890
1927
|
operation: "projects.create",
|
|
1891
1928
|
},
|
|
1929
|
+
{
|
|
1930
|
+
because: "The counted project read is the deletion preview a signed-in person sees in Project General before deleting; a credential is not accepted.",
|
|
1931
|
+
ground: "principal-unreachable",
|
|
1932
|
+
operation: "projects.read",
|
|
1933
|
+
},
|
|
1892
1934
|
{
|
|
1893
1935
|
because: "Project General is the accepted rename surface in this story. The generated SDK and MCP expose the operation to automation; adding a second interactive CLI design is outside it.",
|
|
1894
1936
|
ground: "declared",
|
package/dist/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readPrivateJsonOption } from "./support.js";
|
|
2
2
|
import { MEMBER_IMPLEMENTATIONS } from "./member-commands.js";
|
|
3
|
-
import { GreatwaitClientError, createGreatwaitClient, } from "@greatwait/api-client";
|
|
4
|
-
import { endpointHeaderChangesSchema, isTerminalOccurrenceState, responseSchemaFor, } from "@greatwait/contracts";
|
|
3
|
+
import { GreatwaitClientError, WAIT_ATTEMPT_PAGES_CEILING, createGreatwaitClient, } from "@greatwait/api-client";
|
|
4
|
+
import { PAGE_SIZE, endpointHeaderChangesSchema, isTerminalOccurrenceState, responseSchemaFor, } from "@greatwait/contracts";
|
|
5
5
|
import { parseDuration } from "./args.js";
|
|
6
6
|
import { COMMAND_CATALOG, REQUESTED_SCOPES } from "./catalog.js";
|
|
7
7
|
import { confirmChange } from "./confirm.js";
|
|
@@ -12,7 +12,7 @@ import { CliError, sleepOrAbort } from "./runtime.js";
|
|
|
12
12
|
import { publishedWait, recoveryFor, renderJourney, STEP_COMMANDS, } from "./journey.js";
|
|
13
13
|
import { anonymousClient, ENVIRONMENT_NAMES, openCredential, openSession, resolveOrigin, storeFor, } from "./session.js";
|
|
14
14
|
import { OPERATE_COMMANDS, destinationLines, scheduleRequest } from "./operate.js";
|
|
15
|
-
import { apiUrlOption, expectNoExtraArguments, parseResponse, positionals, } from "./support.js";
|
|
15
|
+
import { apiUrlOption, continuationNote, drainPages, expectNoExtraArguments, pageFlagsCommand, pageRequest, parseResponse, positionals, shellQuote, } from "./support.js";
|
|
16
16
|
/* -------------------------------------------------------------------------- */
|
|
17
17
|
/* Signing in */
|
|
18
18
|
/* -------------------------------------------------------------------------- */
|
|
@@ -186,7 +186,7 @@ const login = async (runtime, args) => {
|
|
|
186
186
|
},
|
|
187
187
|
human: described.known
|
|
188
188
|
? `Signed in to ${origin}, in project ${described.context.project} (${described.context.environmentKind}).`
|
|
189
|
-
: `Signed in to ${origin}
|
|
189
|
+
: `Signed in to ${origin}, but the service couldn't say what this sign-in can reach.`,
|
|
190
190
|
...(described.known ? {} : { nextMove: described.reason }),
|
|
191
191
|
};
|
|
192
192
|
}
|
|
@@ -232,7 +232,7 @@ async function describeThrough(client, origin, programName) {
|
|
|
232
232
|
if (error.status === 404) {
|
|
233
233
|
return {
|
|
234
234
|
known: false,
|
|
235
|
-
reason: `${origin}
|
|
235
|
+
reason: `${origin} is running an older build without GET /v1/context, so it can't say which project this credential acts in.`,
|
|
236
236
|
};
|
|
237
237
|
}
|
|
238
238
|
// **Anything else is the service having a moment, not a bad credential.**
|
|
@@ -767,12 +767,26 @@ const schedulesCreate = async (runtime, args) => {
|
|
|
767
767
|
})(),
|
|
768
768
|
};
|
|
769
769
|
}
|
|
770
|
-
|
|
770
|
+
// Every attempt page, bounded the way the SDK's wait is (W9-80): the first
|
|
771
|
+
// page alone settled a long retry history looking complete.
|
|
772
|
+
const { attempts, nextCursor } = await settledAttempts(session, schedule, occurrence);
|
|
771
773
|
const delivered = watched.occurrence.state === "succeeded";
|
|
774
|
+
const more = nextCursor === undefined
|
|
775
|
+
? undefined
|
|
776
|
+
: `More attempts than this wait reads. Continue with: ${runtime.programName} attempts list ${schedule} ${occurrence}${pageFlagsCommand(args)} --cursor ${nextCursor}`;
|
|
777
|
+
const failedMove = delivered
|
|
778
|
+
? undefined
|
|
779
|
+
: recoveryFor("read-attempt", "attempt-failed");
|
|
780
|
+
const moves = [failedMove, more].filter((move) => move !== undefined);
|
|
772
781
|
return {
|
|
773
782
|
data: {
|
|
774
783
|
...data,
|
|
775
|
-
wait: {
|
|
784
|
+
wait: {
|
|
785
|
+
attempts,
|
|
786
|
+
...(nextCursor === undefined ? {} : { attemptsNextCursor: nextCursor }),
|
|
787
|
+
occurrence: watched.occurrence,
|
|
788
|
+
settled: true,
|
|
789
|
+
},
|
|
776
790
|
},
|
|
777
791
|
human: [
|
|
778
792
|
delivered
|
|
@@ -785,14 +799,28 @@ const schedulesCreate = async (runtime, args) => {
|
|
|
785
799
|
: [``, ...attempts.map(attemptLine)]),
|
|
786
800
|
...repeating,
|
|
787
801
|
].join("\n"),
|
|
788
|
-
...(
|
|
789
|
-
? {}
|
|
790
|
-
: (() => {
|
|
791
|
-
const nextMove = recoveryFor("read-attempt", "attempt-failed");
|
|
792
|
-
return nextMove === undefined ? {} : { nextMove };
|
|
793
|
-
})()),
|
|
802
|
+
...(moves.length === 0 ? {} : { nextMove: moves.join("\n") }),
|
|
794
803
|
};
|
|
795
804
|
};
|
|
805
|
+
/**
|
|
806
|
+
* A settled occurrence's attempts: every page at the contract's largest size,
|
|
807
|
+
* up to the SDK's own ceiling, and the cursor if that stopped the read.
|
|
808
|
+
*/
|
|
809
|
+
async function settledAttempts(session, schedule, occurrence) {
|
|
810
|
+
const attempts = [];
|
|
811
|
+
let cursor;
|
|
812
|
+
for (let pages = 0; pages < WAIT_ATTEMPT_PAGES_CEILING; pages += 1) {
|
|
813
|
+
const page = parseResponse("attempts.list", await session.client.request("attempts_list", schedule, occurrence, {
|
|
814
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
815
|
+
limit: PAGE_SIZE.max,
|
|
816
|
+
}));
|
|
817
|
+
attempts.push(...page.data);
|
|
818
|
+
cursor = page.nextCursor;
|
|
819
|
+
if (cursor === undefined)
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
return { attempts, nextCursor: cursor };
|
|
823
|
+
}
|
|
796
824
|
function attemptLine(attempt) {
|
|
797
825
|
return `${attempt.state.padEnd(20)}${attempt.responseStatus === undefined ? "no response" : `answered ${String(attempt.responseStatus)}`}${attempt.diagnostic === undefined ? "" : ` ${attempt.diagnostic}`}${attempt.remediation === undefined ? "" : ` / ${attempt.remediation}`}`;
|
|
798
826
|
}
|
|
@@ -895,11 +923,12 @@ const exportsRead = async (runtime, args) => {
|
|
|
895
923
|
};
|
|
896
924
|
const exportsPart = async (runtime, args) => {
|
|
897
925
|
const [exportId] = positionals(args, ["<export>"], `${runtime.programName} exports part <export> [--cursor <cursor>]`);
|
|
898
|
-
const
|
|
926
|
+
const apiUrl = apiUrlOption(args);
|
|
927
|
+
const session = await openSession(runtime, { apiUrl });
|
|
899
928
|
const cursor = args.options["--cursor"];
|
|
900
929
|
const part = parseResponse("exports.part", await session.client.request("exports_part", exportId, cursor === undefined ? {} : { cursor }));
|
|
901
930
|
if (part.nextCursor !== undefined && !args.booleans.has("--json")) {
|
|
902
|
-
runtime.stderr(`More to export. Append the next bounded part with: ${runtime.programName} exports part ${exportId} --cursor
|
|
931
|
+
runtime.stderr(`More to export. Append the next bounded part with: ${runtime.programName} exports part ${exportId}${apiUrl === undefined ? "" : ` --api-url ${shellQuote(apiUrl)}`} --cursor ${shellQuote(part.nextCursor)} >> ${part.fileName}\n`);
|
|
903
932
|
}
|
|
904
933
|
return {
|
|
905
934
|
data: part,
|
|
@@ -1009,25 +1038,41 @@ const attemptsList = async (runtime, args) => {
|
|
|
1009
1038
|
}
|
|
1010
1039
|
expectNoExtraArguments(args, 2, `${runtime.programName} attempts list <schedule> <occurrence>`);
|
|
1011
1040
|
const session = await openSession(runtime, { apiUrl: apiUrlOption(args) });
|
|
1012
|
-
|
|
1013
|
-
|
|
1041
|
+
// **One page, `--limit`, `--cursor` and `--all`, like every other listing**
|
|
1042
|
+
// (W9-80). This read only ever fetched the first page and ignored the page
|
|
1043
|
+
// flags its own help declared, so an occurrence retried past one page
|
|
1044
|
+
// showed its earliest tries and never said there were more.
|
|
1045
|
+
const drained = await drainPages(runtime, args, pageRequest(args), async (request) => parseResponse("attempts.list", await session.client.request("attempts_list", schedule, occurrence, request)));
|
|
1046
|
+
const attempts = drained.rows;
|
|
1047
|
+
const failed = attempts.find((attempt) => attempt.state !== "succeeded");
|
|
1048
|
+
const continuation = continuationNote(drained, `${runtime.programName} attempts list ${schedule} ${occurrence}${pageFlagsCommand(args)}`);
|
|
1049
|
+
// **The journey's own sentence, not a paraphrase of the code.**
|
|
1050
|
+
// `diagnostic` and `remediation` are closed vocabularies rather than prose
|
|
1051
|
+
// — the step's `observe` says to read them, and its recovery entry is the
|
|
1052
|
+
// sentence written for a person meeting a failed attempt. Inventing a
|
|
1053
|
+
// second one here would be this command maintaining its own copy of every
|
|
1054
|
+
// delivery failure the service can classify.
|
|
1055
|
+
const recovery = failed === undefined
|
|
1056
|
+
? undefined
|
|
1057
|
+
: recoveryFor("read-attempt", "attempt-failed");
|
|
1058
|
+
const notes = [recovery, continuation].filter((note) => note !== undefined);
|
|
1014
1059
|
return {
|
|
1015
|
-
|
|
1016
|
-
|
|
1060
|
+
// The keys this always had, plus the page bookkeeping every listing
|
|
1061
|
+
// reports; `nextCursor` only when there is more to read.
|
|
1062
|
+
data: {
|
|
1063
|
+
attempts,
|
|
1064
|
+
ceilingReached: drained.ceilingReached,
|
|
1065
|
+
...(drained.nextCursor === undefined
|
|
1066
|
+
? {}
|
|
1067
|
+
: { nextCursor: drained.nextCursor }),
|
|
1068
|
+
occurrence,
|
|
1069
|
+
pages: drained.pages,
|
|
1070
|
+
schedule,
|
|
1071
|
+
},
|
|
1072
|
+
human: attempts.length === 0
|
|
1017
1073
|
? "No attempt has been made yet."
|
|
1018
|
-
:
|
|
1019
|
-
|
|
1020
|
-
// `diagnostic` and `remediation` are closed vocabularies rather than prose
|
|
1021
|
-
// — the step's `observe` says to read them, and its recovery entry is the
|
|
1022
|
-
// sentence written for a person meeting a failed attempt. Inventing a
|
|
1023
|
-
// second one here would be this command maintaining its own copy of every
|
|
1024
|
-
// delivery failure the service can classify.
|
|
1025
|
-
...(failed === undefined
|
|
1026
|
-
? {}
|
|
1027
|
-
: (() => {
|
|
1028
|
-
const nextMove = recoveryFor("read-attempt", "attempt-failed");
|
|
1029
|
-
return nextMove === undefined ? {} : { nextMove };
|
|
1030
|
-
})()),
|
|
1074
|
+
: attempts.map(attemptLine).join("\n"),
|
|
1075
|
+
...(notes.length === 0 ? {} : { nextMove: notes.join("\n") }),
|
|
1031
1076
|
};
|
|
1032
1077
|
};
|
|
1033
1078
|
/* -------------------------------------------------------------------------- */
|
package/dist/operate.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { BULK_COMMANDS } from "./bulk.js";
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import { projectNotificationSettingsUpdateRequestSchema, statusSigningKeyRequestSchema, autoPauseAfterFailuresSchema, deliveryDurationSecondsSchema, deliveryCompletionModeSchema, deliveryOverlapPolicySchema, scheduleRequestSchema, httpRequestSchema, externalIdentifierSchema, scheduleTagsSchema, scheduleTagSchema, scheduleSelectionSchema, normalizeScheduleSelection, projectAttemptSearchRequestSchema, normalizeAttemptSelection, sanitizedDiagnosticText, diagnosticBodyText, responseBodySentence, requestCaptureSentence, attemptStatusGuidance, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, billingRedeemRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
|
|
3
|
+
import { projectNotificationSettingsUpdateRequestSchema, statusSigningKeyRequestSchema, autoPauseAfterFailuresSchema, deliveryDurationSecondsSchema, deliveryCompletionModeSchema, deliveryOverlapPolicySchema, scheduleRequestSchema, httpRequestSchema, externalIdentifierSchema, scheduleTagsSchema, scheduleTagSchema, scheduleSelectionSchema, normalizeScheduleSelection, timeZoneSchema, SCHEDULE_DIRECTIONS, SCHEDULE_ORDERS, projectAttemptSearchRequestSchema, normalizeAttemptSelection, sanitizedDiagnosticText, diagnosticBodyText, responseBodySentence, requestCaptureSentence, attemptStatusGuidance, endpointHeaderChangesSchema, billingAutoTopUpApplyRequestSchema, billingAutoTopUpPreviewRequestSchema, billingAutoTopUpPreviewSchema, billingCeilingUpdateRequestSchema, billingFundingQuoteRequestSchema, billingFundingQuoteSchema, billingNotificationSettingsUpdateRequestSchema, billingRefundRequestCreateSchema, billingRefundResolutionRequestSchema, billingRedeemRequestSchema, projectBillingQuoteSchema, projectBillingQuoteRequestSchema, } from "@greatwait/contracts";
|
|
4
4
|
import { parseDuration } from "./args.js";
|
|
5
5
|
import { confirmChange } from "./confirm.js";
|
|
6
6
|
import { redactSecret } from "./output.js";
|
|
7
7
|
import { CliError } from "./runtime.js";
|
|
8
8
|
import { openSession } from "./session.js";
|
|
9
|
-
import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, readPrivateJsonOption, table, } from "./support.js";
|
|
9
|
+
import { apiUrlOption, conditionalHeaders, continuationNote, drainPages, freshnessNote, pageFlagsCommand, pageRequest, parseInstant, parseResponse, positionals, readJsonOption, readPrivateJsonOption, shellQuote, table, } from "./support.js";
|
|
10
10
|
/* -------------------------------------------------------------------------- */
|
|
11
11
|
/* Shared plumbing */
|
|
12
12
|
/* -------------------------------------------------------------------------- */
|
|
@@ -24,7 +24,7 @@ async function session(runtime, args) {
|
|
|
24
24
|
async function listing(runtime, args, options) {
|
|
25
25
|
const drained = await drainPages(runtime, args, pageRequest(args), options.read);
|
|
26
26
|
const notes = [
|
|
27
|
-
continuationNote(drained, options.command),
|
|
27
|
+
continuationNote(drained, `${options.command}${pageFlagsCommand(args)}`),
|
|
28
28
|
freshnessNote(drained.freshness),
|
|
29
29
|
].filter((note) => note !== undefined);
|
|
30
30
|
return {
|
|
@@ -37,7 +37,11 @@ async function listing(runtime, args, options) {
|
|
|
37
37
|
pages: drained.pages,
|
|
38
38
|
rows: drained.rows,
|
|
39
39
|
},
|
|
40
|
-
|
|
40
|
+
// An empty read that still carries a cursor is not the answer (W9-90):
|
|
41
|
+
// a filtered page can come back empty with more to read.
|
|
42
|
+
human: table(options.columns, drained.rows.map(options.row), drained.nextCursor === undefined
|
|
43
|
+
? options.empty
|
|
44
|
+
: "Nothing matched in the part read so far; more remains to be read."),
|
|
41
45
|
...(notes.length === 0 ? {} : { nextMove: notes.join("\n") }),
|
|
42
46
|
};
|
|
43
47
|
}
|
|
@@ -451,6 +455,16 @@ const attemptsSearch = async (runtime, args) => {
|
|
|
451
455
|
],
|
|
452
456
|
});
|
|
453
457
|
};
|
|
458
|
+
/** An option limited to a published set of words, refused before asking. */
|
|
459
|
+
function oneOf(args, flag, values) {
|
|
460
|
+
const value = args.options[flag];
|
|
461
|
+
if (value === undefined)
|
|
462
|
+
return undefined;
|
|
463
|
+
if (!values.includes(value)) {
|
|
464
|
+
throw new CliError(`${flag} must be ${values.slice(0, -1).join(", ")} or ${values[values.length - 1]}`, "usage");
|
|
465
|
+
}
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
454
468
|
const schedulesList = async (runtime, args) => {
|
|
455
469
|
const parsed = scheduleSelectionSchema.safeParse({
|
|
456
470
|
...correlationFilters(args),
|
|
@@ -461,17 +475,38 @@ const schedulesList = async (runtime, args) => {
|
|
|
461
475
|
dueBefore: args.options["--due-before"] === undefined
|
|
462
476
|
? undefined
|
|
463
477
|
: parseInstant(args.options["--due-before"], "--due-before"),
|
|
478
|
+
dueAfter: args.options["--due-after"] === undefined
|
|
479
|
+
? undefined
|
|
480
|
+
: parseInstant(args.options["--due-after"], "--due-after"),
|
|
464
481
|
});
|
|
465
482
|
if (!parsed.success)
|
|
466
483
|
throw new CliError("The schedule filters are invalid", "usage", parsed.error.issues
|
|
467
484
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
468
485
|
.join("; "));
|
|
486
|
+
const order = oneOf(args, "--order", SCHEDULE_ORDERS);
|
|
487
|
+
const direction = oneOf(args, "--direction", SCHEDULE_DIRECTIONS);
|
|
469
488
|
const filters = normalizeScheduleSelection(parsed.data);
|
|
470
489
|
const active = await session(runtime, args);
|
|
471
490
|
const endpoint = args.options["--endpoint"];
|
|
472
491
|
return listing(runtime, args, {
|
|
473
492
|
columns: ["STATUS", "NAME", "RECURRENCE", "DELIVERS TO", "SCHEDULE"],
|
|
474
|
-
|
|
493
|
+
// Every flag that shaped the request is repeated, quoted: the service
|
|
494
|
+
// refuses a cursor under a different order, direction or filter, so a
|
|
495
|
+
// hint that dropped one could not be pasted (W9-80).
|
|
496
|
+
command: `${runtime.programName} schedules list${correlationCommand(args)}${[
|
|
497
|
+
"--endpoint",
|
|
498
|
+
"--status",
|
|
499
|
+
"--recurrence",
|
|
500
|
+
"--text",
|
|
501
|
+
"--due-before",
|
|
502
|
+
"--due-after",
|
|
503
|
+
"--order",
|
|
504
|
+
"--direction",
|
|
505
|
+
]
|
|
506
|
+
.flatMap((flag) => args.options[flag] === undefined
|
|
507
|
+
? []
|
|
508
|
+
: [` ${flag} ${shellQuote(args.options[flag])}`])
|
|
509
|
+
.join("")}`,
|
|
475
510
|
// An empty filtered page answers the filter, not the project: "no
|
|
476
511
|
// schedules yet" read as the project being empty when nothing matched.
|
|
477
512
|
empty: Object.keys(filters).some((key) => key !== "endpoint")
|
|
@@ -483,6 +518,8 @@ const schedulesList = async (runtime, args) => {
|
|
|
483
518
|
...request,
|
|
484
519
|
...(endpoint === undefined ? {} : { endpoint }),
|
|
485
520
|
...filters,
|
|
521
|
+
...(order === undefined ? {} : { order }),
|
|
522
|
+
...(direction === undefined ? {} : { direction }),
|
|
486
523
|
})),
|
|
487
524
|
row: (schedule) => [
|
|
488
525
|
schedule.status,
|
|
@@ -493,6 +530,64 @@ const schedulesList = async (runtime, args) => {
|
|
|
493
530
|
],
|
|
494
531
|
});
|
|
495
532
|
};
|
|
533
|
+
/**
|
|
534
|
+
* Whole-project schedule counts (W9-80, FXO-3446).
|
|
535
|
+
*
|
|
536
|
+
* The counts a listing cannot give: `schedules list` reads one page, and a
|
|
537
|
+
* total taken from it is the page size. The service computes these over every
|
|
538
|
+
* retained schedule and reuses the answer for up to five minutes, so the age
|
|
539
|
+
* is printed beside them rather than presented as live.
|
|
540
|
+
*/
|
|
541
|
+
const schedulesSummary = async (runtime, args) => {
|
|
542
|
+
positionals(args, [], `${runtime.programName} schedules summary`);
|
|
543
|
+
const timeZone = args.options["--time-zone"];
|
|
544
|
+
if (timeZone !== undefined && !timeZoneSchema.safeParse(timeZone).success)
|
|
545
|
+
throw new CliError(`${timeZone} is not an IANA time zone`, "usage", "Write --time-zone as a zone name, such as Europe/London or America/Chicago.");
|
|
546
|
+
const active = await session(runtime, args);
|
|
547
|
+
const view = parseResponse("schedules.summary", await active.client.request("schedules_summary", timeZone === undefined ? {} : { timeZone }));
|
|
548
|
+
const { counts, pausedAfterFailures } = view;
|
|
549
|
+
const busyDays = view.days.filter((day) => day.created > 0 || day.finished > 0);
|
|
550
|
+
const total = (key) => view.days.reduce((sum, day) => sum + day[key], 0);
|
|
551
|
+
return {
|
|
552
|
+
data: view,
|
|
553
|
+
human: [
|
|
554
|
+
`As of ${ageOf(view.freshness.lagSeconds)} (${view.freshness.asOf})`,
|
|
555
|
+
`Active ${String(counts.active)}`,
|
|
556
|
+
`Paused ${String(counts.paused)}${pausedAfterFailures.count === 0
|
|
557
|
+
? ""
|
|
558
|
+
: `, ${String(pausedAfterFailures.count)} paused itself after repeated failures`}`,
|
|
559
|
+
`Completed ${String(counts.completed)}`,
|
|
560
|
+
`Cancelled ${String(counts.cancelled)}`,
|
|
561
|
+
...(pausedAfterFailures.schedules.length === 0
|
|
562
|
+
? []
|
|
563
|
+
: [
|
|
564
|
+
``,
|
|
565
|
+
table(["PAUSED ITSELF", "AFTER", "WHY", "SCHEDULE"], pausedAfterFailures.schedules.map((schedule) => [
|
|
566
|
+
schedule.name ?? "—",
|
|
567
|
+
`${String(schedule.autoPauseReason.failedRuns)} failed runs`,
|
|
568
|
+
schedule.autoPauseReason.diagnostic,
|
|
569
|
+
schedule.id,
|
|
570
|
+
]), ""),
|
|
571
|
+
]),
|
|
572
|
+
``,
|
|
573
|
+
`${view.window.from} to ${view.window.to} (${view.window.timeZone}): ${String(total("created"))} created, ${String(total("finished"))} finished`,
|
|
574
|
+
// Thirty rows, most of them zero on a quiet project, would bury the
|
|
575
|
+
// days that had something in them; the zero days are in --json.
|
|
576
|
+
table(["DAY", "CREATED", "FINISHED"], busyDays.map((day) => [
|
|
577
|
+
day.date,
|
|
578
|
+
String(day.created),
|
|
579
|
+
String(day.finished),
|
|
580
|
+
]), "No schedule was created or finished in these 30 days."),
|
|
581
|
+
].join("\n"),
|
|
582
|
+
nextMove: "Counts cover retained schedules: retention removes cancelled schedules and one-off schedules that ran, once their history expires.",
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
/** How long ago, in the unit a person reads: seconds, then minutes. */
|
|
586
|
+
function ageOf(seconds) {
|
|
587
|
+
if (seconds < 60)
|
|
588
|
+
return `${String(seconds)}s ago`;
|
|
589
|
+
return `${String(Math.floor(seconds / 60))} min ago`;
|
|
590
|
+
}
|
|
496
591
|
/**
|
|
497
592
|
* A schedule lifecycle change: read, preview where one is owed, act, report.
|
|
498
593
|
*
|
|
@@ -633,7 +728,9 @@ const occurrencesList = async (runtime, args) => {
|
|
|
633
728
|
const active = await session(runtime, args);
|
|
634
729
|
return listing(runtime, args, {
|
|
635
730
|
columns: ["STATE", "PLANNED FOR", "OCCURRENCE", "ATTEMPTS"],
|
|
636
|
-
|
|
731
|
+
// `--newest` is repeated because the service refuses a cursor issued for
|
|
732
|
+
// the other order: a hint without it could not be pasted (W9-80).
|
|
733
|
+
command: `${runtime.programName} occurrences list ${schedule}${args.booleans.has("--newest") ? " --newest" : ""}`,
|
|
637
734
|
empty: "This schedule has no retained occurrences.",
|
|
638
735
|
read: (request) => page("occurrences.list", () => active.client.request("occurrences_list", schedule, {
|
|
639
736
|
...request,
|
|
@@ -735,7 +832,7 @@ const attemptsRead = async (runtime, args) => {
|
|
|
735
832
|
}
|
|
736
833
|
diagnosticLines.push("What we sent");
|
|
737
834
|
if (found.request === undefined)
|
|
738
|
-
diagnosticLines.push(requestCaptureSentence(found.requestState, found.state === undefined));
|
|
835
|
+
diagnosticLines.push(requestCaptureSentence(found.requestState, found.state === undefined, found.requestUnavailable));
|
|
739
836
|
else
|
|
740
837
|
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);
|
|
741
838
|
const guidance = attemptStatusGuidance(found.responseStatus);
|
|
@@ -1912,6 +2009,10 @@ function notificationPreferenceResult(settings) {
|
|
|
1912
2009
|
? []
|
|
1913
2010
|
: [
|
|
1914
2011
|
`Events ${webhook.events.join(", ")}`,
|
|
2012
|
+
// W9-84: the destination is an endpoint, whose key signs.
|
|
2013
|
+
...(webhook.endpoint === undefined
|
|
2014
|
+
? []
|
|
2015
|
+
: [`Endpoint ${webhook.endpoint}`]),
|
|
1915
2016
|
`Signing key ${webhook.signingKey?.id ?? "not provisioned"}`,
|
|
1916
2017
|
]),
|
|
1917
2018
|
].join("\n"),
|
|
@@ -2040,6 +2141,7 @@ export const OPERATE_COMMANDS = {
|
|
|
2040
2141
|
"schedules archive": schedulesArchive,
|
|
2041
2142
|
"schedules cancel": schedulesCancel,
|
|
2042
2143
|
"schedules list": schedulesList,
|
|
2144
|
+
"schedules summary": schedulesSummary,
|
|
2043
2145
|
"executions list": executionsList,
|
|
2044
2146
|
"attempts search": attemptsSearch,
|
|
2045
2147
|
"schedules pause": schedulesPause,
|
package/dist/support.d.ts
CHANGED
|
@@ -103,6 +103,22 @@ export interface DrainedPages<TRow> {
|
|
|
103
103
|
export declare function drainPages<TRow>(runtime: CliRuntime, args: ParsedArguments, request: PageRequest, read: (request: PageRequest) => Promise<Page<TRow>>): Promise<DrainedPages<TRow>>;
|
|
104
104
|
/** The freshness of an answer, in a sentence, or nothing to say. */
|
|
105
105
|
export declare function freshnessNote(freshness: Freshness): string | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* One argument, single-quoted for a POSIX shell, so a pasted hint means what
|
|
108
|
+
* it printed: `$`, backticks and spaces stay literal, and an embedded quote is
|
|
109
|
+
* closed, escaped and reopened.
|
|
110
|
+
*/
|
|
111
|
+
export declare function shellQuote(value: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* The page flags a continuation hint must repeat (W9-80, FXO-3446).
|
|
114
|
+
*
|
|
115
|
+
* **A cursor is only valid with the request that issued it**, and the
|
|
116
|
+
* origin it came from. A hint that dropped `--api-url` sent the cursor to the
|
|
117
|
+
* signed-in default origin, and one that dropped `--limit` read the next page
|
|
118
|
+
* at a different size from the one the person chose. `--cursor` and `--all`
|
|
119
|
+
* are added by {@link continuationNote}.
|
|
120
|
+
*/
|
|
121
|
+
export declare function pageFlagsCommand(args: ParsedArguments): string;
|
|
106
122
|
/**
|
|
107
123
|
* What to say about a listing that stopped before the collection did.
|
|
108
124
|
*
|
package/dist/support.js
CHANGED
|
@@ -216,6 +216,30 @@ export function freshnessNote(freshness) {
|
|
|
216
216
|
return undefined;
|
|
217
217
|
return `Read from a projection that was ${String(freshness.lagSeconds)}s behind at ${freshness.asOf}, so something newer than that may be missing.`;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* One argument, single-quoted for a POSIX shell, so a pasted hint means what
|
|
221
|
+
* it printed: `$`, backticks and spaces stay literal, and an embedded quote is
|
|
222
|
+
* closed, escaped and reopened.
|
|
223
|
+
*/
|
|
224
|
+
export function shellQuote(value) {
|
|
225
|
+
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The page flags a continuation hint must repeat (W9-80, FXO-3446).
|
|
229
|
+
*
|
|
230
|
+
* **A cursor is only valid with the request that issued it**, and the
|
|
231
|
+
* origin it came from. A hint that dropped `--api-url` sent the cursor to the
|
|
232
|
+
* signed-in default origin, and one that dropped `--limit` read the next page
|
|
233
|
+
* at a different size from the one the person chose. `--cursor` and `--all`
|
|
234
|
+
* are added by {@link continuationNote}.
|
|
235
|
+
*/
|
|
236
|
+
export function pageFlagsCommand(args) {
|
|
237
|
+
const limit = args.options["--limit"];
|
|
238
|
+
const apiUrl = args.options["--api-url"];
|
|
239
|
+
// `--limit` is a whole number by the time a page was read (`pageRequest`
|
|
240
|
+
// refuses anything else), so it needs no quoting; an origin might.
|
|
241
|
+
return `${limit === undefined ? "" : ` --limit ${limit}`}${apiUrl === undefined ? "" : ` --api-url ${shellQuote(apiUrl)}`}`;
|
|
242
|
+
}
|
|
219
243
|
/**
|
|
220
244
|
* What to say about a listing that stopped before the collection did.
|
|
221
245
|
*
|
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.3.
|
|
11
|
-
"@greatwait/contracts": "0.3.
|
|
10
|
+
"@greatwait/api-client": "0.3.3",
|
|
11
|
+
"@greatwait/contracts": "0.3.3"
|
|
12
12
|
},
|
|
13
13
|
"description": "The GreatWait command line: sign in, schedule a call to a URL, and read its delivery outcome over the public API.",
|
|
14
14
|
"engines": {
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
},
|
|
49
49
|
"sideEffects": false,
|
|
50
50
|
"type": "module",
|
|
51
|
-
"version": "0.3.
|
|
51
|
+
"version": "0.3.3"
|
|
52
52
|
}
|