@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.
Files changed (76) hide show
  1. package/README.md +193 -128
  2. package/package.json +16 -5
  3. package/src/cli.ts +19 -12
  4. package/src/commands/apply.ts +186 -0
  5. package/src/commands/{cc.ts → management.ts} +173 -112
  6. package/src/commands/new.ts +83 -84
  7. package/src/commands/setup.ts +610 -0
  8. package/src/contracts/charts/chart-spec.ts +1 -1
  9. package/src/contracts/client-tools/index.ts +3 -3
  10. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  11. package/src/contracts/datasets/index.ts +3 -0
  12. package/src/contracts/egress-policy/index.ts +16 -0
  13. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  14. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  15. package/src/contracts/errors/index.ts +38 -0
  16. package/src/contracts/json/index.ts +5 -0
  17. package/src/contracts/manifest/index.ts +973 -0
  18. package/src/contracts/manifest-file/index.ts +16 -0
  19. package/src/contracts/markdown/index.ts +1 -1
  20. package/src/contracts/pattern/index.ts +276 -0
  21. package/src/contracts/server/index.ts +349 -0
  22. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  23. package/src/contracts/triggers/index.ts +112 -0
  24. package/src/contracts/wire/index.ts +67 -33
  25. package/src/{cc → management}/client.ts +33 -7
  26. package/src/{cc → management}/flags.ts +2 -1
  27. package/src/{cc → management}/profiles.ts +10 -8
  28. package/src/scaffold/files.ts +448 -284
  29. package/src/scaffold/ports.ts +12 -24
  30. package/src/scaffold/templates/cspell.json.txt +41 -0
  31. package/src/scaffold/templates/datasets.ts.txt +52 -0
  32. package/src/scaffold/templates/dockerignore.txt +6 -0
  33. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  34. package/src/scaffold/templates/gitignore.txt +1 -1
  35. package/src/scaffold/templates/server.ts.txt +40 -0
  36. package/src/scaffold/templates/setup.ts.txt +44 -0
  37. package/src/ui/prompts.ts +24 -38
  38. package/src/ui/report.ts +5 -15
  39. package/tsconfig.json +2 -3
  40. package/src/commands/graph.ts +0 -228
  41. package/src/commands/swagger.ts +0 -247
  42. package/src/config/load.ts +0 -122
  43. package/src/config/validate.ts +0 -46
  44. package/src/contracts/credentials/index.ts +0 -183
  45. package/src/contracts/egress/index.ts +0 -120
  46. package/src/contracts/graph/clients/embed.ts +0 -62
  47. package/src/contracts/graph/clients/neo4j.ts +0 -85
  48. package/src/contracts/graph/helpers.ts +0 -55
  49. package/src/contracts/graph/index.ts +0 -30
  50. package/src/contracts/graph/schema.ts +0 -65
  51. package/src/contracts/graph/types.ts +0 -125
  52. package/src/contracts/rich-text/index.ts +0 -220
  53. package/src/contracts/runtime/api.ts +0 -345
  54. package/src/contracts/runtime/auth.ts +0 -41
  55. package/src/contracts/runtime/grammar.ts +0 -169
  56. package/src/contracts/runtime/index.ts +0 -15
  57. package/src/contracts/runtime/models.ts +0 -50
  58. package/src/contracts/runtime/signature.ts +0 -255
  59. package/src/contracts/runtime/sync.ts +0 -196
  60. package/src/contracts/runtime/triggers.ts +0 -267
  61. package/src/graph/expand-domains.ts +0 -286
  62. package/src/graph/generate-cypher.ts +0 -201
  63. package/src/graph/seed.ts +0 -199
  64. package/src/graph/validate.ts +0 -78
  65. package/src/scaffold/features.ts +0 -190
  66. package/src/scaffold/templates/domains-README.md.txt +0 -16
  67. package/src/scaffold/templates/index.ts.txt +0 -18
  68. package/src/swagger/extract-endpoints.ts +0 -269
  69. package/src/swagger/generated-block.ts +0 -74
  70. package/src/swagger/openapi-schema.ts +0 -168
  71. package/src/swagger/parse-response.ts +0 -88
  72. package/src/swagger/types.ts +0 -25
  73. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  74. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  75. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  76. /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
+ }