@greatwait/cli 0.1.0-alpha.3 → 0.1.0-alpha.4
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.d.ts +2 -2
- package/dist/args.js +33 -16
- package/dist/catalog.d.ts +196 -0
- package/dist/catalog.js +1453 -0
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +42 -31
- package/dist/commands.d.ts +33 -1
- package/dist/commands.js +199 -144
- package/dist/confirm.d.ts +33 -0
- package/dist/confirm.js +17 -0
- package/dist/exit.d.ts +9 -0
- package/dist/exit.js +19 -0
- package/dist/gw.d.ts +2 -0
- package/dist/gw.js +15 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/journey.d.ts +2 -2
- package/dist/journey.js +20 -4
- package/dist/main.js +6 -16
- package/dist/node-runtime.d.ts +15 -1
- package/dist/node-runtime.js +31 -1
- package/dist/oauth.d.ts +1 -1
- package/dist/oauth.js +15 -15
- package/dist/operate.d.ts +39 -0
- package/dist/operate.js +1394 -0
- package/dist/remediation.d.ts +35 -0
- package/dist/remediation.js +73 -0
- package/dist/runtime.d.ts +21 -1
- package/dist/session.js +5 -5
- package/dist/start.d.ts +15 -0
- package/dist/start.js +26 -0
- package/dist/support.d.ts +133 -0
- package/dist/support.js +270 -0
- package/package.json +5 -4
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,1453 @@
|
|
|
1
|
+
import { OPERATIONS } from "@greatwait/contracts";
|
|
2
|
+
import { EXIT_CLASSES, EXIT_CLASS_MEANINGS } from "./exit.js";
|
|
3
|
+
/**
|
|
4
|
+
* The name every written instruction uses.
|
|
5
|
+
*
|
|
6
|
+
* The package installs two executables — `greatwait` and `gw` — and each states
|
|
7
|
+
* its own name, so what a person is told to type next is what they just typed.
|
|
8
|
+
* The *published* reference, the quickstarts and the first-value journey use
|
|
9
|
+
* this one, because a written instruction has to be followable by somebody who
|
|
10
|
+
* installed either.
|
|
11
|
+
*/
|
|
12
|
+
export const CANONICAL_PROGRAM_NAME = "greatwait";
|
|
13
|
+
/** The principals a command line can present. Never a browser session. */
|
|
14
|
+
export const CLI_PRINCIPALS = ["api-credential", "oauth"];
|
|
15
|
+
/* -------------------------------------------------------------------------- */
|
|
16
|
+
/* Derived properties of a registry entry */
|
|
17
|
+
/* -------------------------------------------------------------------------- */
|
|
18
|
+
/** Whether any principal this command line can hold may call it. */
|
|
19
|
+
export function reachableByCli(operation) {
|
|
20
|
+
if (operation.principals.length === 0)
|
|
21
|
+
return true;
|
|
22
|
+
return operation.principals.some((principal) => CLI_PRINCIPALS.some((held) => held === principal));
|
|
23
|
+
}
|
|
24
|
+
/** Whether the API serves it at all, as opposed to the contract mock alone. */
|
|
25
|
+
export function servedByApi(operation) {
|
|
26
|
+
return operation.surfaces.some((surface) => surface === "api");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Why a command covering this operation must confirm before acting, if it must.
|
|
30
|
+
*
|
|
31
|
+
* **Derived from the registry's own hints rather than from a list here**, so an
|
|
32
|
+
* operation that becomes destructive tomorrow makes a command that does not ask
|
|
33
|
+
* fail this gate rather than quietly keep shipping.
|
|
34
|
+
*
|
|
35
|
+
* Two grounds, and the second is the one that took a correction. Every
|
|
36
|
+
* externally visible call is *not* a confirmation: `schedules create` reaches a
|
|
37
|
+
* receiver by design and is the first thing anybody runs, so demanding a
|
|
38
|
+
* keystroke there would put a prompt in the middle of the published first-value
|
|
39
|
+
* journey. What separates it from `occurrences redeliver` — which does deserve
|
|
40
|
+
* one — is that redelivery acts on something that already exists and sends it
|
|
41
|
+
* again. So the ground is an outward-facing change to an *addressed* object,
|
|
42
|
+
* which is exactly an operation with a path parameter.
|
|
43
|
+
*/
|
|
44
|
+
export function confirmationGround(operation) {
|
|
45
|
+
if (operation.mcp.kind === "tool" && operation.mcp.destructive) {
|
|
46
|
+
return "destructive";
|
|
47
|
+
}
|
|
48
|
+
// **Not conditioned on `mutates`, and a review round is why.** Writing it as
|
|
49
|
+
// "changes stored state *and* reaches outward" conflates two questions: an
|
|
50
|
+
// operation that pings an addressed receiver without persisting anything
|
|
51
|
+
// would have been declared `mutates: false` and skipped the confirmation
|
|
52
|
+
// requirement entirely — while having exactly the character the requirement
|
|
53
|
+
// exists for, a real outbound request made on behalf of an object somebody
|
|
54
|
+
// else may be relying on. No operation in the registry is that shape today,
|
|
55
|
+
// which is precisely why the rule had to be written for the one that will be.
|
|
56
|
+
if (operation.mcp.kind === "tool" &&
|
|
57
|
+
operation.mcp.openWorld &&
|
|
58
|
+
operation.path.includes("{")) {
|
|
59
|
+
return "outward-facing-change";
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The request headers the registry declares for an operation.
|
|
65
|
+
*
|
|
66
|
+
* Read off the schema's own shape rather than listed here, because the point of
|
|
67
|
+
* the rules below is that a header appearing or disappearing changes what the
|
|
68
|
+
* command line must offer. `endpoints.disable` is the case that makes it worth
|
|
69
|
+
* checking: every other endpoint write accepts `If-Match` and that one does
|
|
70
|
+
* not, so a command offering the flag there would document a guard the service
|
|
71
|
+
* ignores — which reads to somebody operating carefully as protection they do
|
|
72
|
+
* not have.
|
|
73
|
+
*/
|
|
74
|
+
export function declaredRequestHeaders(operation) {
|
|
75
|
+
const schema = operation.requestHeaders;
|
|
76
|
+
return schema?.shape === undefined ? [] : Object.keys(schema.shape);
|
|
77
|
+
}
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
/* Shared option declarations */
|
|
80
|
+
/* -------------------------------------------------------------------------- */
|
|
81
|
+
const API_URL = {
|
|
82
|
+
flag: "--api-url",
|
|
83
|
+
summary: "The service to talk to, when it is not the selected one.",
|
|
84
|
+
value: "<origin>",
|
|
85
|
+
};
|
|
86
|
+
const PAGE_OPTIONS = [
|
|
87
|
+
{
|
|
88
|
+
flag: "--limit",
|
|
89
|
+
summary: "How many rows to ask for in one page.",
|
|
90
|
+
value: "<count>",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
flag: "--cursor",
|
|
94
|
+
summary: "Continue from the cursor a previous page returned.",
|
|
95
|
+
value: "<cursor>",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
flag: "--all",
|
|
99
|
+
summary: "Follow cursors until the collection ends, up to a published ceiling.",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
const CONFIRM = {
|
|
103
|
+
flag: "--yes",
|
|
104
|
+
summary: "Confirm without being asked. Required when there is no terminal to ask on.",
|
|
105
|
+
};
|
|
106
|
+
const IF_MATCH = {
|
|
107
|
+
flag: "--if-match",
|
|
108
|
+
summary: "Refuse unless it is still at this version. Read it first to get the ETag.",
|
|
109
|
+
value: "<etag>",
|
|
110
|
+
};
|
|
111
|
+
const IDEMPOTENCY = {
|
|
112
|
+
flag: "--idempotency-key",
|
|
113
|
+
summary: "Make a repeat of this exact request a replay rather than a second.",
|
|
114
|
+
value: "<key>",
|
|
115
|
+
};
|
|
116
|
+
/* -------------------------------------------------------------------------- */
|
|
117
|
+
/* The catalog */
|
|
118
|
+
/* -------------------------------------------------------------------------- */
|
|
119
|
+
/**
|
|
120
|
+
* Every command, in the order the reference and `--help` present them.
|
|
121
|
+
*
|
|
122
|
+
* Ordered by what somebody does rather than alphabetically: sign in, look
|
|
123
|
+
* around, set up a destination, schedule something, watch it, then recover from
|
|
124
|
+
* it. The groups are the headings.
|
|
125
|
+
*/
|
|
126
|
+
export const COMMAND_CATALOG = [
|
|
127
|
+
/* --- Session ---------------------------------------------------------- */
|
|
128
|
+
{
|
|
129
|
+
group: "Session",
|
|
130
|
+
operations: ["context.read"],
|
|
131
|
+
options: [
|
|
132
|
+
API_URL,
|
|
133
|
+
{
|
|
134
|
+
flag: "--api-key",
|
|
135
|
+
summary: "Sign in with a scoped API credential instead of a browser. Takes no value: paste it when asked, or pipe it in.",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
flag: "--no-browser",
|
|
139
|
+
summary: "Print the authorization address instead of opening it.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
// **A way to ask for less than the whole command line can do.** The
|
|
143
|
+
// request is the union of every covered operation's scopes, which is
|
|
144
|
+
// honest — this command really can rotate a credential — but somebody
|
|
145
|
+
// whose only use is scheduling should not have to grant the rest to get
|
|
146
|
+
// there. Narrowing is safe in the direction that matters: a command
|
|
147
|
+
// outside the grant is refused by the service.
|
|
148
|
+
flag: "--scope",
|
|
149
|
+
summary: "Ask for these permissions instead of everything the commands can use, separated by spaces in one argument.",
|
|
150
|
+
value: "<scopes>",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
positionals: [],
|
|
154
|
+
summary: "Sign in to a Greatwait service and remember the credential.",
|
|
155
|
+
words: "login",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
group: "Session",
|
|
159
|
+
operations: [],
|
|
160
|
+
options: [API_URL],
|
|
161
|
+
positionals: [],
|
|
162
|
+
summary: "Revoke the grant where possible and remove the local material.",
|
|
163
|
+
words: "logout",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
group: "Session",
|
|
167
|
+
operations: ["context.read"],
|
|
168
|
+
options: [API_URL],
|
|
169
|
+
positionals: [],
|
|
170
|
+
summary: "Report which service, project and environment this acts in.",
|
|
171
|
+
words: "whoami",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
group: "Session",
|
|
175
|
+
operations: ["meta.version", "context.read"],
|
|
176
|
+
options: [API_URL],
|
|
177
|
+
positionals: [],
|
|
178
|
+
summary: "Check the service, the stored configuration and the credential.",
|
|
179
|
+
words: "doctor",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
group: "Session",
|
|
183
|
+
operations: [],
|
|
184
|
+
options: [API_URL],
|
|
185
|
+
positionals: [
|
|
186
|
+
{
|
|
187
|
+
name: "[list | use <origin>]",
|
|
188
|
+
optional: true,
|
|
189
|
+
summary: "Show the signed-in services, or select one.",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
summary: "Show or choose the service later commands go to.",
|
|
193
|
+
words: "context",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
group: "Session",
|
|
197
|
+
operations: [],
|
|
198
|
+
options: [API_URL],
|
|
199
|
+
positionals: [],
|
|
200
|
+
summary: "Print the published first-value sequence and its commands.",
|
|
201
|
+
words: "journey",
|
|
202
|
+
},
|
|
203
|
+
/* --- Destinations ----------------------------------------------------- */
|
|
204
|
+
{
|
|
205
|
+
group: "Destinations",
|
|
206
|
+
operations: ["endpoints.list"],
|
|
207
|
+
options: [API_URL, ...PAGE_OPTIONS],
|
|
208
|
+
positionals: [],
|
|
209
|
+
summary: "List the destinations this project delivers to.",
|
|
210
|
+
words: "destinations list",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
group: "Destinations",
|
|
214
|
+
operations: ["endpoints.read"],
|
|
215
|
+
options: [API_URL],
|
|
216
|
+
positionals: [
|
|
217
|
+
{ name: "<destination>", summary: "The destination to read." },
|
|
218
|
+
],
|
|
219
|
+
summary: "Read one destination, its status and its signing keys.",
|
|
220
|
+
words: "destinations read",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
group: "Destinations",
|
|
224
|
+
operations: ["endpoints.create"],
|
|
225
|
+
options: [
|
|
226
|
+
API_URL,
|
|
227
|
+
{
|
|
228
|
+
flag: "--url",
|
|
229
|
+
summary: "The https receiver Greatwait will deliver to.",
|
|
230
|
+
value: "<https url>",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
positionals: [],
|
|
234
|
+
summary: "Create a destination and print its signing key once.",
|
|
235
|
+
words: "destinations create",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
confirms: true,
|
|
239
|
+
group: "Destinations",
|
|
240
|
+
operations: ["endpoints.update"],
|
|
241
|
+
options: [
|
|
242
|
+
API_URL,
|
|
243
|
+
{
|
|
244
|
+
flag: "--url",
|
|
245
|
+
summary: "The new https receiver.",
|
|
246
|
+
value: "<https url>",
|
|
247
|
+
},
|
|
248
|
+
IF_MATCH,
|
|
249
|
+
CONFIRM,
|
|
250
|
+
],
|
|
251
|
+
positionals: [
|
|
252
|
+
{ name: "<destination>", summary: "The destination to change." },
|
|
253
|
+
],
|
|
254
|
+
summary: "Point a destination at a different receiver.",
|
|
255
|
+
words: "destinations update",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
// **Confirmed although the registry has no hint that says so.** Rotation is
|
|
259
|
+
// withheld from MCP precisely because it returns a one-time secret, so
|
|
260
|
+
// there is no `destructive` hint to derive from; a call whose answer is
|
|
261
|
+
// lost has rotated a key nobody holds, which is the harm the prompt exists
|
|
262
|
+
// for. The rule below permits a command to be more careful than the
|
|
263
|
+
// registry demands, and this is the one that is.
|
|
264
|
+
confirms: true,
|
|
265
|
+
group: "Destinations",
|
|
266
|
+
operations: ["endpoints.rotateSigningKey"],
|
|
267
|
+
options: [API_URL, IF_MATCH, CONFIRM],
|
|
268
|
+
positionals: [
|
|
269
|
+
{ name: "<destination>", summary: "The destination to rotate." },
|
|
270
|
+
],
|
|
271
|
+
summary: "Mint the next signing key, shown once, with a bounded overlap.",
|
|
272
|
+
words: "destinations rotate-key",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
confirms: true,
|
|
276
|
+
group: "Destinations",
|
|
277
|
+
// **No `--if-match`, because the registry declares none for it.** Every
|
|
278
|
+
// other endpoint write accepts a conditional header and this one does not,
|
|
279
|
+
// so offering the flag here would document a guard the service ignores.
|
|
280
|
+
operations: ["endpoints.disable"],
|
|
281
|
+
options: [API_URL, CONFIRM],
|
|
282
|
+
positionals: [
|
|
283
|
+
{ name: "<destination>", summary: "The destination to disable." },
|
|
284
|
+
],
|
|
285
|
+
summary: "Stop delivering to a destination, keeping its history readable.",
|
|
286
|
+
words: "destinations disable",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
group: "Destinations",
|
|
290
|
+
// No confirmation: enabling gives something back rather than taking it
|
|
291
|
+
// away, and a person who disabled the wrong destination wants it back in
|
|
292
|
+
// one line, not after a question.
|
|
293
|
+
operations: ["endpoints.enable"],
|
|
294
|
+
options: [API_URL],
|
|
295
|
+
positionals: [
|
|
296
|
+
{ name: "<destination>", summary: "The destination to turn back on." },
|
|
297
|
+
],
|
|
298
|
+
summary: "Resume delivering to a destination that was disabled.",
|
|
299
|
+
words: "destinations enable",
|
|
300
|
+
},
|
|
301
|
+
/* --- Schedules -------------------------------------------------------- */
|
|
302
|
+
{
|
|
303
|
+
group: "Schedules",
|
|
304
|
+
operations: ["schedules.list"],
|
|
305
|
+
options: [
|
|
306
|
+
API_URL,
|
|
307
|
+
{
|
|
308
|
+
flag: "--endpoint",
|
|
309
|
+
summary: "Only the schedules that deliver to this destination — everything pointed at it, in one listing.",
|
|
310
|
+
value: "<destination>",
|
|
311
|
+
},
|
|
312
|
+
...PAGE_OPTIONS,
|
|
313
|
+
],
|
|
314
|
+
positionals: [],
|
|
315
|
+
summary: "List this project's schedules, or those delivering to one destination.",
|
|
316
|
+
words: "schedules list",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
group: "Schedules",
|
|
320
|
+
operations: ["schedules.read"],
|
|
321
|
+
options: [API_URL],
|
|
322
|
+
positionals: [{ name: "<schedule>", summary: "The schedule to read." }],
|
|
323
|
+
summary: "Read a schedule, its recurrence, its version and its ETag.",
|
|
324
|
+
words: "schedules read",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
group: "Schedules",
|
|
328
|
+
operations: ["schedules.create"],
|
|
329
|
+
options: [
|
|
330
|
+
API_URL,
|
|
331
|
+
{
|
|
332
|
+
flag: "--endpoint",
|
|
333
|
+
summary: "The destination to deliver to.",
|
|
334
|
+
value: "<destination>",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
flag: "--in",
|
|
338
|
+
summary: "Once, after a delay: 30s, 5m, 2h, 1d.",
|
|
339
|
+
value: "<duration>",
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
flag: "--at",
|
|
343
|
+
summary: "Once, at an instant in UTC.",
|
|
344
|
+
value: "<instant>",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
flag: "--every",
|
|
348
|
+
summary: "Repeatedly, at this interval: 30s, 5m, 2h, 1d.",
|
|
349
|
+
value: "<duration>",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
flag: "--cron",
|
|
353
|
+
summary: "Repeatedly, on a cron expression.",
|
|
354
|
+
value: "<expression>",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
flag: "--time-zone",
|
|
358
|
+
summary: "The zone a cron expression is read in, so daylight saving moves the local time rather than the clock time. IANA names, such as Europe/London.",
|
|
359
|
+
value: "<zone>",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
flag: "--partition-key",
|
|
363
|
+
summary: "The fairness partition this belongs to.",
|
|
364
|
+
value: "<key>",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
flag: "--latency-class",
|
|
368
|
+
summary: "How promptly it must fire: realtime or bulk.",
|
|
369
|
+
value: "<class>",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
flag: "--data",
|
|
373
|
+
summary: "The payload: JSON, @file.json, or - for standard input.",
|
|
374
|
+
value: "<json>",
|
|
375
|
+
},
|
|
376
|
+
IDEMPOTENCY,
|
|
377
|
+
],
|
|
378
|
+
positionals: [],
|
|
379
|
+
summary: "Schedule a delivery, once or repeatedly, and print the first occurrence.",
|
|
380
|
+
words: "schedules create",
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
confirms: true,
|
|
384
|
+
group: "Schedules",
|
|
385
|
+
operations: ["schedules.replace"],
|
|
386
|
+
options: [
|
|
387
|
+
API_URL,
|
|
388
|
+
{
|
|
389
|
+
flag: "--endpoint",
|
|
390
|
+
summary: "The destination to deliver to.",
|
|
391
|
+
value: "<destination>",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
flag: "--in",
|
|
395
|
+
summary: "Once, after a delay: 30s, 5m, 2h, 1d.",
|
|
396
|
+
value: "<duration>",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
flag: "--at",
|
|
400
|
+
summary: "Once, at an instant in UTC.",
|
|
401
|
+
value: "<instant>",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
flag: "--every",
|
|
405
|
+
summary: "Repeatedly, at this interval: 30s, 5m, 2h, 1d.",
|
|
406
|
+
value: "<duration>",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
flag: "--cron",
|
|
410
|
+
summary: "Repeatedly, on a cron expression.",
|
|
411
|
+
value: "<expression>",
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
flag: "--time-zone",
|
|
415
|
+
summary: "The zone a cron expression is read in, so daylight saving moves the local time rather than the clock time. IANA names, such as Europe/London.",
|
|
416
|
+
value: "<zone>",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
flag: "--partition-key",
|
|
420
|
+
summary: "The fairness partition this belongs to.",
|
|
421
|
+
value: "<key>",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
flag: "--latency-class",
|
|
425
|
+
summary: "How promptly it must fire: realtime or bulk.",
|
|
426
|
+
value: "<class>",
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
flag: "--data",
|
|
430
|
+
summary: "The payload: JSON, @file.json, or - for standard input.",
|
|
431
|
+
value: "<json>",
|
|
432
|
+
},
|
|
433
|
+
IF_MATCH,
|
|
434
|
+
CONFIRM,
|
|
435
|
+
],
|
|
436
|
+
positionals: [{ name: "<schedule>", summary: "The schedule to replace." }],
|
|
437
|
+
summary: "Replace a schedule's definition, discarding what it said before.",
|
|
438
|
+
words: "schedules replace",
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
group: "Schedules",
|
|
442
|
+
operations: ["schedules.pause"],
|
|
443
|
+
options: [API_URL, IF_MATCH],
|
|
444
|
+
positionals: [{ name: "<schedule>", summary: "The schedule to pause." }],
|
|
445
|
+
summary: "Stop firing a schedule without discarding it.",
|
|
446
|
+
words: "schedules pause",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
group: "Schedules",
|
|
450
|
+
operations: ["schedules.resume"],
|
|
451
|
+
options: [API_URL, IF_MATCH],
|
|
452
|
+
positionals: [{ name: "<schedule>", summary: "The schedule to resume." }],
|
|
453
|
+
summary: "Start a paused schedule firing again.",
|
|
454
|
+
words: "schedules resume",
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
confirms: true,
|
|
458
|
+
group: "Schedules",
|
|
459
|
+
operations: ["schedules.cancel"],
|
|
460
|
+
options: [API_URL, IF_MATCH, CONFIRM],
|
|
461
|
+
positionals: [{ name: "<schedule>", summary: "The schedule to cancel." }],
|
|
462
|
+
summary: "Cancel a schedule and everything it had still to deliver.",
|
|
463
|
+
words: "schedules cancel",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
group: "Schedules",
|
|
467
|
+
operations: ["schedules.archive"],
|
|
468
|
+
options: [API_URL, IF_MATCH],
|
|
469
|
+
positionals: [{ name: "<schedule>", summary: "The schedule to archive." }],
|
|
470
|
+
summary: "Move a settled schedule out of the working list.",
|
|
471
|
+
words: "schedules archive",
|
|
472
|
+
},
|
|
473
|
+
/* --- Data exports ---------------------------------------------------- */
|
|
474
|
+
{
|
|
475
|
+
group: "Data exports",
|
|
476
|
+
operations: ["exports.create"],
|
|
477
|
+
options: [API_URL],
|
|
478
|
+
positionals: [],
|
|
479
|
+
summary: "Start a bounded export for this project and environment.",
|
|
480
|
+
words: "exports create",
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
group: "Data exports",
|
|
484
|
+
operations: ["exports.read"],
|
|
485
|
+
options: [API_URL],
|
|
486
|
+
positionals: [{ name: "<export>", summary: "The export job to read." }],
|
|
487
|
+
summary: "Read an export job's progress and cutoff.",
|
|
488
|
+
words: "exports read",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
group: "Data exports",
|
|
492
|
+
operations: ["exports.part"],
|
|
493
|
+
options: [API_URL, PAGE_OPTIONS[0]],
|
|
494
|
+
positionals: [{ name: "<export>", summary: "The export job to download." }],
|
|
495
|
+
summary: "Print one bounded NDJSON export part for safe redirection to a file.",
|
|
496
|
+
words: "exports part",
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
confirms: true,
|
|
500
|
+
group: "Data exports",
|
|
501
|
+
operations: ["exports.cancel"],
|
|
502
|
+
options: [API_URL, CONFIRM],
|
|
503
|
+
positionals: [{ name: "<export>", summary: "The export job to cancel." }],
|
|
504
|
+
summary: "Permanently cancel an export job.",
|
|
505
|
+
words: "exports cancel",
|
|
506
|
+
},
|
|
507
|
+
/* --- Occurrences and attempts ----------------------------------------- */
|
|
508
|
+
{
|
|
509
|
+
group: "Occurrences and attempts",
|
|
510
|
+
operations: ["occurrences.list"],
|
|
511
|
+
options: [
|
|
512
|
+
API_URL,
|
|
513
|
+
...PAGE_OPTIONS,
|
|
514
|
+
{
|
|
515
|
+
flag: "--newest",
|
|
516
|
+
summary: "Newest first, which is what a person looking at a schedule wants. A cursor belongs to the order it was issued under.",
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
positionals: [{ name: "<schedule>", summary: "The schedule to list." }],
|
|
520
|
+
summary: "List the retained occurrences of a schedule.",
|
|
521
|
+
words: "occurrences list",
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
group: "Occurrences and attempts",
|
|
525
|
+
operations: ["occurrences.read"],
|
|
526
|
+
options: [API_URL],
|
|
527
|
+
positionals: [
|
|
528
|
+
{ name: "<schedule>", summary: "The schedule it belongs to." },
|
|
529
|
+
{ name: "<occurrence>", summary: "The occurrence to read." },
|
|
530
|
+
],
|
|
531
|
+
summary: "Read one occurrence and the state it is in.",
|
|
532
|
+
words: "occurrences read",
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
group: "Occurrences and attempts",
|
|
536
|
+
operations: ["occurrences.read"],
|
|
537
|
+
options: [
|
|
538
|
+
API_URL,
|
|
539
|
+
{
|
|
540
|
+
flag: "--timeout",
|
|
541
|
+
summary: "Give up after this long instead of the published deadline.",
|
|
542
|
+
value: "<duration>",
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
positionals: [
|
|
546
|
+
{ name: "<schedule>", summary: "The schedule it belongs to." },
|
|
547
|
+
{ name: "<occurrence>", summary: "The occurrence to wait for." },
|
|
548
|
+
],
|
|
549
|
+
summary: "Watch an occurrence until it settles, or until the deadline.",
|
|
550
|
+
words: "occurrences wait",
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
confirms: true,
|
|
554
|
+
group: "Occurrences and attempts",
|
|
555
|
+
operations: ["occurrences.redeliver"],
|
|
556
|
+
options: [API_URL, IDEMPOTENCY, CONFIRM],
|
|
557
|
+
positionals: [
|
|
558
|
+
{ name: "<schedule>", summary: "The schedule it belongs to." },
|
|
559
|
+
{ name: "<occurrence>", summary: "The occurrence to send again." },
|
|
560
|
+
],
|
|
561
|
+
summary: "Deliver an occurrence again to the destination it belongs to.",
|
|
562
|
+
words: "occurrences redeliver",
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
group: "Occurrences and attempts",
|
|
566
|
+
operations: ["attempts.list"],
|
|
567
|
+
options: [API_URL, ...PAGE_OPTIONS],
|
|
568
|
+
positionals: [
|
|
569
|
+
{ name: "<schedule>", summary: "The schedule it belongs to." },
|
|
570
|
+
{ name: "<occurrence>", summary: "The occurrence to list attempts for." },
|
|
571
|
+
],
|
|
572
|
+
summary: "List every delivery attempt, with its diagnosis.",
|
|
573
|
+
words: "attempts list",
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
group: "Occurrences and attempts",
|
|
577
|
+
operations: ["attempts.read"],
|
|
578
|
+
options: [API_URL],
|
|
579
|
+
positionals: [
|
|
580
|
+
{ name: "<schedule>", summary: "The schedule it belongs to." },
|
|
581
|
+
{ name: "<occurrence>", summary: "The occurrence it belongs to." },
|
|
582
|
+
{ name: "<attempt>", summary: "The attempt to read." },
|
|
583
|
+
],
|
|
584
|
+
summary: "Read one attempt in full, including what the receiver answered.",
|
|
585
|
+
words: "attempts read",
|
|
586
|
+
},
|
|
587
|
+
/* --- Planning --------------------------------------------------------- */
|
|
588
|
+
{
|
|
589
|
+
group: "Planning",
|
|
590
|
+
operations: ["forecast.read"],
|
|
591
|
+
options: [
|
|
592
|
+
API_URL,
|
|
593
|
+
{
|
|
594
|
+
flag: "--from",
|
|
595
|
+
summary: "The start of the window, in UTC.",
|
|
596
|
+
value: "<instant>",
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
flag: "--to",
|
|
600
|
+
summary: "The end of the window, in UTC.",
|
|
601
|
+
value: "<instant>",
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
flag: "--bucket",
|
|
605
|
+
summary: "Bucket width in seconds.",
|
|
606
|
+
value: "<seconds>",
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
flag: "--breakdown",
|
|
610
|
+
summary: "Group the counts by this dimension.",
|
|
611
|
+
value: "<dimension>",
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
positionals: [],
|
|
615
|
+
summary: "Show what is due in a window, and how fresh the answer is.",
|
|
616
|
+
words: "forecast",
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
group: "Planning",
|
|
620
|
+
operations: ["usage.read"],
|
|
621
|
+
options: [
|
|
622
|
+
API_URL,
|
|
623
|
+
{ flag: "--from", summary: "The first day, in UTC.", value: "<instant>" },
|
|
624
|
+
{ flag: "--to", summary: "The last day, in UTC.", value: "<instant>" },
|
|
625
|
+
],
|
|
626
|
+
positionals: [],
|
|
627
|
+
summary: "Show terminal occurrence volume by day.",
|
|
628
|
+
words: "usage",
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
group: "Planning",
|
|
632
|
+
operations: ["billing.summary"],
|
|
633
|
+
options: [API_URL],
|
|
634
|
+
positionals: [],
|
|
635
|
+
summary: "Show exact execution credit and effective billing state.",
|
|
636
|
+
words: "billing",
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
group: "Planning",
|
|
640
|
+
operations: ["billing.funding"],
|
|
641
|
+
options: [API_URL],
|
|
642
|
+
positionals: [],
|
|
643
|
+
summary: "Show execution funding, automatic payment authority and payment outcomes.",
|
|
644
|
+
words: "billing funding",
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
group: "Planning",
|
|
648
|
+
operations: ["billing.notices"],
|
|
649
|
+
options: [API_URL, ...PAGE_OPTIONS],
|
|
650
|
+
positionals: [],
|
|
651
|
+
summary: "List persistent organization billing notices and delivery state.",
|
|
652
|
+
words: "billing notices",
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
group: "Planning",
|
|
656
|
+
operations: ["billing.notificationsettings"],
|
|
657
|
+
options: [API_URL],
|
|
658
|
+
positionals: [],
|
|
659
|
+
summary: "Show billing notification policy and its revision.",
|
|
660
|
+
words: "billing notification-settings",
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
confirms: true,
|
|
664
|
+
group: "Planning",
|
|
665
|
+
operations: ["billing.notificationsettingsupdate"],
|
|
666
|
+
options: [
|
|
667
|
+
API_URL,
|
|
668
|
+
{
|
|
669
|
+
flag: "--low-credit",
|
|
670
|
+
summary: "Send the low-credit notice below this exact USD amount.",
|
|
671
|
+
value: "<dollars>",
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
flag: "--noncritical-email",
|
|
675
|
+
summary: "Enable or disable email for noncritical notices.",
|
|
676
|
+
value: "<on|off>",
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
flag: "--expected-revision",
|
|
680
|
+
summary: "Refuse if notification settings have changed.",
|
|
681
|
+
value: "<revision>",
|
|
682
|
+
},
|
|
683
|
+
IDEMPOTENCY,
|
|
684
|
+
CONFIRM,
|
|
685
|
+
],
|
|
686
|
+
positionals: [],
|
|
687
|
+
summary: "Update optional billing notifications with approval.",
|
|
688
|
+
words: "billing update-notifications",
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
group: "Planning",
|
|
692
|
+
operations: ["billing.fundingquote"],
|
|
693
|
+
options: [
|
|
694
|
+
API_URL,
|
|
695
|
+
{
|
|
696
|
+
flag: "--credit",
|
|
697
|
+
summary: "Execution credit to buy in USD, at least 10.",
|
|
698
|
+
value: "<dollars>",
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
positionals: [],
|
|
702
|
+
summary: "Preview manual execution funding before tax is calculated at checkout.",
|
|
703
|
+
words: "billing quote-funding",
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
confirms: true,
|
|
707
|
+
group: "Planning",
|
|
708
|
+
operations: ["billing.purchase"],
|
|
709
|
+
options: [
|
|
710
|
+
API_URL,
|
|
711
|
+
{
|
|
712
|
+
flag: "--quote",
|
|
713
|
+
summary: "The exact object returned by quote-funding (the data field in JSON output).",
|
|
714
|
+
value: "<json|@file|->",
|
|
715
|
+
},
|
|
716
|
+
IDEMPOTENCY,
|
|
717
|
+
CONFIRM,
|
|
718
|
+
],
|
|
719
|
+
positionals: [],
|
|
720
|
+
summary: "Confirm a manual execution-credit purchase with a stable retry key.",
|
|
721
|
+
words: "billing purchase",
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
confirms: true,
|
|
725
|
+
group: "Planning",
|
|
726
|
+
operations: ["billing.ceiling"],
|
|
727
|
+
options: [
|
|
728
|
+
API_URL,
|
|
729
|
+
{
|
|
730
|
+
flag: "--ceiling",
|
|
731
|
+
summary: "Set the UTC-month automatic payment ceiling in USD, or turn it off.",
|
|
732
|
+
value: "<dollars|off>",
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
flag: "--expected-revision",
|
|
736
|
+
summary: "Refuse if automatic payment authority has changed.",
|
|
737
|
+
value: "<revision>",
|
|
738
|
+
},
|
|
739
|
+
IDEMPOTENCY,
|
|
740
|
+
CONFIRM,
|
|
741
|
+
],
|
|
742
|
+
positionals: [],
|
|
743
|
+
summary: "Set exact automatic payment authority with approval.",
|
|
744
|
+
words: "billing ceiling",
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
group: "Planning",
|
|
748
|
+
operations: ["billing.autotopuppreview"],
|
|
749
|
+
options: [
|
|
750
|
+
API_URL,
|
|
751
|
+
{
|
|
752
|
+
flag: "--action",
|
|
753
|
+
summary: "Preview enabling, editing, disabling or resuming the rule.",
|
|
754
|
+
value: "<enable|edit|disable|resume>",
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
flag: "--credit",
|
|
758
|
+
summary: "Fixed execution credit purchased by enable or edit, in USD.",
|
|
759
|
+
value: "<dollars>",
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
flag: "--trigger-below",
|
|
763
|
+
summary: "Trigger when usable execution credit is below this USD amount.",
|
|
764
|
+
value: "<dollars>",
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
flag: "--ceiling",
|
|
768
|
+
summary: "Gross automatic-payment ceiling per UTC month, in USD.",
|
|
769
|
+
value: "<dollars>",
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
flag: "--expected-revision",
|
|
773
|
+
summary: "Refuse if automatic payment authority has changed.",
|
|
774
|
+
value: "<revision>",
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
positionals: [],
|
|
778
|
+
summary: "Preview one exact automatic top-up rule without enabling it.",
|
|
779
|
+
words: "billing preview-topup",
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
confirms: true,
|
|
783
|
+
group: "Planning",
|
|
784
|
+
operations: ["billing.autotopup"],
|
|
785
|
+
options: [
|
|
786
|
+
API_URL,
|
|
787
|
+
{
|
|
788
|
+
flag: "--preview",
|
|
789
|
+
summary: "The exact object returned by preview-topup (the data field in JSON output).",
|
|
790
|
+
value: "<json|@file|->",
|
|
791
|
+
},
|
|
792
|
+
IDEMPOTENCY,
|
|
793
|
+
CONFIRM,
|
|
794
|
+
],
|
|
795
|
+
positionals: [],
|
|
796
|
+
summary: "Confirm an automatic top-up preview with a stable retry key.",
|
|
797
|
+
words: "billing apply-topup",
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
confirms: true,
|
|
801
|
+
group: "Planning",
|
|
802
|
+
operations: ["billing.refundrequest"],
|
|
803
|
+
options: [
|
|
804
|
+
API_URL,
|
|
805
|
+
{
|
|
806
|
+
flag: "--payment-operation",
|
|
807
|
+
summary: "The paid funding operation to review.",
|
|
808
|
+
value: "<operation>",
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
flag: "--amount",
|
|
812
|
+
summary: "Unused purchased credit requested in USD.",
|
|
813
|
+
value: "<dollars>",
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
flag: "--reason",
|
|
817
|
+
summary: "Why an operator should review the request.",
|
|
818
|
+
value: "<text>",
|
|
819
|
+
},
|
|
820
|
+
IDEMPOTENCY,
|
|
821
|
+
CONFIRM,
|
|
822
|
+
],
|
|
823
|
+
positionals: [],
|
|
824
|
+
summary: "Record a request for operator-reviewed unused-credit refund.",
|
|
825
|
+
words: "billing refund-request",
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
confirms: true,
|
|
829
|
+
group: "Planning",
|
|
830
|
+
operations: ["billing.refundresolution"],
|
|
831
|
+
options: [
|
|
832
|
+
API_URL,
|
|
833
|
+
{
|
|
834
|
+
flag: "--refund-request",
|
|
835
|
+
summary: "The pending refund request an external operator reviewed.",
|
|
836
|
+
value: "<request>",
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
flag: "--decision",
|
|
840
|
+
summary: "Record the external review outcome.",
|
|
841
|
+
value: "<completed|declined>",
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
flag: "--provider-refund",
|
|
845
|
+
summary: "Existing Stripe refund ID required for completion.",
|
|
846
|
+
value: "<re_...>",
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
flag: "--gross",
|
|
850
|
+
summary: "Existing provider refund gross amount in USD.",
|
|
851
|
+
value: "<dollars>",
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
flag: "--tax",
|
|
855
|
+
summary: "Tax included in the existing provider refund in USD.",
|
|
856
|
+
value: "<dollars>",
|
|
857
|
+
},
|
|
858
|
+
IDEMPOTENCY,
|
|
859
|
+
CONFIRM,
|
|
860
|
+
],
|
|
861
|
+
positionals: [],
|
|
862
|
+
summary: "Record a completed or declined external refund review.",
|
|
863
|
+
words: "billing resolve-refund",
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
confirms: true,
|
|
867
|
+
group: "Planning",
|
|
868
|
+
operations: ["billing.budgets"],
|
|
869
|
+
options: [
|
|
870
|
+
API_URL,
|
|
871
|
+
{
|
|
872
|
+
flag: "--advisory-budget",
|
|
873
|
+
summary: "Set the monthly advisory amount in USD, or turn it off.",
|
|
874
|
+
value: "<dollars|off>",
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
flag: "--hard-cap",
|
|
878
|
+
summary: "Set the monthly hard cap in USD, or turn it off.",
|
|
879
|
+
value: "<dollars|off>",
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
flag: "--expected-revision",
|
|
883
|
+
summary: "Refuse if the budget policy has changed since this revision.",
|
|
884
|
+
value: "<revision>",
|
|
885
|
+
},
|
|
886
|
+
IDEMPOTENCY,
|
|
887
|
+
CONFIRM,
|
|
888
|
+
],
|
|
889
|
+
positionals: [],
|
|
890
|
+
summary: "Set exact monthly execution budgets with approval.",
|
|
891
|
+
words: "billing budgets",
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
group: "Planning",
|
|
895
|
+
operations: ["billing.projects"],
|
|
896
|
+
options: [API_URL],
|
|
897
|
+
positionals: [],
|
|
898
|
+
summary: "Show named project access, payment recovery and monthly fees.",
|
|
899
|
+
words: "billing projects",
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
group: "Planning",
|
|
903
|
+
operations: ["billing.quote"],
|
|
904
|
+
options: [
|
|
905
|
+
API_URL,
|
|
906
|
+
{
|
|
907
|
+
flag: "--action",
|
|
908
|
+
summary: "Preview an addition, cancellation or included transfer.",
|
|
909
|
+
value: "<add|cancel|transfer-included>",
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
flag: "--target-project",
|
|
913
|
+
summary: "The named project whose billing changes.",
|
|
914
|
+
value: "<project>",
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
flag: "--from-project",
|
|
918
|
+
summary: "The former included project for a transfer.",
|
|
919
|
+
value: "<project>",
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
positionals: [],
|
|
923
|
+
summary: "Preview a named project billing change without authorizing payment.",
|
|
924
|
+
words: "billing quote-project",
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
confirms: true,
|
|
928
|
+
group: "Planning",
|
|
929
|
+
operations: ["billing.change"],
|
|
930
|
+
options: [
|
|
931
|
+
API_URL,
|
|
932
|
+
{
|
|
933
|
+
flag: "--quote",
|
|
934
|
+
summary: "The exact quote object returned by quote-project (the data field in JSON output).",
|
|
935
|
+
value: "<json|@file|->",
|
|
936
|
+
},
|
|
937
|
+
IDEMPOTENCY,
|
|
938
|
+
CONFIRM,
|
|
939
|
+
],
|
|
940
|
+
positionals: [],
|
|
941
|
+
summary: "Confirm an exact project billing quote with a stable retry key.",
|
|
942
|
+
words: "billing change-project",
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
confirms: true,
|
|
946
|
+
group: "Planning",
|
|
947
|
+
operations: ["billing.portal"],
|
|
948
|
+
options: [API_URL, CONFIRM],
|
|
949
|
+
positionals: [],
|
|
950
|
+
summary: "Create a hosted payment-method and invoice recovery link.",
|
|
951
|
+
words: "billing portal",
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
group: "Planning",
|
|
955
|
+
operations: ["billing.ledger"],
|
|
956
|
+
options: [API_URL, ...PAGE_OPTIONS],
|
|
957
|
+
positionals: [],
|
|
958
|
+
summary: "List immutable execution-credit ledger entries.",
|
|
959
|
+
words: "billing ledger",
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
group: "Planning",
|
|
963
|
+
operations: ["timing.summary"],
|
|
964
|
+
options: [API_URL],
|
|
965
|
+
positionals: [],
|
|
966
|
+
summary: "Show the rolling 24-hour delivery timing cohort.",
|
|
967
|
+
words: "timing",
|
|
968
|
+
},
|
|
969
|
+
/* --- Credentials ------------------------------------------------------ */
|
|
970
|
+
{
|
|
971
|
+
group: "Credentials",
|
|
972
|
+
operations: ["credentials.list"],
|
|
973
|
+
options: [API_URL, ...PAGE_OPTIONS],
|
|
974
|
+
positionals: [],
|
|
975
|
+
summary: "List this project's API credentials, by prefix, never in full.",
|
|
976
|
+
words: "credentials list",
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
confirms: true,
|
|
980
|
+
group: "Credentials",
|
|
981
|
+
operations: ["credentials.rotate"],
|
|
982
|
+
options: [API_URL, CONFIRM],
|
|
983
|
+
positionals: [
|
|
984
|
+
{ name: "<credential>", summary: "The credential to rotate." },
|
|
985
|
+
],
|
|
986
|
+
summary: "Mint the replacement for a credential, shown once.",
|
|
987
|
+
words: "credentials rotate",
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
confirms: true,
|
|
991
|
+
group: "Credentials",
|
|
992
|
+
operations: ["credentials.revoke"],
|
|
993
|
+
options: [API_URL, CONFIRM],
|
|
994
|
+
positionals: [
|
|
995
|
+
{ name: "<credential>", summary: "The credential to revoke." },
|
|
996
|
+
],
|
|
997
|
+
summary: "Stop a credential working, everywhere, immediately.",
|
|
998
|
+
words: "credentials revoke",
|
|
999
|
+
},
|
|
1000
|
+
/* --- Test sinks ------------------------------------------------------- */
|
|
1001
|
+
{
|
|
1002
|
+
group: "Test sinks",
|
|
1003
|
+
operations: ["testsinks.create"],
|
|
1004
|
+
options: [
|
|
1005
|
+
API_URL,
|
|
1006
|
+
{
|
|
1007
|
+
flag: "--endpoint",
|
|
1008
|
+
summary: "The destination whose deliveries this sink stands in for.",
|
|
1009
|
+
value: "<destination>",
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
flag: "--program",
|
|
1013
|
+
summary: "How it should answer from the start: JSON, @file.json, or - for standard input.",
|
|
1014
|
+
value: "<json>",
|
|
1015
|
+
},
|
|
1016
|
+
],
|
|
1017
|
+
positionals: [],
|
|
1018
|
+
summary: "Create a receiver Greatwait hosts, for a test environment only.",
|
|
1019
|
+
words: "sinks create",
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
group: "Test sinks",
|
|
1023
|
+
operations: ["testsinks.list"],
|
|
1024
|
+
options: [API_URL, ...PAGE_OPTIONS],
|
|
1025
|
+
positionals: [],
|
|
1026
|
+
summary: "List the test sinks in this project.",
|
|
1027
|
+
words: "sinks list",
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
group: "Test sinks",
|
|
1031
|
+
operations: ["testsinks.read"],
|
|
1032
|
+
options: [API_URL],
|
|
1033
|
+
positionals: [{ name: "<sink>", summary: "The sink to read." }],
|
|
1034
|
+
summary: "Read a test sink and the program it is running.",
|
|
1035
|
+
words: "sinks read",
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
group: "Test sinks",
|
|
1039
|
+
operations: ["testsinks.configure"],
|
|
1040
|
+
options: [
|
|
1041
|
+
API_URL,
|
|
1042
|
+
{
|
|
1043
|
+
flag: "--program",
|
|
1044
|
+
summary: "The behaviour program: JSON, @file.json, or - for standard input.",
|
|
1045
|
+
value: "<json>",
|
|
1046
|
+
},
|
|
1047
|
+
],
|
|
1048
|
+
positionals: [{ name: "<sink>", summary: "The sink to program." }],
|
|
1049
|
+
summary: "Tell a test sink how to answer, so a failure can be reproduced.",
|
|
1050
|
+
words: "sinks configure",
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
confirms: true,
|
|
1054
|
+
group: "Test sinks",
|
|
1055
|
+
operations: ["testsinks.delete"],
|
|
1056
|
+
options: [API_URL, CONFIRM],
|
|
1057
|
+
positionals: [{ name: "<sink>", summary: "The sink to delete." }],
|
|
1058
|
+
summary: "Delete a test sink and the deliveries recorded against it.",
|
|
1059
|
+
words: "sinks delete",
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
group: "Test sinks",
|
|
1063
|
+
operations: ["sinkdeliveries.list"],
|
|
1064
|
+
options: [API_URL, ...PAGE_OPTIONS],
|
|
1065
|
+
positionals: [{ name: "<sink>", summary: "The sink to list." }],
|
|
1066
|
+
summary: "List what a test sink received.",
|
|
1067
|
+
words: "sinks deliveries",
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
group: "Test sinks",
|
|
1071
|
+
operations: ["sinkdeliveries.read"],
|
|
1072
|
+
options: [API_URL],
|
|
1073
|
+
positionals: [
|
|
1074
|
+
{ name: "<sink>", summary: "The sink it arrived at." },
|
|
1075
|
+
{ name: "<delivery>", summary: "The recorded delivery to read." },
|
|
1076
|
+
],
|
|
1077
|
+
summary: "Read one recorded delivery, including the bytes that arrived.",
|
|
1078
|
+
words: "sinks delivery",
|
|
1079
|
+
},
|
|
1080
|
+
];
|
|
1081
|
+
/**
|
|
1082
|
+
* Every operation with no command, and why.
|
|
1083
|
+
*
|
|
1084
|
+
* The first six are the same fact stated six times: they need a browser
|
|
1085
|
+
* session, and a command line holds an API credential or an OAuth grant. That
|
|
1086
|
+
* is a property of the registry entry, so the rule checks it rather than
|
|
1087
|
+
* believing this list — and it is also why the story says credential and grant
|
|
1088
|
+
* operations appear "only where the current principal rules can support them
|
|
1089
|
+
* honestly". Creating a credential from a credential is the escalation the
|
|
1090
|
+
* control plane refuses on purpose.
|
|
1091
|
+
*/
|
|
1092
|
+
export const OPERATION_OMISSIONS = [
|
|
1093
|
+
{
|
|
1094
|
+
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.",
|
|
1095
|
+
ground: "principal-unreachable",
|
|
1096
|
+
operation: "organizations.lifecycle",
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
because: "Closing an organization requires the signed-in organization administrator session; allowing a project credential to close its parent would widen its authority.",
|
|
1100
|
+
ground: "principal-unreachable",
|
|
1101
|
+
operation: "organizations.close",
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
because: "Restoring an organization requires the same signed-in organization administrator session used by the browser recovery flow; project credentials have no organization-wide authority.",
|
|
1105
|
+
ground: "principal-unreachable",
|
|
1106
|
+
operation: "organizations.restore",
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
because: "The workspace execution-credit panel is the accepted human control for applying a pending policy; the MCP tool provides the approved automation path.",
|
|
1110
|
+
ground: "declared",
|
|
1111
|
+
operation: "billing.reconcile",
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
because: "The endpoint page is the accepted place to mark this human-observed server update done; adding a second interactive CLI design is outside this remediation.",
|
|
1115
|
+
ground: "declared",
|
|
1116
|
+
operation: "endpoints.acknowledgeSigningKeyRotation",
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
because: "Minting a credential needs a signed-in person. A command line holding one credential could otherwise mint another, which is the escalation the control plane refuses by declaring a session principal.",
|
|
1120
|
+
ground: "principal-unreachable",
|
|
1121
|
+
operation: "credentials.create",
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
because: "Reveals an endpoint's signing secret to a signed-in person on the endpoint's page. A command line holds a credential, and the create and the rotation already hand the secret to a credential once (FXO-2244).",
|
|
1125
|
+
ground: "principal-unreachable",
|
|
1126
|
+
operation: "endpoints.revealSigningSecret",
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
because: "Reads the connection detail of a credential for the dashboard, behind a browser session.",
|
|
1130
|
+
ground: "principal-unreachable",
|
|
1131
|
+
operation: "credentials.connection",
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
because: "Projects are chosen by a signed-in person; a credential already acts inside exactly one.",
|
|
1135
|
+
ground: "principal-unreachable",
|
|
1136
|
+
operation: "projects.list",
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
because: "Creating a project is an act of the account, not of a credential scoped to one.",
|
|
1140
|
+
ground: "principal-unreachable",
|
|
1141
|
+
operation: "projects.create",
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
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.",
|
|
1145
|
+
ground: "declared",
|
|
1146
|
+
operation: "projects.rename",
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
because: "Project General is the accepted archive surface in this story. The generated SDK and MCP expose the operation to automation; adding a second interactive CLI design is outside it.",
|
|
1150
|
+
ground: "declared",
|
|
1151
|
+
operation: "projects.archive",
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
because: "Permanent project deletion needs the counted confirmation Project General presents. The generated SDK and MCP expose it; a CLI confirmation design is not accepted in this story.",
|
|
1155
|
+
ground: "declared",
|
|
1156
|
+
operation: "projects.delete",
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
because: "The Executions page is the accepted project-wide history surface in this story. The generated SDK and MCP expose the operation to automation; adding a second interactive CLI design is outside it.",
|
|
1160
|
+
ground: "declared",
|
|
1161
|
+
operation: "executions.list",
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
because: "Grants are what a person gave to an application, and only that person may read them.",
|
|
1165
|
+
ground: "principal-unreachable",
|
|
1166
|
+
operation: "grants.list",
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
because: "Revoking a grant is the person's act. `greatwait logout` revokes this command line's own grant, which is the part it holds.",
|
|
1170
|
+
ground: "principal-unreachable",
|
|
1171
|
+
operation: "grants.revoke",
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
because: "The contract mock's control plane, not an API operation.",
|
|
1175
|
+
ground: "not-on-the-api",
|
|
1176
|
+
operation: "mock.listFixtures",
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
because: "The contract mock's control plane, not an API operation.",
|
|
1180
|
+
ground: "not-on-the-api",
|
|
1181
|
+
operation: "mock.getFixture",
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
because: "The contract mock's control plane, not an API operation.",
|
|
1185
|
+
ground: "not-on-the-api",
|
|
1186
|
+
operation: "mock.setScenario",
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
because: "Reports which workspace layer a service occupies, so the architecture boundary is observable at runtime. It answers a question about the build rather than about anything a person operates, and `greatwait doctor` already reports the build it reached.",
|
|
1190
|
+
ground: "declared",
|
|
1191
|
+
operation: "meta.scaffold",
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
because: "A transitional shape kept for one migrating consumer and scheduled for withdrawal. `greatwait forecast` reads the same data in the shape this product publishes, and a command line built on the shape being withdrawn would have to be rewritten when it goes.",
|
|
1195
|
+
ground: "declared",
|
|
1196
|
+
operation: "forecast.windows",
|
|
1197
|
+
},
|
|
1198
|
+
];
|
|
1199
|
+
/* -------------------------------------------------------------------------- */
|
|
1200
|
+
/* Rendering */
|
|
1201
|
+
/* -------------------------------------------------------------------------- */
|
|
1202
|
+
/**
|
|
1203
|
+
* The one-line spelling of a command, used by `--help` and by the reference.
|
|
1204
|
+
*
|
|
1205
|
+
* **Rendered from the declaration in both places rather than written twice.**
|
|
1206
|
+
* The story asks that the reference be generated or exhaustively checked
|
|
1207
|
+
* against the executable; sharing the renderer is the version of that with
|
|
1208
|
+
* nothing to check, because a page and a help screen produced by the same
|
|
1209
|
+
* function cannot disagree about what a command takes.
|
|
1210
|
+
*/
|
|
1211
|
+
export function usageLine(command, programName = CANONICAL_PROGRAM_NAME) {
|
|
1212
|
+
return [
|
|
1213
|
+
programName,
|
|
1214
|
+
command.words,
|
|
1215
|
+
...command.positionals.map((positional) => positional.optional === true && !positional.name.startsWith("[")
|
|
1216
|
+
? `[${positional.name}]`
|
|
1217
|
+
: positional.name),
|
|
1218
|
+
...command.options.map((option) => option.value === undefined
|
|
1219
|
+
? `[${option.flag}]`
|
|
1220
|
+
: `[${option.flag} ${option.value}]`),
|
|
1221
|
+
].join(" ");
|
|
1222
|
+
}
|
|
1223
|
+
/** The help for one command: what it does, what it takes, what each means. */
|
|
1224
|
+
export function commandHelp(command, programName = CANONICAL_PROGRAM_NAME) {
|
|
1225
|
+
const width = Math.max(0, ...command.positionals.map((positional) => positional.name.length), ...command.options.map((option) => option.flag.length +
|
|
1226
|
+
(option.value === undefined ? 0 : option.value.length + 1)));
|
|
1227
|
+
const rows = [
|
|
1228
|
+
...command.positionals.map((positional) => [positional.name, positional.summary]),
|
|
1229
|
+
...command.options.map((option) => [
|
|
1230
|
+
option.value === undefined
|
|
1231
|
+
? option.flag
|
|
1232
|
+
: `${option.flag} ${option.value}`,
|
|
1233
|
+
option.summary,
|
|
1234
|
+
]),
|
|
1235
|
+
];
|
|
1236
|
+
return [
|
|
1237
|
+
usageLine(command, programName),
|
|
1238
|
+
``,
|
|
1239
|
+
command.summary,
|
|
1240
|
+
...(rows.length === 0 ? [] : [``]),
|
|
1241
|
+
...rows.map(([name, summary]) => ` ${name.padEnd(width)} ${summary}`),
|
|
1242
|
+
].join("\n");
|
|
1243
|
+
}
|
|
1244
|
+
/** Every command, grouped, for the overview `--help` prints. */
|
|
1245
|
+
export function overview(commands = COMMAND_CATALOG) {
|
|
1246
|
+
const groups = [];
|
|
1247
|
+
const width = Math.max(...commands.map((command) => command.words.length));
|
|
1248
|
+
for (const command of commands) {
|
|
1249
|
+
if (groups[groups.length - 1] !== command.group)
|
|
1250
|
+
groups.push(command.group);
|
|
1251
|
+
}
|
|
1252
|
+
return [...new Set(groups)]
|
|
1253
|
+
.map((group) => [
|
|
1254
|
+
`${group}`,
|
|
1255
|
+
...commands
|
|
1256
|
+
.filter((command) => command.group === group)
|
|
1257
|
+
.map((command) => ` ${command.words.padEnd(width)} ${command.summary}`),
|
|
1258
|
+
].join("\n"))
|
|
1259
|
+
.join("\n\n");
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* The scopes `login` asks a person to grant.
|
|
1263
|
+
*
|
|
1264
|
+
* **Derived from what the commands actually call.** A list maintained beside
|
|
1265
|
+
* the catalog is a list that goes stale in the direction nobody notices: a
|
|
1266
|
+
* command added for an operation needing a scope nobody requested fails at
|
|
1267
|
+
* runtime with `scope_missing`, against a grant the person cannot widen without
|
|
1268
|
+
* signing in again. Deriving it also means a widening shows up in the generated
|
|
1269
|
+
* reference's diff, which is where a reviewer will see it.
|
|
1270
|
+
*/
|
|
1271
|
+
export function requestedScopes(inputs) {
|
|
1272
|
+
const byId = new Map(inputs.operations.map((operation) => [operation.id, operation]));
|
|
1273
|
+
const scopes = new Set();
|
|
1274
|
+
for (const command of inputs.commands) {
|
|
1275
|
+
for (const id of command.operations) {
|
|
1276
|
+
for (const scope of byId.get(id)?.scopes ?? [])
|
|
1277
|
+
scopes.add(scope);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return [...scopes].sort();
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Everything wrong with the declarations, as sentences.
|
|
1284
|
+
*
|
|
1285
|
+
* A pure function of its inputs so the cases below can hand it a catalog that
|
|
1286
|
+
* violates one rule at a time. Empty means the CLI, its reference and the
|
|
1287
|
+
* registry agree.
|
|
1288
|
+
*/
|
|
1289
|
+
export function coverageProblems(inputs) {
|
|
1290
|
+
const problems = [];
|
|
1291
|
+
const byId = new Map(inputs.operations.map((operation) => [operation.id, operation]));
|
|
1292
|
+
// **One flag cannot be valued on one command and bare on another.** The
|
|
1293
|
+
// parser runs *before* the dispatcher picks a command — it has to, because a
|
|
1294
|
+
// flag may precede the command words — so value-ness is a property of the
|
|
1295
|
+
// name across the whole catalog rather than of the command. Two commands
|
|
1296
|
+
// disagreeing about it would silently corrupt one of them: a bare flag would
|
|
1297
|
+
// swallow the next token, or a valued one would drop its value into the
|
|
1298
|
+
// positionals. Nothing else in the repository looks at this, and no command
|
|
1299
|
+
// today gets it wrong, which is the moment to write the rule down.
|
|
1300
|
+
const valueness = new Map();
|
|
1301
|
+
for (const command of inputs.commands) {
|
|
1302
|
+
for (const option of command.options) {
|
|
1303
|
+
const takesValue = option.value !== undefined;
|
|
1304
|
+
const seen = valueness.get(option.flag);
|
|
1305
|
+
if (seen !== undefined && seen !== takesValue) {
|
|
1306
|
+
problems.push(`${option.flag} takes a value on one command and not on another`);
|
|
1307
|
+
}
|
|
1308
|
+
valueness.set(option.flag, takesValue);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const seenWords = new Set();
|
|
1312
|
+
for (const command of inputs.commands) {
|
|
1313
|
+
const parts = command.words.split(" ");
|
|
1314
|
+
if (parts.length < 1 || parts.length > 2) {
|
|
1315
|
+
problems.push(`${command.words} is not one or two words, and the dispatcher looks up no more than two`);
|
|
1316
|
+
}
|
|
1317
|
+
if (seenWords.has(command.words)) {
|
|
1318
|
+
problems.push(`two commands are declared as ${command.words}`);
|
|
1319
|
+
}
|
|
1320
|
+
seenWords.add(command.words);
|
|
1321
|
+
const flags = new Set();
|
|
1322
|
+
for (const option of command.options) {
|
|
1323
|
+
if (!option.flag.startsWith("--")) {
|
|
1324
|
+
problems.push(`${command.words} declares ${option.flag}, which is not an option`);
|
|
1325
|
+
}
|
|
1326
|
+
if (flags.has(option.flag)) {
|
|
1327
|
+
problems.push(`${command.words} declares ${option.flag} twice`);
|
|
1328
|
+
}
|
|
1329
|
+
flags.add(option.flag);
|
|
1330
|
+
}
|
|
1331
|
+
// **A command that asks must publish the way to answer without a
|
|
1332
|
+
// terminal.** Otherwise automation meets the prompt policy's refusal with
|
|
1333
|
+
// no documented flag to satisfy it, and the reference page is where
|
|
1334
|
+
// somebody looks for one.
|
|
1335
|
+
if (command.confirms === true && !flags.has("--yes")) {
|
|
1336
|
+
problems.push(`${command.words} confirms and does not declare --yes`);
|
|
1337
|
+
}
|
|
1338
|
+
for (const id of command.operations) {
|
|
1339
|
+
const operation = byId.get(id);
|
|
1340
|
+
if (operation === undefined) {
|
|
1341
|
+
problems.push(`${command.words} covers ${id}, which the registry does not declare`);
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
if (!reachableByCli(operation)) {
|
|
1345
|
+
problems.push(`${command.words} covers ${id}, which no principal a command line can hold may call`);
|
|
1346
|
+
}
|
|
1347
|
+
if (!servedByApi(operation)) {
|
|
1348
|
+
problems.push(`${command.words} covers ${id}, which the API does not serve`);
|
|
1349
|
+
}
|
|
1350
|
+
const ground = confirmationGround(operation);
|
|
1351
|
+
if (ground !== undefined && command.confirms !== true) {
|
|
1352
|
+
problems.push(`${command.words} covers ${id}, which is ${ground}, and does not confirm`);
|
|
1353
|
+
}
|
|
1354
|
+
// **Both directions, because each is a different lie.** A command that
|
|
1355
|
+
// covers a conditional operation and offers no `--if-match` flattens
|
|
1356
|
+
// optimistic concurrency out of the surface: the only way to act on a
|
|
1357
|
+
// schedule somebody else may have moved becomes last-writer-wins. And a
|
|
1358
|
+
// required idempotency header the command line never lets anybody supply
|
|
1359
|
+
// makes a safe retry impossible to spell.
|
|
1360
|
+
const headers = declaredRequestHeaders(operation);
|
|
1361
|
+
if (headers.includes("If-Match") && !flags.has("--if-match")) {
|
|
1362
|
+
problems.push(`${command.words} covers ${id}, which takes If-Match, and does not offer --if-match`);
|
|
1363
|
+
}
|
|
1364
|
+
if (headers.includes("Idempotency-Key") &&
|
|
1365
|
+
!flags.has("--idempotency-key")) {
|
|
1366
|
+
problems.push(`${command.words} covers ${id}, which takes Idempotency-Key, and does not offer --idempotency-key`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// The other way round: a flag with nothing behind it. An option documented
|
|
1370
|
+
// on a command whose operations declare no such header is one somebody
|
|
1371
|
+
// passes, watches be ignored, and reports as a bug in the service.
|
|
1372
|
+
for (const [flag, header] of [
|
|
1373
|
+
["--if-match", "If-Match"],
|
|
1374
|
+
["--idempotency-key", "Idempotency-Key"],
|
|
1375
|
+
]) {
|
|
1376
|
+
if (!flags.has(flag))
|
|
1377
|
+
continue;
|
|
1378
|
+
const supported = command.operations.some((id) => {
|
|
1379
|
+
const operation = byId.get(id);
|
|
1380
|
+
return (operation !== undefined &&
|
|
1381
|
+
declaredRequestHeaders(operation).includes(header));
|
|
1382
|
+
});
|
|
1383
|
+
if (!supported) {
|
|
1384
|
+
problems.push(`${command.words} offers ${flag} and reaches no operation that takes ${header}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const covered = new Set(inputs.commands.flatMap((command) => command.operations));
|
|
1389
|
+
const omitted = new Map();
|
|
1390
|
+
for (const omission of inputs.omissions) {
|
|
1391
|
+
const operation = byId.get(omission.operation);
|
|
1392
|
+
if (operation === undefined) {
|
|
1393
|
+
problems.push(`${omission.operation} is omitted, and the registry does not declare it`);
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (omitted.has(omission.operation)) {
|
|
1397
|
+
problems.push(`${omission.operation} is omitted twice`);
|
|
1398
|
+
}
|
|
1399
|
+
omitted.set(omission.operation, omission);
|
|
1400
|
+
if (covered.has(omission.operation)) {
|
|
1401
|
+
problems.push(`${omission.operation} is both covered by a command and omitted`);
|
|
1402
|
+
}
|
|
1403
|
+
if (omission.because.trim().length < 20) {
|
|
1404
|
+
problems.push(`${omission.operation} is omitted with no stated reason`);
|
|
1405
|
+
}
|
|
1406
|
+
// **The two derived grounds are checked, not believed.** A claim that an
|
|
1407
|
+
// operation is out of reach stays true only while the registry says so, and
|
|
1408
|
+
// the day a session-only operation gains an API-credential principal is the
|
|
1409
|
+
// day this omission becomes a missing command nobody is looking for.
|
|
1410
|
+
if (omission.ground === "principal-unreachable" &&
|
|
1411
|
+
reachableByCli(operation)) {
|
|
1412
|
+
problems.push(`${omission.operation} is omitted as principal-unreachable and a command line can call it`);
|
|
1413
|
+
}
|
|
1414
|
+
if (omission.ground === "not-on-the-api" && servedByApi(operation)) {
|
|
1415
|
+
problems.push(`${omission.operation} is omitted as not-on-the-api and the API serves it`);
|
|
1416
|
+
}
|
|
1417
|
+
if (omission.ground === "declared" &&
|
|
1418
|
+
(!reachableByCli(operation) || !servedByApi(operation))) {
|
|
1419
|
+
problems.push(`${omission.operation} is omitted as a product decision and the registry already explains it — state the accurate ground`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
for (const operation of inputs.operations) {
|
|
1423
|
+
if (covered.has(operation.id) || omitted.has(operation.id))
|
|
1424
|
+
continue;
|
|
1425
|
+
problems.push(`${operation.id} has no command and no stated reason for having none`);
|
|
1426
|
+
}
|
|
1427
|
+
return problems;
|
|
1428
|
+
}
|
|
1429
|
+
/** The declarations as they stand, against the real registry. */
|
|
1430
|
+
export function catalogInputs() {
|
|
1431
|
+
return {
|
|
1432
|
+
commands: COMMAND_CATALOG,
|
|
1433
|
+
omissions: OPERATION_OMISSIONS,
|
|
1434
|
+
operations: OPERATIONS,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* The scopes `login` asks for, from what the commands call.
|
|
1439
|
+
*
|
|
1440
|
+
* Asking for everything the server offers would be asking a person to grant a
|
|
1441
|
+
* command line permissions it has no code to use; asking for less than the
|
|
1442
|
+
* commands need is a `scope_missing` on a grant they cannot widen without
|
|
1443
|
+
* signing in again. Neither is a list anybody maintains: this is that union.
|
|
1444
|
+
*/
|
|
1445
|
+
export const REQUESTED_SCOPES = requestedScopes(catalogInputs());
|
|
1446
|
+
/**
|
|
1447
|
+
* Re-exported so the reference generator has one import.
|
|
1448
|
+
*
|
|
1449
|
+
* The page publishes the exit numbers and what they mean, and both come from
|
|
1450
|
+
* the module the runtime reads. A generator reaching into `./exit` separately
|
|
1451
|
+
* would work; this keeps the catalog the single thing a projection depends on.
|
|
1452
|
+
*/
|
|
1453
|
+
export { EXIT_CLASSES, EXIT_CLASS_MEANINGS };
|