@m6d/cortex-cli 0.12.1 → 0.13.0

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.
Files changed (74) hide show
  1. package/README.md +135 -130
  2. package/package.json +16 -5
  3. package/src/cli.ts +17 -12
  4. package/src/commands/apply.ts +169 -0
  5. package/src/commands/{cc.ts → management.ts} +150 -100
  6. package/src/commands/new.ts +50 -74
  7. package/src/contracts/charts/chart-spec.ts +1 -1
  8. package/src/contracts/client-tools/index.ts +3 -3
  9. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  10. package/src/contracts/datasets/index.ts +3 -0
  11. package/src/contracts/egress-policy/index.ts +16 -0
  12. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  13. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  14. package/src/contracts/errors/index.ts +38 -0
  15. package/src/contracts/json/index.ts +5 -0
  16. package/src/contracts/manifest/index.ts +932 -0
  17. package/src/contracts/manifest-file/index.ts +16 -0
  18. package/src/contracts/markdown/index.ts +1 -1
  19. package/src/contracts/pattern/index.ts +276 -0
  20. package/src/contracts/server/index.ts +247 -0
  21. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  22. package/src/contracts/triggers/index.ts +112 -0
  23. package/src/contracts/wire/index.ts +61 -32
  24. package/src/{cc → management}/client.ts +19 -7
  25. package/src/{cc → management}/flags.ts +2 -1
  26. package/src/{cc → management}/profiles.ts +5 -5
  27. package/src/scaffold/files.ts +362 -237
  28. package/src/scaffold/ports.ts +13 -24
  29. package/src/scaffold/templates/datasets.ts.txt +52 -0
  30. package/src/scaffold/templates/dockerignore.txt +7 -0
  31. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  32. package/src/scaffold/templates/gitignore.txt +2 -1
  33. package/src/scaffold/templates/server.ts.txt +40 -0
  34. package/src/scaffold/templates/setup.ts.txt +52 -0
  35. package/src/ui/prompts.ts +4 -31
  36. package/src/ui/report.ts +5 -15
  37. package/tsconfig.json +2 -3
  38. package/src/commands/graph.ts +0 -228
  39. package/src/commands/swagger.ts +0 -247
  40. package/src/config/load.ts +0 -122
  41. package/src/config/validate.ts +0 -46
  42. package/src/contracts/credentials/index.ts +0 -183
  43. package/src/contracts/egress/index.ts +0 -120
  44. package/src/contracts/graph/clients/embed.ts +0 -62
  45. package/src/contracts/graph/clients/neo4j.ts +0 -85
  46. package/src/contracts/graph/helpers.ts +0 -55
  47. package/src/contracts/graph/index.ts +0 -30
  48. package/src/contracts/graph/schema.ts +0 -65
  49. package/src/contracts/graph/types.ts +0 -125
  50. package/src/contracts/rich-text/index.ts +0 -220
  51. package/src/contracts/runtime/api.ts +0 -345
  52. package/src/contracts/runtime/auth.ts +0 -41
  53. package/src/contracts/runtime/grammar.ts +0 -169
  54. package/src/contracts/runtime/index.ts +0 -15
  55. package/src/contracts/runtime/models.ts +0 -50
  56. package/src/contracts/runtime/signature.ts +0 -255
  57. package/src/contracts/runtime/sync.ts +0 -196
  58. package/src/contracts/runtime/triggers.ts +0 -267
  59. package/src/graph/expand-domains.ts +0 -286
  60. package/src/graph/generate-cypher.ts +0 -201
  61. package/src/graph/seed.ts +0 -199
  62. package/src/graph/validate.ts +0 -78
  63. package/src/scaffold/features.ts +0 -190
  64. package/src/scaffold/templates/domains-README.md.txt +0 -16
  65. package/src/scaffold/templates/index.ts.txt +0 -18
  66. package/src/swagger/extract-endpoints.ts +0 -269
  67. package/src/swagger/generated-block.ts +0 -74
  68. package/src/swagger/openapi-schema.ts +0 -168
  69. package/src/swagger/parse-response.ts +0 -88
  70. package/src/swagger/types.ts +0 -25
  71. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  72. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  73. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  74. /package/src/{cc → management}/output.ts +0 -0
@@ -1,74 +1,68 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
3
  import { type Command, Option } from "@commander-js/extra-typings";
4
- import { canCarrySecret } from "@cortex/contracts/egress";
4
+ import { canCarrySecret } from "@cortex/contracts/egress-policy";
5
5
  import { isRecord } from "@cortex/contracts/json";
6
6
 
7
- import { callManagement, inputSchemaOf, loadOpenApi, ManagementError } from "@/cc/client";
8
- import type { OpenApiDocument, OpenApiOperation } from "@/cc/client";
9
- import { flagsFor, parseAnswer, toOption } from "@/cc/flags";
10
- import type { Flag } from "@/cc/flags";
11
- import { print } from "@/cc/output";
7
+ import { callManagement, inputSchemaOf, loadOpenApi, ManagementError } from "@/management/client";
8
+ import type { OpenApiDocument, OpenApiOperation } from "@/management/client";
9
+ import { flagsFor, parseAnswer, toOption } from "@/management/flags";
10
+ import type { Flag } from "@/management/flags";
11
+ import { print } from "@/management/output";
12
12
  import {
13
13
  ProfileFileError,
14
14
  profilesPath,
15
15
  removeProfile,
16
16
  resolveProfile,
17
17
  saveProfile,
18
- } from "@/cc/profiles";
18
+ } from "@/management/profiles";
19
19
  import * as prompts from "@/ui/prompts";
20
20
  import * as report from "@/ui/report";
21
21
 
22
- const PROFILE_COMMANDS = new Set(["login", "logout", "status"]);
23
22
  const MAX_PAGES = 1000;
24
23
 
24
+ /** The root command; `--profile` names the saved Cortex every command here talks to. */
25
+ type Program = Command<[], { profile?: string }>;
26
+
25
27
  /**
26
- * `cortex cc <router> <procedure> [id] [flags]`: the Control Center's
27
- * management API as commands. The tree is built from the CC's own OpenAPI
28
- * document on each run, so it always matches the deployment it points at
29
- * and a new procedure needs no CLI work. Profiles hold the URL and key.
28
+ * `cortex <router> <procedure> [id] [flags]`: Cortex's management API as
29
+ * commands, beside `login`, `logout` and `status`. The tree is built from
30
+ * Cortex's own OpenAPI document on each run, so it always matches the
31
+ * deployment it points at and a new procedure needs no CLI work. Profiles
32
+ * hold the URL and key. Registered after every other command, since any
33
+ * name argv gives that none of them claims is a router's.
30
34
  */
31
- export function registerCc(program: Command, argv: string[]) {
32
- const cc = program
33
- .command("cc")
34
- .helpGroup("Control Center:")
35
- .summary("Manage a Control Center without its console")
36
- .description(
37
- "Manage a Control Center without its console.\n\nCommands come from the CC's management API: `cortex cc <router> <procedure>`.\nLog in once with a management key; profiles live in ~/.cortex/cc.json.",
38
- )
39
- .option("-p, --profile <name>", "Which saved Control Center to talk to")
40
- .addOption(
41
- new Option("-o, --output <format>", "Output format")
42
- .choices(["table", "json"])
43
- .default("table"),
44
- )
45
- .helpCommand(false);
46
-
47
- cc.command("login")
48
- .description("Save a Control Center URL and management key as a profile")
35
+ export function registerManagement(program: Program, argv: string[]) {
36
+ program
37
+ .command("login")
38
+ .helpGroup("Cortex:")
39
+ .description("Save a Cortex URL and key as a profile")
49
40
  .option("--url <url>")
50
- .option("--name <name>", "Profile name", "default")
51
41
  .action(async function (options) {
52
- await login(options);
42
+ await login(options.url, program.opts().profile ?? "default");
53
43
  });
54
44
 
55
- cc.command("logout")
45
+ program
46
+ .command("logout")
47
+ .helpGroup("Cortex:")
56
48
  .description("Forget a profile")
57
- .option("--name <name>", "Profile name", "default")
58
- .action(function (options) {
59
- report.header("cortex cc logout");
49
+ .action(function () {
50
+ const name = program.opts().profile ?? "default";
51
+ report.header("cortex logout");
60
52
  report.footer(
61
- withProfileFile(() => removeProfile(options.name))
62
- ? `removed ${options.name}`
63
- : `no profile ${options.name}`,
53
+ withProfileFile(() => removeProfile(name))
54
+ ? `removed ${name}`
55
+ : `no profile ${name}`,
64
56
  );
65
57
  });
66
58
 
67
- cc.command("status")
68
- .description("Which Control Center commands run against, and with which key")
59
+ program
60
+ .command("status")
61
+ .helpGroup("Cortex:")
62
+ .description("Show the Cortex URL and key commands use")
69
63
  .action(async function () {
70
- const profile = requireProfile(cc.opts().profile);
71
- report.header("cortex cc status");
64
+ const profile = requireProfile(program.opts().profile);
65
+ report.header("cortex status");
72
66
  report.input("Profile", profile.name);
73
67
  report.input("URL", profile.url);
74
68
  const me = await whoami(profile);
@@ -76,56 +70,56 @@ export function registerCc(program: Command, argv: string[]) {
76
70
  });
77
71
 
78
72
  // The resource tree needs the document, which needs a profile, so it is
79
- // built only when argv actually names a resource command: the first
80
- // operand after `cc`. Commander parses the group's own options here, so
81
- // every spelling it accepts (`-p x`, `-px`, `--profile=x`) names the same
82
- // profile the commands then run against. Root options may precede `cc`.
83
- const at = argv.findIndex((word) => !word.startsWith("-"));
84
- if (at === -1 || argv[at] !== "cc") return;
85
- const { operands } = cc.parseOptions(argv.slice(at + 1));
86
- const first = operands[0];
87
- if (first === undefined || PROFILE_COMMANDS.has(first)) return;
88
- return buildResourceCommands(cc, requireProfile(cc.opts().profile));
73
+ // built only when argv's first operand names no command registered so
74
+ // far. Commander parses the root's own options here, so every spelling
75
+ // it accepts (`-p x`, `-px`, `--profile=x`) names the same profile the
76
+ // commands then run against.
77
+ const first = program.parseOptions(argv).operands[0];
78
+ if (first === undefined || program.commands.some((command) => command.name() === first)) {
79
+ return;
80
+ }
81
+ return buildResourceCommands(program, requireProfile(program.opts().profile));
89
82
  }
90
83
 
91
84
  /**
92
85
  * The key is never a flag (argv is visible to other processes and lands in
93
- * shell history): it is prompted for masked, or read from `CORTEX_CC_KEY`
86
+ * shell history): it is prompted for masked, or read from `CORTEX_KEY`
94
87
  * when there is no terminal to ask.
95
88
  */
96
- async function login(options: { url?: string; name: string }) {
89
+ async function login(given: string | undefined, name: string) {
97
90
  const url = requireTls(
98
- (
99
- options.url ?? (await prompts.askValue("Control Center URL", "https://cc.example.com"))
100
- ).replace(/\/+$/, ""),
91
+ (given ?? (await prompts.askValue("Cortex URL", "https://cortex.example.com"))).replace(
92
+ /\/+$/,
93
+ "",
94
+ ),
101
95
  );
102
96
  const key = process.stdin.isTTY
103
97
  ? await prompts.askSecret("Management key")
104
- : (process.env["CORTEX_CC_KEY"] ??
98
+ : (process.env["CORTEX_KEY"] ??
105
99
  report.fail({
106
100
  what: "No terminal to ask for the management key",
107
- blocks: ["Set CORTEX_CC_KEY in the environment and run login again."],
101
+ blocks: ["Set CORTEX_KEY in the environment and run login again."],
108
102
  }));
109
103
  const me = await whoami({ url, key });
110
104
  if (!me) {
111
105
  report.fail({
112
- what: "The Control Center rejected that key",
106
+ what: "Cortex rejected that key",
113
107
  blocks: [
114
108
  "Mint one on your account page, or set BOOTSTRAP_MANAGEMENT_KEY on a headless deployment.",
115
109
  ],
116
110
  });
117
111
  }
118
- withProfileFile(() => saveProfile(options.name, { url, key }));
119
- report.header("cortex cc login");
120
- report.input("Profile", options.name);
112
+ withProfileFile(() => saveProfile(name, { url, key }));
113
+ report.header("cortex login");
114
+ report.input("Profile", name);
121
115
  report.input("URL", url);
122
116
  report.input("Key", me);
123
117
  report.footer(`saved to ${profilesPath()}`);
124
118
  }
125
119
 
126
120
  /**
127
- * The key the Control Center recognized the bearer as, or null when it does
128
- * not authenticate. A Control Center that cannot be reached, or answers with
121
+ * The key Cortex recognized the bearer as, or null when it does not
122
+ * authenticate. A Cortex that cannot be reached, or answers with
129
123
  * anything but 200/401, is the run's outcome, not a CLI bug.
130
124
  */
131
125
  async function whoami(profile: { url: string; key: string }) {
@@ -145,7 +139,7 @@ async function whoami(profile: { url: string; key: string }) {
145
139
  report.fail({
146
140
  what: `Could not reach ${profile.url}`,
147
141
  error,
148
- blocks: ["Check the URL and that the Control Center is up."],
142
+ blocks: ["Check the URL and that Cortex is up."],
149
143
  });
150
144
  }
151
145
  }
@@ -158,7 +152,7 @@ function withProfileFile<T>(operation: () => T) {
158
152
  if (error instanceof ProfileFileError) {
159
153
  report.fail({
160
154
  what: error.message,
161
- blocks: ["Fix or delete the file, then log in again:", " cortex cc login"],
155
+ blocks: ["Fix or delete the file, then log in again:", " cortex login"],
162
156
  });
163
157
  }
164
158
  throw error;
@@ -176,15 +170,17 @@ function requireTls(url: string) {
176
170
  });
177
171
  }
178
172
 
179
- function requireProfile(name: string | undefined) {
173
+ /** The named profile, or the current one; a missing or plain-http one ends the run with how to fix it. */
174
+ export function requireProfile(name: string | undefined) {
180
175
  const profile = withProfileFile(() => resolveProfile(name));
181
176
  if (!profile) {
182
177
  report.fail({
183
- what: "No Control Center to talk to",
178
+ what: "No Cortex to talk to",
184
179
  blocks: [
185
180
  "Log in once:",
186
- " cortex cc login",
187
- "or set CORTEX_CC_URL and CORTEX_CC_KEY in the environment.",
181
+ " cortex login",
182
+ "or set CORTEX_URL and CORTEX_KEY in the environment.",
183
+ "`cortex --help` lists the commands that need neither.",
188
184
  ],
189
185
  });
190
186
  }
@@ -194,11 +190,9 @@ function requireProfile(name: string | undefined) {
194
190
 
195
191
  type ResolvedProfile = NonNullable<ReturnType<typeof resolveProfile>>;
196
192
 
197
- type CcGroup = Command<[], { profile?: string; output: "table" | "json" }>;
198
-
199
193
  type Operation = { path: string; method: "get" | "post"; operation: OpenApiOperation };
200
194
 
201
- async function buildResourceCommands(cc: CcGroup, profile: ResolvedProfile) {
195
+ async function buildResourceCommands(program: Program, profile: ResolvedProfile) {
202
196
  const loaded = await loadOpenApi(profile.url).catch((error: unknown) =>
203
197
  report.fail({
204
198
  what: `Could not read the management API at ${profile.url}`,
@@ -220,9 +214,12 @@ async function buildResourceCommands(cc: CcGroup, profile: ResolvedProfile) {
220
214
  [...byRouter.values()].flat().map((o) => [o.operation.operationId, o]),
221
215
  );
222
216
  for (const [router, operations] of byRouter) {
223
- const group = cc.command(router).description(`${router} procedures`).helpCommand(false);
217
+ const group = program
218
+ .command(router)
219
+ .description(`${router} procedures`)
220
+ .helpCommand(false);
224
221
  for (const operation of operations) {
225
- registerOperation({ cc, group, router, operation, byProcedure, document, profile });
222
+ registerOperation({ group, router, operation, byProcedure, document, profile });
226
223
  }
227
224
  }
228
225
  }
@@ -248,7 +245,6 @@ function groupByRouter(document: OpenApiDocument) {
248
245
  * on writes to a publishable router.
249
246
  */
250
247
  function registerOperation(scope: {
251
- cc: CcGroup;
252
248
  group: Command;
253
249
  router: string;
254
250
  operation: Operation;
@@ -256,7 +252,7 @@ function registerOperation(scope: {
256
252
  document: OpenApiDocument;
257
253
  profile: ResolvedProfile;
258
254
  }) {
259
- const { cc, group, router, byProcedure, document, profile } = scope;
255
+ const { group, router, byProcedure, document, profile } = scope;
260
256
  const { path, method, operation } = scope.operation;
261
257
  const name = operation.operationId.split(".")[1] ?? path;
262
258
  const input = inputSchemaOf(operation);
@@ -266,7 +262,7 @@ function registerOperation(scope: {
266
262
  // (`x-publish-procedure`); a create that leaves the publication content
267
263
  // blank carries none, since publishing it would only fail. Fields the
268
264
  // document marks live (`x-live-fields`) are reported as such after the
269
- // write instead of as part of the draft.
265
+ // write, apart from the draft.
270
266
  const publishWith = operation["x-publish-procedure"];
271
267
  const publisher = publishWith === undefined ? undefined : byProcedure.get(publishWith);
272
268
  const canPublish = publisher !== undefined;
@@ -279,6 +275,7 @@ function registerOperation(scope: {
279
275
 
280
276
  command.action(async function (...args) {
281
277
  const options = command.opts() as Record<string, unknown> & {
278
+ output: "table" | "json";
282
279
  json?: string;
283
280
  all?: boolean;
284
281
  publish?: boolean;
@@ -286,14 +283,13 @@ function registerOperation(scope: {
286
283
  // Commander passes the options object first when the command declares
287
284
  // no argument, so only a string is the `[id]` positional.
288
285
  const positional = typeof args[0] === "string" ? args[0] : undefined;
289
- const { json, all, publish, ...given } = options;
286
+ const { output: format, json, all, publish, ...given } = options;
290
287
  const body = await collectInput(flags, given, json, positional, name);
291
- const format = cc.opts().output;
292
288
  // `-o json` promises stdout holds only the result; prose moves to stderr.
293
289
  const note = format === "json" ? report.aside : report.note;
294
290
  if (format === "table") {
295
- report.header(`cortex cc ${router} ${name}`);
296
- report.input("Control Center", profile.url);
291
+ report.header(`cortex ${router} ${name}`);
292
+ report.input("Cortex", profile.url);
297
293
  }
298
294
  const result = await callOrFail(profile, () =>
299
295
  all
@@ -320,9 +316,26 @@ function declareCommand(
320
316
  flags: Flag[],
321
317
  shape: { paginated: boolean; canPublish: boolean },
322
318
  ) {
323
- const command = group.command(name).description(summary);
319
+ const command = group
320
+ .command(name)
321
+ .description(summary)
322
+ .addOption(
323
+ new Option("-o, --output <format>", "Output format")
324
+ .choices(["table", "json"])
325
+ .default("table"),
326
+ );
327
+ // Optional to Commander even when the input requires it: `--json` may hold
328
+ // it, and collecting the input asks for, or names, a required field left
329
+ // out. So the help says which it is.
324
330
  const idFlag = flags.find((flag) => flag.name === "id");
325
- if (idFlag) command.argument(idFlag.required ? "<id>" : "[id]");
331
+ if (idFlag) {
332
+ command.argument(
333
+ "[id]",
334
+ idFlag.required
335
+ ? "Required, here or in --json, or asked for on a terminal"
336
+ : "Optional",
337
+ );
338
+ }
326
339
  // A sensitive field never becomes a flag: argv is visible to every
327
340
  // process on the machine and lands in shell history. It is prompted for
328
341
  // masked on a terminal, or supplied through `--json -` from stdin.
@@ -339,7 +352,7 @@ function declareCommand(
339
352
 
340
353
  /**
341
354
  * A write to a publishable router is a draft: publish it now, or say how to.
342
- * Fields the server reads live are already in effect and are named as such,
355
+ * Fields Cortex reads live are already in effect and are named as such,
343
356
  * since "draft" would understate what just happened.
344
357
  */
345
358
  async function publishAfterWrite(
@@ -351,11 +364,11 @@ async function publishAfterWrite(
351
364
  ) {
352
365
  const id = (result as { id?: string } | null)?.id;
353
366
  if (!id) return;
354
- const command = `cortex cc ${publisher.operation.operationId.replace(".", " ")} ${id}`;
367
+ const command = `cortex ${publisher.operation.operationId.replace(".", " ")} ${id}`;
355
368
  const live = options.liveFields.length > 0;
356
369
  if (live) {
357
370
  options.note(
358
- `${options.liveFields.join(", ")} ${options.liveFields.length === 1 ? "is" : "are"} live: bound on the server's next config revalidation, no publish needed.`,
371
+ `${options.liveFields.join(", ")} ${options.liveFields.length === 1 ? "is" : "are"} live: agents use them from their next turn, no publish needed.`,
359
372
  );
360
373
  }
361
374
  if (publish) {
@@ -371,31 +384,64 @@ async function publishAfterWrite(
371
384
  }
372
385
 
373
386
  /**
374
- * A management refusal, or a Control Center that cannot be reached, is the
375
- * run's outcome, in the error grammar; anything else is a bug.
387
+ * A management refusal, or a Cortex that cannot be reached, is the
388
+ * run's outcome, in the error grammar, with whatever the refusal names;
389
+ * anything else is a bug.
376
390
  */
377
- async function callOrFail<T>(profile: ResolvedProfile, call: () => Promise<T>) {
391
+ export async function callOrFail<T>(profile: ResolvedProfile, call: () => Promise<T>) {
378
392
  try {
379
393
  return await call();
380
394
  } catch (error) {
381
395
  if (error instanceof ManagementError) {
382
- report.fail({ what: `${error.status} ${error.code}: ${error.message}` });
396
+ report.fail({
397
+ what: `${error.status} ${error.code}: ${error.message}`,
398
+ blocks: refusalBlocks(error.detail ?? {}),
399
+ });
383
400
  }
384
401
  if (error instanceof TypeError) {
385
402
  report.fail({
386
403
  what: `Could not reach ${profile.url}`,
387
404
  error,
388
- blocks: ["Check the URL and that the Control Center is up."],
405
+ blocks: ["Check the URL and that Cortex is up."],
389
406
  });
390
407
  }
391
408
  throw error;
392
409
  }
393
410
  }
394
411
 
412
+ /**
413
+ * What a refusal names, a block each: the manifest entry it is about (its
414
+ * kind and name), each issue at its path in that entry, then every other
415
+ * field (who holds a name, what is missing) as `field: value`.
416
+ */
417
+ function refusalBlocks({ error: _literal, kind, name, issues, ...rest }: Record<string, unknown>) {
418
+ const entry = typeof kind === "string" && typeof name === "string";
419
+ const fields = {
420
+ ...(entry ? {} : { kind, name }),
421
+ ...(Array.isArray(issues) ? {} : { issues }),
422
+ ...rest,
423
+ };
424
+ return [
425
+ entry ? `${kind} ${JSON.stringify(name)}` : "",
426
+ Array.isArray(issues) ? issues.map(issueLine).join("\n") : "",
427
+ Object.entries(fields)
428
+ .filter(([, value]) => value !== undefined)
429
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
430
+ .join("\n"),
431
+ ].filter((block) => block !== "");
432
+ }
433
+
434
+ /** One issue at its path in the entry, or at the entry itself when the path is empty. */
435
+ function issueLine(issue: unknown) {
436
+ if (!isRecord(issue)) return JSON.stringify(issue);
437
+ const path = issue["path"];
438
+ return `${typeof path === "string" && path !== "" ? path : "(entry)"}: ${String(issue["message"])}`;
439
+ }
440
+
395
441
  /**
396
442
  * The body: `--json` first, flags layered over it, the positional as `id`.
397
- * A required field still missing is asked for on a TTY and named as the
398
- * flag to add otherwise.
443
+ * A required field left missing is asked for on a TTY and named as the
444
+ * flag, or the positional, to add otherwise.
399
445
  */
400
446
  async function collectInput(
401
447
  flags: Flag[],
@@ -413,7 +459,11 @@ async function collectInput(
413
459
  const missing = flags.filter((flag) => flag.required && body.get(flag.name) === undefined);
414
460
  if (missing.length > 0 && !process.stdin.isTTY) {
415
461
  const named = (flag: Flag) =>
416
- flag.sensitive ? `${flag.name} (via --json -)` : `--${flag.name}`;
462
+ flag.name === "id"
463
+ ? "<id>"
464
+ : flag.sensitive
465
+ ? `${flag.name} (via --json -)`
466
+ : `--${flag.name}`;
417
467
  report.fail({
418
468
  what: `${verb} needs ${named(missing[0]!)}`,
419
469
  blocks: [
@@ -455,7 +505,7 @@ function readJsonBody(source: string) {
455
505
  if (source === "-" && process.stdin.isTTY) {
456
506
  report.fail({
457
507
  what: "--json - reads the body from stdin, but nothing is piped in",
458
- blocks: ["Pipe a JSON document:", " cat body.json | cortex cc ... --json -"],
508
+ blocks: ["Pipe a JSON document:", " cat body.json | cortex ... --json -"],
459
509
  });
460
510
  }
461
511
  const label = source === "-" ? "stdin" : source;
@@ -488,8 +538,8 @@ async function walkPages(
488
538
  const items = new Array<unknown>();
489
539
  let count = 0;
490
540
  let filteredCount = 0;
491
- // Bounded: a server that reports more rows than it serves must not spin,
492
- // and a listing the bound cannot cover is an error, never a short result.
541
+ // Bounded: if Cortex reports more rows than it serves, the walk must not
542
+ // spin, and a listing the bound cannot cover is an error, never a short result.
493
543
  for (let pageIndex = 0; pageIndex < MAX_PAGES; pageIndex += 1) {
494
544
  const page = (await callManagement(profile, method, path, {
495
545
  ...given,