@m6d/cortex-cli 0.12.1 → 0.14.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 +193 -128
- package/package.json +16 -5
- package/src/cli.ts +19 -12
- package/src/commands/apply.ts +186 -0
- package/src/commands/{cc.ts → management.ts} +173 -112
- package/src/commands/new.ts +83 -84
- package/src/commands/setup.ts +610 -0
- 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 +973 -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 +349 -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 +67 -33
- package/src/{cc → management}/client.ts +33 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +10 -8
- package/src/scaffold/files.ts +448 -284
- package/src/scaffold/ports.ts +12 -24
- package/src/scaffold/templates/cspell.json.txt +41 -0
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +6 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +1 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +44 -0
- package/src/ui/prompts.ts +24 -38
- 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 -62
- 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,74 @@
|
|
|
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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import {
|
|
8
|
+
callManagement,
|
|
9
|
+
inputSchemaOf,
|
|
10
|
+
loadOpenApi,
|
|
11
|
+
ManagementError,
|
|
12
|
+
UnreachableError,
|
|
13
|
+
} from "@/management/client";
|
|
14
|
+
import type { OpenApiDocument, OpenApiOperation } from "@/management/client";
|
|
15
|
+
import { flagsFor, parseAnswer, toOption } from "@/management/flags";
|
|
16
|
+
import type { Flag } from "@/management/flags";
|
|
17
|
+
import { print } from "@/management/output";
|
|
12
18
|
import {
|
|
13
19
|
ProfileFileError,
|
|
14
20
|
profilesPath,
|
|
15
21
|
removeProfile,
|
|
16
22
|
resolveProfile,
|
|
17
23
|
saveProfile,
|
|
18
|
-
} from "@/
|
|
24
|
+
} from "@/management/profiles";
|
|
19
25
|
import * as prompts from "@/ui/prompts";
|
|
20
26
|
import * as report from "@/ui/report";
|
|
21
27
|
|
|
22
|
-
const PROFILE_COMMANDS = new Set(["login", "logout", "status"]);
|
|
23
28
|
const MAX_PAGES = 1000;
|
|
24
29
|
|
|
30
|
+
/** The root command; `--profile` names the saved Cortex every command here talks to. */
|
|
31
|
+
type Program = Command<[], { profile?: string }>;
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
|
-
* `cortex
|
|
27
|
-
*
|
|
28
|
-
* document on each run, so it always matches the
|
|
29
|
-
* and a new procedure needs no CLI work. Profiles
|
|
34
|
+
* `cortex <router> <procedure> [id] [flags]`: Cortex's management API as
|
|
35
|
+
* commands, beside `login`, `logout` and `status`. The tree is built from
|
|
36
|
+
* Cortex's own OpenAPI document on each run, so it always matches the
|
|
37
|
+
* deployment it points at and a new procedure needs no CLI work. Profiles
|
|
38
|
+
* hold the URL and key. Registered after every other command, since any
|
|
39
|
+
* name argv gives that none of them claims is a router's.
|
|
30
40
|
*/
|
|
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")
|
|
41
|
+
export function registerManagement(program: Program, argv: string[]) {
|
|
42
|
+
program
|
|
43
|
+
.command("login")
|
|
44
|
+
.helpGroup("Cortex:")
|
|
45
|
+
.description("Save a Cortex URL and key as a profile")
|
|
49
46
|
.option("--url <url>")
|
|
50
|
-
.option("--name <name>", "Profile name", "default")
|
|
51
47
|
.action(async function (options) {
|
|
52
|
-
await login(options);
|
|
48
|
+
await login(options.url, program.opts().profile ?? "default");
|
|
53
49
|
});
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
program
|
|
52
|
+
.command("logout")
|
|
53
|
+
.helpGroup("Cortex:")
|
|
56
54
|
.description("Forget a profile")
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
report.header("cortex
|
|
55
|
+
.action(function () {
|
|
56
|
+
const name = program.opts().profile ?? "default";
|
|
57
|
+
report.header("cortex logout");
|
|
60
58
|
report.footer(
|
|
61
|
-
withProfileFile(() => removeProfile(
|
|
62
|
-
? `removed ${
|
|
63
|
-
: `no profile ${
|
|
59
|
+
withProfileFile(() => removeProfile(name))
|
|
60
|
+
? `removed ${name}`
|
|
61
|
+
: `no profile ${name}`,
|
|
64
62
|
);
|
|
65
63
|
});
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
.
|
|
65
|
+
program
|
|
66
|
+
.command("status")
|
|
67
|
+
.helpGroup("Cortex:")
|
|
68
|
+
.description("Show the Cortex URL and key commands use")
|
|
69
69
|
.action(async function () {
|
|
70
|
-
const profile = requireProfile(
|
|
71
|
-
report.header("cortex
|
|
70
|
+
const profile = requireProfile(program.opts().profile);
|
|
71
|
+
report.header("cortex status");
|
|
72
72
|
report.input("Profile", profile.name);
|
|
73
73
|
report.input("URL", profile.url);
|
|
74
74
|
const me = await whoami(profile);
|
|
@@ -76,56 +76,56 @@ export function registerCc(program: Command, argv: string[]) {
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
// 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));
|
|
79
|
+
// built only when argv's first operand names no command registered so
|
|
80
|
+
// far. Commander parses the root's own options here, so every spelling
|
|
81
|
+
// it accepts (`-p x`, `-px`, `--profile=x`) names the same profile the
|
|
82
|
+
// commands then run against.
|
|
83
|
+
const first = program.parseOptions(argv).operands[0];
|
|
84
|
+
if (first === undefined || program.commands.some((command) => command.name() === first)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
return buildResourceCommands(program, requireProfile(program.opts().profile));
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
/**
|
|
92
91
|
* 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 `
|
|
92
|
+
* shell history): it is prompted for masked, or read from `CORTEX_KEY`
|
|
94
93
|
* when there is no terminal to ask.
|
|
95
94
|
*/
|
|
96
|
-
async function login(
|
|
95
|
+
async function login(given: string | undefined, name: string) {
|
|
97
96
|
const url = requireTls(
|
|
98
|
-
(
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
(given ?? (await prompts.askValue("Cortex URL", "https://cortex.example.com"))).replace(
|
|
98
|
+
/\/+$/,
|
|
99
|
+
"",
|
|
100
|
+
),
|
|
101
101
|
);
|
|
102
102
|
const key = process.stdin.isTTY
|
|
103
103
|
? await prompts.askSecret("Management key")
|
|
104
|
-
: (process.env["
|
|
104
|
+
: (process.env["CORTEX_KEY"] ??
|
|
105
105
|
report.fail({
|
|
106
106
|
what: "No terminal to ask for the management key",
|
|
107
|
-
blocks: ["Set
|
|
107
|
+
blocks: ["Set CORTEX_KEY in the environment and run login again."],
|
|
108
108
|
}));
|
|
109
109
|
const me = await whoami({ url, key });
|
|
110
110
|
if (!me) {
|
|
111
111
|
report.fail({
|
|
112
|
-
what: "
|
|
112
|
+
what: "Cortex rejected that key",
|
|
113
113
|
blocks: [
|
|
114
114
|
"Mint one on your account page, or set BOOTSTRAP_MANAGEMENT_KEY on a headless deployment.",
|
|
115
115
|
],
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
-
withProfileFile(() => saveProfile(
|
|
119
|
-
report.header("cortex
|
|
120
|
-
report.input("Profile",
|
|
118
|
+
withProfileFile(() => saveProfile(name, { url, key }));
|
|
119
|
+
report.header("cortex login");
|
|
120
|
+
report.input("Profile", name);
|
|
121
121
|
report.input("URL", url);
|
|
122
122
|
report.input("Key", me);
|
|
123
123
|
report.footer(`saved to ${profilesPath()}`);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* The key
|
|
128
|
-
*
|
|
127
|
+
* The key Cortex recognized the bearer as, or null when it does not
|
|
128
|
+
* authenticate. A Cortex that cannot be reached, or answers with
|
|
129
129
|
* anything but 200/401, is the run's outcome, not a CLI bug.
|
|
130
130
|
*/
|
|
131
131
|
async function whoami(profile: { url: string; key: string }) {
|
|
@@ -145,7 +145,7 @@ async function whoami(profile: { url: string; key: string }) {
|
|
|
145
145
|
report.fail({
|
|
146
146
|
what: `Could not reach ${profile.url}`,
|
|
147
147
|
error,
|
|
148
|
-
blocks: ["Check the URL and that
|
|
148
|
+
blocks: ["Check the URL and that Cortex is up."],
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
}
|
|
@@ -158,7 +158,7 @@ function withProfileFile<T>(operation: () => T) {
|
|
|
158
158
|
if (error instanceof ProfileFileError) {
|
|
159
159
|
report.fail({
|
|
160
160
|
what: error.message,
|
|
161
|
-
blocks: ["Fix or delete the file, then log in again:", " cortex
|
|
161
|
+
blocks: ["Fix or delete the file, then log in again:", " cortex login"],
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
throw error;
|
|
@@ -176,15 +176,17 @@ function requireTls(url: string) {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
/** The named profile, or the current one; a missing or plain-http one ends the run with how to fix it. */
|
|
180
|
+
export function requireProfile(name: string | undefined) {
|
|
180
181
|
const profile = withProfileFile(() => resolveProfile(name));
|
|
181
182
|
if (!profile) {
|
|
182
183
|
report.fail({
|
|
183
|
-
what: "No
|
|
184
|
+
what: "No Cortex to talk to",
|
|
184
185
|
blocks: [
|
|
185
186
|
"Log in once:",
|
|
186
|
-
" cortex
|
|
187
|
-
"or set
|
|
187
|
+
" cortex login",
|
|
188
|
+
"or set CORTEX_URL and CORTEX_KEY in the environment.",
|
|
189
|
+
"`cortex --help` lists the commands that need neither.",
|
|
188
190
|
],
|
|
189
191
|
});
|
|
190
192
|
}
|
|
@@ -194,11 +196,9 @@ function requireProfile(name: string | undefined) {
|
|
|
194
196
|
|
|
195
197
|
type ResolvedProfile = NonNullable<ReturnType<typeof resolveProfile>>;
|
|
196
198
|
|
|
197
|
-
type CcGroup = Command<[], { profile?: string; output: "table" | "json" }>;
|
|
198
|
-
|
|
199
199
|
type Operation = { path: string; method: "get" | "post"; operation: OpenApiOperation };
|
|
200
200
|
|
|
201
|
-
async function buildResourceCommands(
|
|
201
|
+
async function buildResourceCommands(program: Program, profile: ResolvedProfile) {
|
|
202
202
|
const loaded = await loadOpenApi(profile.url).catch((error: unknown) =>
|
|
203
203
|
report.fail({
|
|
204
204
|
what: `Could not read the management API at ${profile.url}`,
|
|
@@ -220,9 +220,12 @@ async function buildResourceCommands(cc: CcGroup, profile: ResolvedProfile) {
|
|
|
220
220
|
[...byRouter.values()].flat().map((o) => [o.operation.operationId, o]),
|
|
221
221
|
);
|
|
222
222
|
for (const [router, operations] of byRouter) {
|
|
223
|
-
const group =
|
|
223
|
+
const group = program
|
|
224
|
+
.command(router)
|
|
225
|
+
.description(`${router} procedures`)
|
|
226
|
+
.helpCommand(false);
|
|
224
227
|
for (const operation of operations) {
|
|
225
|
-
registerOperation({
|
|
228
|
+
registerOperation({ group, router, operation, byProcedure, document, profile });
|
|
226
229
|
}
|
|
227
230
|
}
|
|
228
231
|
}
|
|
@@ -248,7 +251,6 @@ function groupByRouter(document: OpenApiDocument) {
|
|
|
248
251
|
* on writes to a publishable router.
|
|
249
252
|
*/
|
|
250
253
|
function registerOperation(scope: {
|
|
251
|
-
cc: CcGroup;
|
|
252
254
|
group: Command;
|
|
253
255
|
router: string;
|
|
254
256
|
operation: Operation;
|
|
@@ -256,7 +258,7 @@ function registerOperation(scope: {
|
|
|
256
258
|
document: OpenApiDocument;
|
|
257
259
|
profile: ResolvedProfile;
|
|
258
260
|
}) {
|
|
259
|
-
const {
|
|
261
|
+
const { group, router, byProcedure, document, profile } = scope;
|
|
260
262
|
const { path, method, operation } = scope.operation;
|
|
261
263
|
const name = operation.operationId.split(".")[1] ?? path;
|
|
262
264
|
const input = inputSchemaOf(operation);
|
|
@@ -266,7 +268,7 @@ function registerOperation(scope: {
|
|
|
266
268
|
// (`x-publish-procedure`); a create that leaves the publication content
|
|
267
269
|
// blank carries none, since publishing it would only fail. Fields the
|
|
268
270
|
// document marks live (`x-live-fields`) are reported as such after the
|
|
269
|
-
// write
|
|
271
|
+
// write, apart from the draft.
|
|
270
272
|
const publishWith = operation["x-publish-procedure"];
|
|
271
273
|
const publisher = publishWith === undefined ? undefined : byProcedure.get(publishWith);
|
|
272
274
|
const canPublish = publisher !== undefined;
|
|
@@ -279,6 +281,7 @@ function registerOperation(scope: {
|
|
|
279
281
|
|
|
280
282
|
command.action(async function (...args) {
|
|
281
283
|
const options = command.opts() as Record<string, unknown> & {
|
|
284
|
+
output: "table" | "json";
|
|
282
285
|
json?: string;
|
|
283
286
|
all?: boolean;
|
|
284
287
|
publish?: boolean;
|
|
@@ -286,14 +289,13 @@ function registerOperation(scope: {
|
|
|
286
289
|
// Commander passes the options object first when the command declares
|
|
287
290
|
// no argument, so only a string is the `[id]` positional.
|
|
288
291
|
const positional = typeof args[0] === "string" ? args[0] : undefined;
|
|
289
|
-
const { json, all, publish, ...given } = options;
|
|
292
|
+
const { output: format, json, all, publish, ...given } = options;
|
|
290
293
|
const body = await collectInput(flags, given, json, positional, name);
|
|
291
|
-
const format = cc.opts().output;
|
|
292
294
|
// `-o json` promises stdout holds only the result; prose moves to stderr.
|
|
293
295
|
const note = format === "json" ? report.aside : report.note;
|
|
294
296
|
if (format === "table") {
|
|
295
|
-
report.header(`cortex
|
|
296
|
-
report.input("
|
|
297
|
+
report.header(`cortex ${router} ${name}`);
|
|
298
|
+
report.input("Cortex", profile.url);
|
|
297
299
|
}
|
|
298
300
|
const result = await callOrFail(profile, () =>
|
|
299
301
|
all
|
|
@@ -320,9 +322,26 @@ function declareCommand(
|
|
|
320
322
|
flags: Flag[],
|
|
321
323
|
shape: { paginated: boolean; canPublish: boolean },
|
|
322
324
|
) {
|
|
323
|
-
const command = group
|
|
325
|
+
const command = group
|
|
326
|
+
.command(name)
|
|
327
|
+
.description(summary)
|
|
328
|
+
.addOption(
|
|
329
|
+
new Option("-o, --output <format>", "Output format")
|
|
330
|
+
.choices(["table", "json"])
|
|
331
|
+
.default("table"),
|
|
332
|
+
);
|
|
333
|
+
// Optional to Commander even when the input requires it: `--json` may hold
|
|
334
|
+
// it, and collecting the input asks for, or names, a required field left
|
|
335
|
+
// out. So the help says which it is.
|
|
324
336
|
const idFlag = flags.find((flag) => flag.name === "id");
|
|
325
|
-
if (idFlag)
|
|
337
|
+
if (idFlag) {
|
|
338
|
+
command.argument(
|
|
339
|
+
"[id]",
|
|
340
|
+
idFlag.required
|
|
341
|
+
? "Required, here or in --json, or asked for on a terminal"
|
|
342
|
+
: "Optional",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
326
345
|
// A sensitive field never becomes a flag: argv is visible to every
|
|
327
346
|
// process on the machine and lands in shell history. It is prompted for
|
|
328
347
|
// masked on a terminal, or supplied through `--json -` from stdin.
|
|
@@ -339,7 +358,7 @@ function declareCommand(
|
|
|
339
358
|
|
|
340
359
|
/**
|
|
341
360
|
* A write to a publishable router is a draft: publish it now, or say how to.
|
|
342
|
-
* Fields
|
|
361
|
+
* Fields Cortex reads live are already in effect and are named as such,
|
|
343
362
|
* since "draft" would understate what just happened.
|
|
344
363
|
*/
|
|
345
364
|
async function publishAfterWrite(
|
|
@@ -351,11 +370,11 @@ async function publishAfterWrite(
|
|
|
351
370
|
) {
|
|
352
371
|
const id = (result as { id?: string } | null)?.id;
|
|
353
372
|
if (!id) return;
|
|
354
|
-
const command = `cortex
|
|
373
|
+
const command = `cortex ${publisher.operation.operationId.replace(".", " ")} ${id}`;
|
|
355
374
|
const live = options.liveFields.length > 0;
|
|
356
375
|
if (live) {
|
|
357
376
|
options.note(
|
|
358
|
-
`${options.liveFields.join(", ")} ${options.liveFields.length === 1 ? "is" : "are"} live:
|
|
377
|
+
`${options.liveFields.join(", ")} ${options.liveFields.length === 1 ? "is" : "are"} live: agents use them from their next turn, no publish needed.`,
|
|
359
378
|
);
|
|
360
379
|
}
|
|
361
380
|
if (publish) {
|
|
@@ -370,32 +389,70 @@ async function publishAfterWrite(
|
|
|
370
389
|
}
|
|
371
390
|
}
|
|
372
391
|
|
|
373
|
-
/**
|
|
374
|
-
|
|
375
|
-
* run's outcome, in the error grammar; anything else is a bug.
|
|
376
|
-
*/
|
|
377
|
-
async function callOrFail<T>(profile: ResolvedProfile, call: () => Promise<T>) {
|
|
392
|
+
/** `call`'s result, or the run ended by `failCall`. */
|
|
393
|
+
export async function callOrFail<T>(profile: ResolvedProfile, call: () => Promise<T>) {
|
|
378
394
|
try {
|
|
379
395
|
return await call();
|
|
380
396
|
} catch (error) {
|
|
381
|
-
|
|
382
|
-
report.fail({ what: `${error.status} ${error.code}: ${error.message}` });
|
|
383
|
-
}
|
|
384
|
-
if (error instanceof TypeError) {
|
|
385
|
-
report.fail({
|
|
386
|
-
what: `Could not reach ${profile.url}`,
|
|
387
|
-
error,
|
|
388
|
-
blocks: ["Check the URL and that the Control Center is up."],
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
throw error;
|
|
397
|
+
return failCall(profile, error);
|
|
392
398
|
}
|
|
393
399
|
}
|
|
394
400
|
|
|
401
|
+
/**
|
|
402
|
+
* A management refusal, or a Cortex that cannot be reached, is the
|
|
403
|
+
* run's outcome, in the error grammar, with whatever the refusal names;
|
|
404
|
+
* anything else is a bug, and rethrown.
|
|
405
|
+
*/
|
|
406
|
+
export function failCall(profile: { url: string }, error: unknown): never {
|
|
407
|
+
if (error instanceof ManagementError) {
|
|
408
|
+
report.fail({
|
|
409
|
+
what: `${error.status} ${error.code}: ${error.message}`,
|
|
410
|
+
blocks: refusalBlocks(error.detail ?? {}),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (error instanceof UnreachableError) {
|
|
414
|
+
report.fail({
|
|
415
|
+
what: `Could not reach ${profile.url}`,
|
|
416
|
+
error,
|
|
417
|
+
blocks: ["Check the URL and that Cortex is up."],
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* What a refusal names, a block each: the manifest entry it is about (its
|
|
425
|
+
* kind and name), each issue at its path in that entry, then every other
|
|
426
|
+
* field (who holds a name, what is missing) as `field: value`.
|
|
427
|
+
*/
|
|
428
|
+
function refusalBlocks({ error: _literal, kind, name, issues, ...rest }: Record<string, unknown>) {
|
|
429
|
+
const entry = typeof kind === "string" && typeof name === "string";
|
|
430
|
+
const fields = {
|
|
431
|
+
...(entry ? {} : { kind, name }),
|
|
432
|
+
...(Array.isArray(issues) ? {} : { issues }),
|
|
433
|
+
...rest,
|
|
434
|
+
};
|
|
435
|
+
return [
|
|
436
|
+
entry ? `${kind} ${JSON.stringify(name)}` : "",
|
|
437
|
+
Array.isArray(issues) ? issues.map(issueLine).join("\n") : "",
|
|
438
|
+
Object.entries(fields)
|
|
439
|
+
.filter(([, value]) => value !== undefined)
|
|
440
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
|
441
|
+
.join("\n"),
|
|
442
|
+
].filter((block) => block !== "");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** One issue at its path in the entry, or at the entry itself when the path is empty. */
|
|
446
|
+
function issueLine(issue: unknown) {
|
|
447
|
+
if (!isRecord(issue)) return JSON.stringify(issue);
|
|
448
|
+
const path = issue["path"];
|
|
449
|
+
return `${typeof path === "string" && path !== "" ? path : "(entry)"}: ${String(issue["message"])}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
395
452
|
/**
|
|
396
453
|
* The body: `--json` first, flags layered over it, the positional as `id`.
|
|
397
|
-
* A required field
|
|
398
|
-
* flag to add otherwise.
|
|
454
|
+
* A required field left missing is asked for on a TTY and named as the
|
|
455
|
+
* flag, or the positional, to add otherwise.
|
|
399
456
|
*/
|
|
400
457
|
async function collectInput(
|
|
401
458
|
flags: Flag[],
|
|
@@ -413,7 +470,11 @@ async function collectInput(
|
|
|
413
470
|
const missing = flags.filter((flag) => flag.required && body.get(flag.name) === undefined);
|
|
414
471
|
if (missing.length > 0 && !process.stdin.isTTY) {
|
|
415
472
|
const named = (flag: Flag) =>
|
|
416
|
-
flag.
|
|
473
|
+
flag.name === "id"
|
|
474
|
+
? "<id>"
|
|
475
|
+
: flag.sensitive
|
|
476
|
+
? `${flag.name} (via --json -)`
|
|
477
|
+
: `--${flag.name}`;
|
|
417
478
|
report.fail({
|
|
418
479
|
what: `${verb} needs ${named(missing[0]!)}`,
|
|
419
480
|
blocks: [
|
|
@@ -455,7 +516,7 @@ function readJsonBody(source: string) {
|
|
|
455
516
|
if (source === "-" && process.stdin.isTTY) {
|
|
456
517
|
report.fail({
|
|
457
518
|
what: "--json - reads the body from stdin, but nothing is piped in",
|
|
458
|
-
blocks: ["Pipe a JSON document:", " cat body.json | cortex
|
|
519
|
+
blocks: ["Pipe a JSON document:", " cat body.json | cortex ... --json -"],
|
|
459
520
|
});
|
|
460
521
|
}
|
|
461
522
|
const label = source === "-" ? "stdin" : source;
|
|
@@ -477,7 +538,7 @@ function readJsonBody(source: string) {
|
|
|
477
538
|
return parsed;
|
|
478
539
|
}
|
|
479
540
|
|
|
480
|
-
async function walkPages(
|
|
541
|
+
export async function walkPages(
|
|
481
542
|
profile: ResolvedProfile,
|
|
482
543
|
method: "get" | "post",
|
|
483
544
|
path: string,
|
|
@@ -488,8 +549,8 @@ async function walkPages(
|
|
|
488
549
|
const items = new Array<unknown>();
|
|
489
550
|
let count = 0;
|
|
490
551
|
let filteredCount = 0;
|
|
491
|
-
// Bounded:
|
|
492
|
-
// and a listing the bound cannot cover is an error, never a short result.
|
|
552
|
+
// Bounded: if Cortex reports more rows than it serves, the walk must not
|
|
553
|
+
// spin, and a listing the bound cannot cover is an error, never a short result.
|
|
493
554
|
for (let pageIndex = 0; pageIndex < MAX_PAGES; pageIndex += 1) {
|
|
494
555
|
const page = (await callManagement(profile, method, path, {
|
|
495
556
|
...given,
|