@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
@@ -2,13 +2,14 @@ import { spawnSync } from "node:child_process";
2
2
  import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
 
5
- import { Option, type Command } from "@commander-js/extra-typings";
5
+ import type { Command } from "@commander-js/extra-typings";
6
+ import { CORTEX_MANIFEST_ID } from "@cortex/contracts/manifest";
6
7
 
7
8
  import pkg from "../../package.json";
8
- import { FEATURE_NAMES, FEATURES, type Feature } from "@/scaffold/features";
9
- import { scaffoldFiles, type Database } from "@/scaffold/files";
10
- import { allocatePorts } from "@/scaffold/ports";
11
- import { askDatabase, askFeatures, askName, intro, log, outro, runSteps } from "@/ui/prompts";
9
+ import { askProvider } from "@/commands/setup";
10
+ import { AGENT_SLUG, manifestIdOf, scaffoldFiles } from "@/scaffold/files";
11
+ import { allocatePorts, type Ports } from "@/scaffold/ports";
12
+ import { askConfirm, askName, intro, log, outro, runSteps } from "@/ui/prompts";
12
13
  import * as report from "@/ui/report";
13
14
 
14
15
  /** npm's rule for an unscoped package name. The name is the directory too. */
@@ -18,55 +19,32 @@ const MAX_NAME = 214;
18
19
  /** The two names npm rejects outright, however well-formed they otherwise are. */
19
20
  const RESERVED_NAMES = ["node_modules", "favicon.ico"];
20
21
 
21
- export function registerNew(program: Command) {
22
+ export function registerNew(program: Command<[], { verbose?: true }>) {
22
23
  const command = program
23
24
  .command("new")
24
25
  .helpGroup("Project:")
25
- .description("Scaffold a new Cortex server")
26
+ .description("Scaffold a backend, its agent, and Cortex to run it on")
26
27
  .argument("[name]", "Directory to create, and the generated package name")
27
- .addOption(
28
- new Option("--database <type>", "Database the project is configured for")
29
- .choices(["postgres", "mssql"] as const)
30
- .default("postgres" as const),
31
- )
32
- // Both halves of every toggle, so a flag can answer its question either
33
- // way and `--yes` is not the only way to say "no". Declaring the positive
34
- // first leaves the value `undefined` when neither is passed, which is
35
- // what marks the question as still unanswered.
36
- .option("--attachments", FEATURES.attachments.hint)
37
- .option("--no-attachments", "Leave attachments out")
38
- .option("--redis", FEATURES.redis.hint)
39
- .option("--no-redis", "Leave Redis out")
40
- .option("--auth", FEATURES.auth.hint)
41
- .option("--no-auth", "Leave auth out")
42
- .option("--knowledge", FEATURES.knowledge.hint)
43
- .option("--no-knowledge", "Leave the knowledge graph out")
44
- .option("--control-center", FEATURES.controlCenter.hint)
45
- .option("--no-control-center", "Leave the Control Center out")
28
+ .option("--datasets", "Serve an example table as a dataset host")
29
+ // Both halves of the toggle, so the flag can answer its question either
30
+ // way and `--yes` is not the only way to say "yes". Declaring the
31
+ // positive first leaves the value `undefined` when neither is passed,
32
+ // which is what marks the question as still unanswered.
33
+ .option("--setup", `Set up the ${AGENT_SLUG} agent on the first bun run dev`)
34
+ .option("--no-setup", "Leave Cortex empty; you register the backend yourself")
46
35
  .option("--yes", "Accept the defaults instead of asking");
47
36
 
48
37
  command.action(async function (name, options) {
49
- // The globals live on the root command, which reaches this file untyped —
50
- // `Command` here is the parameter type, not cli.ts's inferred one.
51
- const { verbose = false } = program.opts() as { verbose?: boolean };
52
-
53
- await scaffold(name, {
54
- ...options,
55
- verbose,
56
- databasePassed: command.getOptionValueSource("database") !== "default",
57
- });
38
+ const { verbose = false } = program.opts();
39
+
40
+ await scaffold(name, { ...options, verbose });
58
41
  });
59
42
  }
60
43
 
61
- type Options = {
62
- database: Database;
63
- yes?: boolean;
64
- verbose: boolean;
65
- databasePassed: boolean;
66
- } & Partial<Record<Feature, boolean>>;
44
+ type Options = { datasets?: true; setup?: boolean; yes?: true; verbose: boolean };
67
45
 
68
46
  async function scaffold(name: string | undefined, options: Options) {
69
- const { verbose, databasePassed } = options;
47
+ const { verbose } = options;
70
48
 
71
49
  // No TTY ⇒ never prompt and behave as `--yes`. On Bun a piped stdin reports
72
50
  // `undefined` rather than `false`, which is why this compares against `true`.
@@ -77,6 +55,7 @@ async function scaffold(name: string | undefined, options: Options) {
77
55
  // A name that came from a flag is checked before anything prints; one that
78
56
  // is prompted for is checked by the prompt, which can ask again.
79
57
  if (name !== undefined && !isValidName(name)) failInvalidName(name);
58
+ if (name !== undefined && manifestIdOf(name) === CORTEX_MANIFEST_ID) failCortexId(name);
80
59
  if (name !== undefined && isTaken(resolve(process.cwd(), name))) failTaken(name);
81
60
 
82
61
  const started = Date.now();
@@ -84,15 +63,15 @@ async function scaffold(name: string | undefined, options: Options) {
84
63
 
85
64
  const project = name ?? (await askName(checkName)).trim();
86
65
 
87
- const database = interactive && !databasePassed ? await askDatabase() : options.database;
88
- const features = await chooseFeatures(options, interactive);
66
+ const datasets = options.datasets === true;
67
+ const { setup, provider } = await askSetup(options.setup, interactive);
89
68
 
90
69
  const target = resolve(process.cwd(), project);
91
70
 
92
- // Allocated here rather than inside the generators, which stay pure: picking a
71
+ // Drawn here rather than inside the generators, which stay pure: picking a
93
72
  // port means probing the machine for a free one.
94
73
  const ports = allocatePorts();
95
- const files = scaffoldFiles(project, database, features, ports);
74
+ const files = scaffoldFiles({ name: project, datasets, setup, ports, env: provider?.env });
96
75
  let failedInstall: string | undefined;
97
76
 
98
77
  await runSteps([
@@ -102,11 +81,18 @@ async function scaffold(name: string | undefined, options: Options) {
102
81
  for (const [path, contents] of Object.entries(files)) {
103
82
  const file = join(target, path);
104
83
  mkdirSync(dirname(file), { recursive: true });
105
- writeFileSync(file, contents);
84
+ // .env holds the provider's key before setup.ts adds the rest.
85
+ writeFileSync(file, contents, { mode: path === ".env" ? 0o600 : 0o644 });
106
86
  }
107
87
 
108
- const summary = [database, ...features, "model"].join(" · ");
109
- return `Wrote ${Object.keys(files).length} files to ./${project}\n${summary}`;
88
+ const summary = [
89
+ "server",
90
+ ...(datasets ? ["dataset host"] : []),
91
+ "manifest",
92
+ "Cortex",
93
+ ...(setup ? ["first-run setup"] : []),
94
+ ];
95
+ return `Wrote ${Object.keys(files).length} files to ./${project}\n${summary.join(" · ")}`;
110
96
  },
111
97
  },
112
98
  {
@@ -152,7 +138,25 @@ async function scaffold(name: string | undefined, options: Options) {
152
138
  if (failedInstall !== undefined) warnInstall(project, failedInstall, verbose);
153
139
 
154
140
  outro(`Done in ${seconds(started)}`);
155
- process.stdout.write(nextSteps(project, features, files));
141
+ process.stdout.write(nextSteps(project, ports, setup, provider !== undefined));
142
+ }
143
+
144
+ /**
145
+ * Whether the first `bun run dev` sets up the agent, and the model provider
146
+ * it runs on. A flag settles the first question; otherwise it is asked, or
147
+ * under `--yes` answered yes. The provider has no default, so only a terminal
148
+ * asks for one; without it, setup registers the server and stops.
149
+ */
150
+ async function askSetup(flag: boolean | undefined, interactive: boolean) {
151
+ const setup =
152
+ flag ??
153
+ (!interactive ||
154
+ (await askConfirm(
155
+ `Set up the ${AGENT_SLUG} agent when you first run bun run dev?`,
156
+ true,
157
+ )));
158
+ const provider = setup && interactive ? await askProvider() : undefined;
159
+ return { setup, provider };
156
160
  }
157
161
 
158
162
  /**
@@ -172,39 +176,20 @@ function warnInstall(project: string, output: string, verbose: boolean) {
172
176
  );
173
177
  }
174
178
 
175
- function nextSteps(project: string, features: Feature[], files: ReturnType<typeof scaffoldFiles>) {
176
- const keys = [
177
- "CORTEX_DATABASE_URL",
178
- "CORTEX_MODEL_*",
179
- ...features.map((feature) => FEATURES[feature].secrets).filter((key) => key !== undefined),
180
- ];
181
-
182
- return (
183
- `\nNext steps:\n\n` +
184
- step(`cd ${project}`) +
185
- step("edit .env", `fill in ${keys.length === 2 ? keys.join(" and ") : keys.join(", ")}`) +
186
- (files["docker-compose.yml"] === undefined ? "" : step("docker compose up -d")) +
187
- // The `port` the generated cortex.config.ts sets, which is the server's
188
- // own default. Service ports move at scaffold time; this one does not.
189
- step("bun run dev", "→ http://localhost:3331") +
190
- `\n`
191
- );
192
- }
193
-
194
- /**
195
- * One multi-select for the whole catalogue, minus whatever the flags already
196
- * settled: a passed flag suppresses its question, the rest are still asked.
197
- */
198
- async function chooseFeatures(options: Options, interactive: boolean) {
199
- const chosen = new Set(FEATURE_NAMES.filter((feature) => options[feature] === true));
200
- const undecided = FEATURE_NAMES.filter((feature) => options[feature] === undefined);
201
-
202
- if (interactive && undecided.length > 0) {
203
- for (const feature of await askFeatures(undecided)) chosen.add(feature);
204
- }
205
-
206
- // Catalogue order, not the order the flags or the checkboxes came in.
207
- return FEATURE_NAMES.filter((feature) => chosen.has(feature));
179
+ /** What the first `bun run dev` sets up: the agent, the server alone (no model yet), or nothing. */
180
+ function nextSteps(project: string, ports: Ports, setup: boolean, provider: boolean) {
181
+ const runs = {
182
+ agent: step("bun run dev", `→ sets up ${AGENT_SLUG}, then prints where to chat`),
183
+ server:
184
+ step("bun run dev", "→ registers the server") +
185
+ step("bun cortex setup", "adds a model provider, then applies"),
186
+ none:
187
+ step("bun run dev", `→ Cortex at http://localhost:${ports.cortex}`) +
188
+ step("README.md", "register the server, then bun cortex apply"),
189
+ };
190
+ const run = setup ? (provider ? runs.agent : runs.server) : runs.none;
191
+
192
+ return `\nNext steps:\n\n${step(`cd ${project}`)}${run}\n`;
208
193
  }
209
194
 
210
195
  async function install(cwd: string) {
@@ -245,11 +230,14 @@ function isValidName(name: string) {
245
230
  return name.length <= MAX_NAME && VALID_NAME.test(name) && !RESERVED_NAMES.includes(name);
246
231
  }
247
232
 
248
- /** The prompt's rules, which are the flag path's two failures asked one at a time. */
233
+ /** The prompt's rules, which are the flag path's failures asked one at a time. */
249
234
  function checkName(candidate: string) {
250
235
  // One line each: clack prints them inside the prompt, which will not wrap.
251
236
  if (RESERVED_NAMES.includes(candidate)) return `npm does not allow the name ${candidate}.`;
252
237
  if (!isValidName(candidate)) return "Lowercase letters, digits, and . _ - only.";
238
+ if (manifestIdOf(candidate) === CORTEX_MANIFEST_ID) {
239
+ return `The manifest would take Cortex's own id, ${CORTEX_MANIFEST_ID}.`;
240
+ }
253
241
  if (isTaken(resolve(process.cwd(), candidate))) {
254
242
  return `./${candidate} already exists and is not an empty directory.`;
255
243
  }
@@ -307,6 +295,17 @@ function missingName(): never {
307
295
  });
308
296
  }
309
297
 
298
+ /** The manifest takes its id from the name, and Cortex refuses any manifest under its own. */
299
+ function failCortexId(name: string): never {
300
+ report.fail({
301
+ what: `"${name}" would give the manifest Cortex's own id, ${CORTEX_MANIFEST_ID}`,
302
+ blocks: [
303
+ "cortex.manifest.ts takes its id from the project name, and Cortex keeps that\nid for its own manifest. Pick another name:",
304
+ ` cortex new ${CORTEX_MANIFEST_ID}-app`,
305
+ ],
306
+ });
307
+ }
308
+
310
309
  function failTaken(name: string): never {
311
310
  report.fail({
312
311
  what: `./${name} already exists and is not an empty directory.`,