@m6d/cortex-cli 0.12.0 → 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.
- package/README.md +135 -130
- package/package.json +16 -5
- package/src/cli.ts +17 -12
- package/src/commands/apply.ts +169 -0
- package/src/commands/{cc.ts → management.ts} +150 -100
- package/src/commands/new.ts +50 -74
- package/src/contracts/charts/chart-spec.ts +1 -1
- package/src/contracts/client-tools/index.ts +3 -3
- package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
- package/src/contracts/datasets/index.ts +3 -0
- package/src/contracts/egress-policy/index.ts +16 -0
- package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
- package/src/contracts/{egress → egress-policy}/url.ts +22 -9
- package/src/contracts/errors/index.ts +38 -0
- package/src/contracts/json/index.ts +5 -0
- package/src/contracts/manifest/index.ts +932 -0
- package/src/contracts/manifest-file/index.ts +16 -0
- package/src/contracts/markdown/index.ts +1 -1
- package/src/contracts/pattern/index.ts +276 -0
- package/src/contracts/server/index.ts +247 -0
- package/src/contracts/server/reserved-names.fixtures.json +18 -0
- package/src/contracts/triggers/index.ts +112 -0
- package/src/contracts/wire/index.ts +61 -32
- package/src/{cc → management}/client.ts +19 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +5 -5
- package/src/scaffold/files.ts +362 -237
- package/src/scaffold/ports.ts +13 -24
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +7 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +2 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +52 -0
- package/src/ui/prompts.ts +4 -31
- package/src/ui/report.ts +5 -15
- package/tsconfig.json +2 -3
- package/src/commands/graph.ts +0 -228
- package/src/commands/swagger.ts +0 -247
- package/src/config/load.ts +0 -122
- package/src/config/validate.ts +0 -46
- package/src/contracts/credentials/index.ts +0 -183
- package/src/contracts/egress/index.ts +0 -120
- package/src/contracts/graph/clients/embed.ts +0 -45
- package/src/contracts/graph/clients/neo4j.ts +0 -85
- package/src/contracts/graph/helpers.ts +0 -55
- package/src/contracts/graph/index.ts +0 -30
- package/src/contracts/graph/schema.ts +0 -65
- package/src/contracts/graph/types.ts +0 -125
- package/src/contracts/rich-text/index.ts +0 -220
- package/src/contracts/runtime/api.ts +0 -345
- package/src/contracts/runtime/auth.ts +0 -41
- package/src/contracts/runtime/grammar.ts +0 -169
- package/src/contracts/runtime/index.ts +0 -15
- package/src/contracts/runtime/models.ts +0 -50
- package/src/contracts/runtime/signature.ts +0 -255
- package/src/contracts/runtime/sync.ts +0 -196
- package/src/contracts/runtime/triggers.ts +0 -267
- package/src/graph/expand-domains.ts +0 -286
- package/src/graph/generate-cypher.ts +0 -201
- package/src/graph/seed.ts +0 -199
- package/src/graph/validate.ts +0 -78
- package/src/scaffold/features.ts +0 -190
- package/src/scaffold/templates/domains-README.md.txt +0 -16
- package/src/scaffold/templates/index.ts.txt +0 -18
- package/src/swagger/extract-endpoints.ts +0 -269
- package/src/swagger/generated-block.ts +0 -74
- package/src/swagger/openapi-schema.ts +0 -168
- package/src/swagger/parse-response.ts +0 -88
- package/src/swagger/types.ts +0 -25
- /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
- /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
- /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
- /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 "@/
|
|
8
|
-
import type { OpenApiDocument, OpenApiOperation } from "@/
|
|
9
|
-
import { flagsFor, parseAnswer, toOption } from "@/
|
|
10
|
-
import type { Flag } from "@/
|
|
11
|
-
import { print } from "@/
|
|
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 "@/
|
|
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
|
|
27
|
-
*
|
|
28
|
-
* document on each run, so it always matches the
|
|
29
|
-
* and a new procedure needs no CLI work. Profiles
|
|
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
|
|
32
|
-
|
|
33
|
-
.command("
|
|
34
|
-
.helpGroup("
|
|
35
|
-
.
|
|
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
|
-
|
|
45
|
+
program
|
|
46
|
+
.command("logout")
|
|
47
|
+
.helpGroup("Cortex:")
|
|
56
48
|
.description("Forget a profile")
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
report.header("cortex
|
|
49
|
+
.action(function () {
|
|
50
|
+
const name = program.opts().profile ?? "default";
|
|
51
|
+
report.header("cortex logout");
|
|
60
52
|
report.footer(
|
|
61
|
-
withProfileFile(() => removeProfile(
|
|
62
|
-
? `removed ${
|
|
63
|
-
: `no profile ${
|
|
53
|
+
withProfileFile(() => removeProfile(name))
|
|
54
|
+
? `removed ${name}`
|
|
55
|
+
: `no profile ${name}`,
|
|
64
56
|
);
|
|
65
57
|
});
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
.
|
|
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(
|
|
71
|
-
report.header("cortex
|
|
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
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 `
|
|
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(
|
|
89
|
+
async function login(given: string | undefined, name: string) {
|
|
97
90
|
const url = requireTls(
|
|
98
|
-
(
|
|
99
|
-
|
|
100
|
-
|
|
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["
|
|
98
|
+
: (process.env["CORTEX_KEY"] ??
|
|
105
99
|
report.fail({
|
|
106
100
|
what: "No terminal to ask for the management key",
|
|
107
|
-
blocks: ["Set
|
|
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: "
|
|
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(
|
|
119
|
-
report.header("cortex
|
|
120
|
-
report.input("Profile",
|
|
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
|
|
128
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
178
|
+
what: "No Cortex to talk to",
|
|
184
179
|
blocks: [
|
|
185
180
|
"Log in once:",
|
|
186
|
-
" cortex
|
|
187
|
-
"or set
|
|
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(
|
|
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 =
|
|
217
|
+
const group = program
|
|
218
|
+
.command(router)
|
|
219
|
+
.description(`${router} procedures`)
|
|
220
|
+
.helpCommand(false);
|
|
224
221
|
for (const operation of operations) {
|
|
225
|
-
registerOperation({
|
|
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 {
|
|
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
|
|
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
|
|
296
|
-
report.input("
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
375
|
-
* run's outcome, in the error grammar
|
|
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({
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
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,
|