@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { Command } from "@commander-js/extra-typings";
|
|
5
|
+
import { appliedManifestSchema, manifestChangesSchema } from "@cortex/contracts/manifest";
|
|
6
|
+
import { loadManifestFile } from "@cortex/contracts/manifest-file";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import { callManagement } from "@/management/client";
|
|
10
|
+
import { callOrFail, requireProfile } from "@/commands/management";
|
|
11
|
+
import * as report from "@/ui/report";
|
|
12
|
+
|
|
13
|
+
// Discovery is this exact name in the cwd, with no upward walk, so the
|
|
14
|
+
// argument's default is the discovery rule.
|
|
15
|
+
export const DEFAULT_MANIFEST = "./cortex.manifest.ts";
|
|
16
|
+
|
|
17
|
+
const CHANGES = ["create", "update", "delete"] as const;
|
|
18
|
+
const MARKS = { create: "+", update: "~", delete: "-" } satisfies Record<
|
|
19
|
+
(typeof CHANGES)[number],
|
|
20
|
+
string
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `cortex apply [path]`: makes Cortex match a manifest in one
|
|
25
|
+
* transaction, through the management API with the profile's key.
|
|
26
|
+
* `--dry-run` answers what would change and changes nothing.
|
|
27
|
+
*/
|
|
28
|
+
export function registerApply(program: Command<[], { verbose?: true; profile?: string }>) {
|
|
29
|
+
const command = program
|
|
30
|
+
.command("apply")
|
|
31
|
+
.helpGroup("Cortex:")
|
|
32
|
+
.description("Make Cortex match a manifest")
|
|
33
|
+
.argument("[path]", "The manifest, TypeScript or JSON", DEFAULT_MANIFEST)
|
|
34
|
+
.option("--dry-run", "Show what would change, and change nothing");
|
|
35
|
+
|
|
36
|
+
command.action(async function (path, options) {
|
|
37
|
+
const { verbose = false, profile: profileName } = program.opts();
|
|
38
|
+
const dryRun = options.dryRun === true;
|
|
39
|
+
const file = findManifest(path);
|
|
40
|
+
const profile = requireProfile(profileName);
|
|
41
|
+
|
|
42
|
+
report.header(dryRun ? "cortex apply --dry-run" : "cortex apply");
|
|
43
|
+
report.input("Manifest", label(file));
|
|
44
|
+
report.input("Cortex", profile.url);
|
|
45
|
+
|
|
46
|
+
const { changes, triggerSecrets } = await applyManifest({ profile, file, dryRun, verbose });
|
|
47
|
+
const created = countNames(changes.create);
|
|
48
|
+
const updated = countNames(changes.update);
|
|
49
|
+
const deleted = countNames(changes.delete);
|
|
50
|
+
report.footer(
|
|
51
|
+
created + updated + deleted === 0
|
|
52
|
+
? "no changes"
|
|
53
|
+
: dryRun
|
|
54
|
+
? `would create ${created}, update ${updated}, delete ${deleted}`
|
|
55
|
+
: `created ${created}, updated ${updated}, deleted ${deleted}`,
|
|
56
|
+
);
|
|
57
|
+
printTriggerSecrets(triggerSecrets);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Loads the manifest at `file` and sends it, a preview under `dryRun`, then
|
|
63
|
+
* prints its changes one row per entry. Ends the run on an invalid file, a
|
|
64
|
+
* refusal, or a summary this CLI cannot read. `cortex apply` and `cortex
|
|
65
|
+
* setup` both apply through it; each prints the trigger secrets it returns.
|
|
66
|
+
*/
|
|
67
|
+
export async function applyManifest(options: {
|
|
68
|
+
profile: ReturnType<typeof requireProfile>;
|
|
69
|
+
file: string;
|
|
70
|
+
dryRun: boolean;
|
|
71
|
+
verbose: boolean;
|
|
72
|
+
}) {
|
|
73
|
+
const { profile, file, dryRun, verbose } = options;
|
|
74
|
+
const manifest = await loadManifest(file, verbose);
|
|
75
|
+
const answer = await callOrFail(profile, () =>
|
|
76
|
+
callManagement(
|
|
77
|
+
profile,
|
|
78
|
+
"post",
|
|
79
|
+
dryRun ? "/manifests/preview" : "/manifests/sync",
|
|
80
|
+
manifest,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
// The secrets are read apart from the changes: a sync has committed by
|
|
84
|
+
// now, and a summary this CLI is too old to read must not lose them.
|
|
85
|
+
const { triggerSecrets = {} } = appliedManifestSchema
|
|
86
|
+
.pick({ triggerSecrets: true })
|
|
87
|
+
.partial()
|
|
88
|
+
.parse(answer);
|
|
89
|
+
const applied = manifestChangesSchema.safeParse(answer);
|
|
90
|
+
if (!applied.success) {
|
|
91
|
+
printTriggerSecrets(triggerSecrets);
|
|
92
|
+
report.fail({
|
|
93
|
+
what: `${profile.url} answered with a summary this CLI cannot read`,
|
|
94
|
+
blocks: [
|
|
95
|
+
`The ${dryRun ? "preview" : "apply"} went through. Update the CLI to read its summary:`,
|
|
96
|
+
" bun add -d @m6d/cortex-cli@latest",
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
printChanges(applied.data);
|
|
101
|
+
return { changes: applied.data, triggerSecrets };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** How a manifest path prints: relative to the cwd, `./`-prefixed. */
|
|
105
|
+
function label(path: string) {
|
|
106
|
+
const relativePath = relative(process.cwd(), path);
|
|
107
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The manifest's absolute path; a missing file ends the run before anything prints. */
|
|
111
|
+
export function findManifest(path: string) {
|
|
112
|
+
const absolute = resolve(process.cwd(), path);
|
|
113
|
+
if (existsSync(absolute)) return absolute;
|
|
114
|
+
return report.fail({
|
|
115
|
+
what: `No manifest at ${path}`,
|
|
116
|
+
blocks:
|
|
117
|
+
path === DEFAULT_MANIFEST
|
|
118
|
+
? [
|
|
119
|
+
"cortex apply reads exactly ./cortex.manifest.ts in the directory you run in.\nIt does not search parent directories.",
|
|
120
|
+
"Run it from the project root, or point at the file:",
|
|
121
|
+
" cortex apply ./deploy/cortex.manifest.ts",
|
|
122
|
+
]
|
|
123
|
+
: ["The path is resolved from the directory you run in."],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The file's default export, checked against the manifest contract here so
|
|
129
|
+
* a mistake is named before anything is sent, then sent as written: Cortex
|
|
130
|
+
* fills its own defaults, which may be newer than this CLI's.
|
|
131
|
+
*/
|
|
132
|
+
async function loadManifest(path: string, verbose: boolean) {
|
|
133
|
+
const { written, parsed } = await loadManifestFile(path).catch((error: unknown) =>
|
|
134
|
+
report.fail({
|
|
135
|
+
what: `Could not load ${label(path)}`,
|
|
136
|
+
error,
|
|
137
|
+
blocks: [
|
|
138
|
+
"Importing the file has to run cleanly on its own: it default-exports the\nmanifest, and every package it imports has to be installed (`bun add -d\n@m6d/cortex` for `defineManifest`).",
|
|
139
|
+
],
|
|
140
|
+
verbose,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
if (!parsed.success) {
|
|
144
|
+
report.fail({
|
|
145
|
+
what: `${label(path)} is not a valid manifest`,
|
|
146
|
+
blocks: [z.prettifyError(parsed.error)],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return written;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One row per changed entry under its kind: `+` created, `~` updated, `-` deleted. */
|
|
153
|
+
function printChanges(changes: Record<(typeof CHANGES)[number], Record<string, string[]>>) {
|
|
154
|
+
const rows = new Map<string, string[]>();
|
|
155
|
+
for (const change of CHANGES) {
|
|
156
|
+
for (const [kind, names] of Object.entries(changes[change])) {
|
|
157
|
+
const marked = names.map((name) => `${MARKS[change]} ${name}`);
|
|
158
|
+
rows.set(kind, [...(rows.get(kind) ?? []), ...marked]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [kind, lines] of rows) {
|
|
162
|
+
for (const [index, line] of lines.entries()) {
|
|
163
|
+
report.row(index === 0 ? kind : "", line, 14);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function countNames(byKind: Record<string, string[]>) {
|
|
169
|
+
return Object.values(byKind).flat().length;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The fire secrets this apply minted, for each trigger it created or whose
|
|
174
|
+
* `fireCredential` it dropped; nothing shows them again.
|
|
175
|
+
*/
|
|
176
|
+
export function printTriggerSecrets(secrets: Record<string, string>) {
|
|
177
|
+
const entries = Object.entries(secrets);
|
|
178
|
+
if (entries.length === 0) return;
|
|
179
|
+
report.note(
|
|
180
|
+
[
|
|
181
|
+
"Trigger fire secrets, shown this once. Store them now: nothing shows them again,",
|
|
182
|
+
"and only rotating a trigger's secret gives it a new one.",
|
|
183
|
+
...entries.map(([name, secret]) => ` ${name} ${secret}`),
|
|
184
|
+
].join("\n"),
|
|
185
|
+
);
|
|
186
|
+
}
|