@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
@@ -1,17 +1,16 @@
1
1
  /**
2
- * Host ports for the services a generated project runs, chosen once at scaffold
3
- * time.
2
+ * Host ports for the Cortex stack a generated project runs, chosen once at
3
+ * scaffold time.
4
4
  *
5
- * The canonical ports — 5432, 6379, 9000/9001, 7474/7687 — are precisely the ones
6
- * a developer already running Postgres or Redis has bound, so a scaffold that
7
- * hardcoded them collides on its first `docker compose up -d`. Only the host side
8
- * moves: container ports stay canonical, so images, health checks and anything
9
- * inside the compose network are unaffected. The server's own port is not one of
10
- * these — it is the project's to choose, and stays the documented default.
5
+ * Only the app and the engine are published: the browser and this
6
+ * project's callers reach them, and nothing outside the stack reaches its
7
+ * databases. The backend listens on the third. Fixed ports would collide with
8
+ * the next project scaffolded on the same machine, so each project draws its
9
+ * own. Container ports stay canonical.
11
10
  *
12
11
  * Every generated file spells its ports `{{port.<name>}}` and gets them filled in
13
- * by `applyPorts`. That is the point of the token: compose and `.env` cannot
14
- * drift apart, because neither holds a number of its own.
12
+ * by `applyPorts`. That is the point of the token: compose and the README
13
+ * cannot drift apart, because neither holds a number of its own.
15
14
  */
16
15
 
17
16
  /** High, unprivileged, and clear of the ephemeral range macOS and Linux hand out. */
@@ -51,11 +50,7 @@ function allocate(taken: Set<number>) {
51
50
  return first ?? FIRST;
52
51
  }
53
52
 
54
- /**
55
- * Every port a generated project can need, allocated together so they are
56
- * distinct within the project. Features that were not selected simply never
57
- * mention theirs.
58
- */
53
+ /** Every port a generated project publishes, allocated together so they are distinct. */
59
54
  export function allocatePorts() {
60
55
  const taken = new Set<number>();
61
56
 
@@ -65,19 +60,12 @@ export function allocatePorts() {
65
60
  return port;
66
61
  }
67
62
 
68
- return {
69
- postgres: next(),
70
- redis: next(),
71
- minio: next(),
72
- minioConsole: next(),
73
- neo4j: next(),
74
- neo4jBolt: next(),
75
- };
63
+ return { cortex: next(), engine: next(), server: next() };
76
64
  }
77
65
 
78
66
  export type Ports = ReturnType<typeof allocatePorts>;
79
67
 
80
- /** Fills in `{{port.postgres}}` and friends. Applied to every file, once, at the end. */
68
+ /** Fills in every `{{port.<name>}}`. Applied to every file, once, at the end. */
81
69
  export function applyPorts(contents: string, ports: Ports) {
82
70
  return Object.entries(ports).reduce(
83
71
  (text, [name, port]) => text.replaceAll(`{{port.${name}}}`, String(port)),
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": "0.2",
3
+ "language": "en",
4
+ "words": ["mprocs"],
5
+ "ignorePaths": [
6
+ "node_modules/**",
7
+ "out/**",
8
+ "build/**",
9
+ "**/dist/**",
10
+ "coverage/**",
11
+ "*.tsbuildinfo",
12
+ "bun.lock",
13
+ "package-lock.json",
14
+ "pnpm-lock.yaml",
15
+ "yarn.lock",
16
+ "*.db",
17
+ "*.sqlite",
18
+ "scratch.*",
19
+ ".env*",
20
+ "**/*.min.js",
21
+ "**/*.min.css",
22
+ "**/db/migrations/**",
23
+ "**/i18n/**",
24
+ "docs",
25
+ ".claude",
26
+ ".angular",
27
+ ".vercel/**",
28
+ "vendor/**",
29
+ ".gitignore"
30
+ ],
31
+ "dictionaries": [
32
+ "typescript",
33
+ "node",
34
+ "npm",
35
+ "html",
36
+ "css",
37
+ "bash",
38
+ "companies",
39
+ "softwareTerms"
40
+ ]
41
+ }
@@ -0,0 +1,52 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { defineDatasetHost } from "@m6d/cortex";
3
+ import { drizzleDataset } from "@m6d/cortex/drizzle";
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+ import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
6
+
7
+ /*
8
+ * This backend as a dataset host: an `orders` table agents ask grouped
9
+ * questions of, such as revenue per product by month. It lives in memory and
10
+ * is seeded on every boot; point `db` and the table at your own database.
11
+ * The host answers whoever reaches its URL, so `resolveUser` is where yours
12
+ * verifies the token Cortex forwards, and `scoped` narrows rows to that user.
13
+ */
14
+
15
+ const orders = sqliteTable("orders", {
16
+ id: integer().primaryKey(),
17
+ product: text().notNull(),
18
+ quantity: integer().notNull(),
19
+ total: real().notNull(),
20
+ /** ISO text, which the dataset reads as a date. */
21
+ placedAt: text().notNull(),
22
+ });
23
+
24
+ const sqlite = new Database(":memory:");
25
+ sqlite.run(
26
+ "create table orders (id integer primary key, product text not null, quantity integer not null, total real not null, placed_at text not null)",
27
+ );
28
+ const db = drizzle({ client: sqlite, casing: "snake_case" });
29
+ db.insert(orders)
30
+ .values([
31
+ { product: "Notebook", quantity: 3, total: 12, placedAt: "2026-01-05" },
32
+ { product: "Pen", quantity: 10, total: 15, placedAt: "2026-01-19" },
33
+ { product: "Notebook", quantity: 1, total: 4, placedAt: "2026-02-02" },
34
+ { product: "Backpack", quantity: 1, total: 49, placedAt: "2026-02-14" },
35
+ { product: "Pen", quantity: 4, total: 6, placedAt: "2026-03-08" },
36
+ ])
37
+ .run();
38
+
39
+ export const datasets = defineDatasetHost({
40
+ expose: [
41
+ drizzleDataset({
42
+ name: "orders",
43
+ description: "One row per order: what was bought, how many, for how much, and when.",
44
+ table: orders,
45
+ dialect: "sqlite",
46
+ db,
47
+ types: { placedAt: "date" },
48
+ scoped: () => undefined,
49
+ }),
50
+ ],
51
+ resolveUser: () => ({}),
52
+ });
@@ -0,0 +1,6 @@
1
+ # .env never goes into an image: it holds this checkout's secrets. With
2
+ # .env.example left out too, setup.ts writes none during the image's own
3
+ # `bun install`.
4
+ .env*
5
+ node_modules/
6
+ .git/
@@ -1,19 +1,15 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
3
3
 
4
- // The single root everything else hangs off. Left to auto-detection, framework
5
- // heuristics mark far too much as reachable and unused exports stop being
6
- // reportable at all.
7
- "entry": ["index.ts"],
4
+ // The roots everything else hangs off: the backend, the manifest
5
+ // `cortex apply` loads, and the env setup `bun install` runs. Left to
6
+ // auto-detection, framework heuristics mark far too much as reachable and
7
+ // unused exports stop being reportable.
8
+ "entry": ["index.ts", "cortex.manifest.ts", "setup.ts"],
8
9
 
9
- // These are @m6d/cortex-server's peer dependencies. The server needs them
10
- // resolvable and it is this package.json's job to install them, but nothing
11
- // here imports them until you write the code that does — a tool schema in
12
- // zod, an extra route on `server.app`. A requirement that lives in another
13
- // package's `peerDependencies` is not something reachability analysis can
14
- // see, so it is stated here instead. Once your own code imports one, its
15
- // entry is doing nothing and can go.
16
- "ignoreDependencies": ["@tanstack/ai", "@tanstack/ai-openai", "hono", "jose", "openai", "zod"],
10
+ // The CLI is run as `cortex`, a binary nothing here imports, so
11
+ // reachability analysis cannot see that the project uses it.
12
+ "ignoreDependencies": ["@m6d/cortex-cli"],
17
13
 
18
14
  "duplicates": {
19
15
  // fallow's default is 0, which means "no limit": clone groups get printed
@@ -1,7 +1,7 @@
1
1
  # Dependencies
2
2
  node_modules/
3
3
 
4
- # Environment variables — .env.example is the committed one
4
+ # Environment variables: .env.example is the committed one
5
5
  .env
6
6
  .env.local
7
7
  .env.*.local
@@ -0,0 +1,40 @@
1
+ import { defineServer } from "@m6d/cortex";
2
+ import { z } from "zod";
3
+
4
+ /**
5
+ * This backend as a Cortex server: the variable its agents' prompts read,
6
+ * and the native tool it runs for them. Cortex presents CORTEX_SERVER_KEY as
7
+ * the bearer of every call, and nothing below runs for any other.
8
+ */
9
+ export const server = defineServer({
10
+ key: process.env["CORTEX_SERVER_KEY"]!,
11
+ variables: {
12
+ // Resolved once per thread; prompts read it as {{userName}}.
13
+ userName: {
14
+ description: "Display name of the signed-in user",
15
+ example: "Sara",
16
+ type: "string",
17
+ resolve: ({ user }) => {
18
+ const name = user?.claims["name"];
19
+ return typeof name === "string" ? name : "guest";
20
+ },
21
+ },
22
+ },
23
+ tools: {
24
+ rollDice: {
25
+ description: "Roll dice and report each face and the total.",
26
+ input: z.object({
27
+ count: z.number().int().min(1).max(10).describe("How many dice to roll"),
28
+ sides: z.number().int().min(2).max(100).default(6).describe("Faces per die"),
29
+ }),
30
+ readOnly: true,
31
+ run: ({ count, sides }) => {
32
+ const faces = Array.from(
33
+ { length: count },
34
+ () => 1 + Math.floor(Math.random() * sides),
35
+ );
36
+ return { faces, total: faces.reduce((sum, face) => sum + face, 0) };
37
+ },
38
+ },
39
+ },
40
+ });
@@ -0,0 +1,44 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3
+
4
+ /*
5
+ * Writes .env on every `bun install`: it starts from the committed
6
+ * .env.example, or from .env when there is one, and each value left blank
7
+ * that is listed below is generated here, the Compose project this
8
+ * checkout's stack runs as among them. A value already set is never
9
+ * changed, so a fresh clone gets a stack and secrets of its own, and a rerun
10
+ * changes nothing. Without either file, as in an image build, it writes
11
+ * nothing.
12
+ */
13
+
14
+ const token = () => randomBytes(32).toString("base64url");
15
+ const key = () => randomBytes(32).toString("base64");
16
+
17
+ /** The package name as Compose takes a project name: lowercase words and hyphens. */
18
+ const { name } = JSON.parse(readFileSync("package.json", "utf8")) as { name: string };
19
+ const project =
20
+ name
21
+ .replace(/[^a-z0-9]+/g, "-")
22
+ .slice(0, 40)
23
+ .replace(/^-+|-+$/g, "") || "cortex";
24
+
25
+ const GENERATED = [
26
+ ["COMPOSE_PROJECT_NAME", () => `${project}-${randomBytes(4).toString("hex")}`],
27
+ ["CORTEX_SERVER_KEY", token],
28
+ ["DEFAULT_ADMIN_PASSWORD", token],
29
+ // Cortex takes management keys only under this prefix. cspell:ignore mgmt
30
+ ["BOOTSTRAP_MANAGEMENT_KEY", () => `cortex_mgmt_${token()}`],
31
+ ["BETTER_AUTH_SECRET", key],
32
+ ["SECRETS_MASTER_KEY", key],
33
+ ] as const;
34
+
35
+ const source = existsSync(".env") ? ".env" : ".env.example";
36
+ if (existsSync(source)) {
37
+ // Only its owner reads a file of secrets, however it was first created.
38
+ if (source === ".env") chmodSync(".env", 0o600);
39
+ let text = readFileSync(source, "utf8");
40
+ for (const [variable, generate] of GENERATED) {
41
+ text = text.replace(new RegExp(`^${variable}=$`, "m"), `${variable}=${generate()}`);
42
+ }
43
+ writeFileSync(".env", text, { mode: 0o600 });
44
+ }
package/src/ui/prompts.ts CHANGED
@@ -8,20 +8,16 @@
8
8
 
9
9
  import {
10
10
  cancel,
11
+ confirm,
11
12
  intro,
12
13
  isCancel,
13
14
  log,
14
- multiselect,
15
15
  outro,
16
16
  password,
17
- select,
18
17
  spinner,
19
18
  text,
20
19
  } from "@clack/prompts";
21
20
 
22
- import { FEATURES, type Feature } from "@/scaffold/features";
23
- import type { Database } from "@/scaffold/files";
24
-
25
21
  export { intro, log, outro };
26
22
 
27
23
  async function orExit<T>(answer: Promise<T | symbol>) {
@@ -54,52 +50,42 @@ export function askName(check: (name: string) => string | undefined) {
54
50
  );
55
51
  }
56
52
 
57
- /**
58
- * The one structural pick: it changes a `cortex.config.ts` line and decides
59
- * whether the generated compose file has a database service at all.
60
- */
61
- export function askDatabase() {
62
- return orExit(
63
- select<Database>({
64
- message: "Database",
65
- initialValue: "postgres",
66
- options: [
67
- { value: "postgres", hint: "docker-compose.yml runs one for you" },
68
- { value: "mssql", hint: "normally an existing external instance" },
69
- ],
70
- }),
71
- );
72
- }
73
-
74
- /** One multi-select for the whole catalogue — minus anything a flag settled. */
75
- export function askFeatures(undecided: Feature[]) {
76
- return orExit(
77
- multiselect<Feature>({
78
- message: "Which optional features?",
79
- required: false,
80
- options: undecided.map((feature) => ({ value: feature, hint: FEATURES[feature].hint })),
81
- }),
82
- );
53
+ /** A yes-or-no question a flag left open, answered no unless the caller says otherwise. */
54
+ export function askConfirm(message: string, initialValue = false) {
55
+ return orExit(confirm({ message, initialValue }));
83
56
  }
84
57
 
85
- /** A required value a command was not given: asked by name, re-asked while empty. */
86
- export function askValue(name: string, hint?: string) {
58
+ /**
59
+ * A required value a command was not given: asked by name, re-asked while
60
+ * empty or while `check` returns a complaint about the trimmed value.
61
+ */
62
+ export function askValue(
63
+ name: string,
64
+ hint?: string,
65
+ check: (value: string) => string | undefined = () => undefined,
66
+ ) {
87
67
  return orExit(
88
68
  text({
89
69
  message: name,
90
70
  placeholder: hint,
91
- validate: (value) =>
92
- (value ?? "").trim().length === 0 ? `${name} is required.` : undefined,
71
+ validate: function (value) {
72
+ const candidate = (value ?? "").trim();
73
+ return candidate.length === 0 ? `${name} is required.` : check(candidate);
74
+ },
93
75
  }),
94
76
  );
95
77
  }
96
78
 
97
- /** A credential: typed masked, never echoed. */
98
- export function askSecret(name: string) {
79
+ /** A credential: typed masked, never echoed, and re-asked while `check` complains. */
80
+ export function askSecret(
81
+ name: string,
82
+ check: (value: string) => string | undefined = () => undefined,
83
+ ) {
99
84
  return orExit(
100
85
  password({
101
86
  message: name,
102
- validate: (value) => ((value ?? "").length === 0 ? `${name} is required.` : undefined),
87
+ validate: (value) =>
88
+ (value ?? "").length === 0 ? `${name} is required.` : check(value ?? ""),
103
89
  }),
104
90
  );
105
91
  }
package/src/ui/report.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The output grammar, shared by every command that runs against a project.
2
+ * The output grammar, shared by every command.
3
3
  * The report layout is deliberately frozen; this file is the only
4
4
  * place that knows the box characters.
5
5
  */
@@ -31,12 +31,7 @@ function describe(error: unknown) {
31
31
  // anything opened the box above it.
32
32
  let open = false;
33
33
 
34
- /** `1 node` / `3 nodes` — every count the box prints goes through this. */
35
- export function plural(count: number, noun: string) {
36
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
37
- }
38
-
39
- /** `┌ cortex graph seed` — opens a run. */
34
+ /** `┌ cortex login` — opens a run. */
40
35
  export function header(title: string) {
41
36
  open = true;
42
37
  write(process.stdout, ["", `${pc.gray("┌")} ${title}`, BAR]);
@@ -50,20 +45,15 @@ export function input(label: string, value: string) {
50
45
  write(process.stdout, [`${pc.green("◇")} ${label.padEnd(9)} ${value}`]);
51
46
  }
52
47
 
53
- export function blank() {
54
- write(process.stdout, [BAR]);
55
- }
56
-
57
48
  /**
58
- * `│ updated leaves/listLeaves.endpoint.ts` — a row in the body. An empty
59
- * label indents to the same gutter, for the second line a finding gets. `width`
60
- * is the label column: findings fit in 10, domain names need 14.
49
+ * `│ name orders` — a row in the body. An empty label indents to the same
50
+ * gutter, for a continuation line. `width` is the label column.
61
51
  */
62
52
  export function row(label: string, text: string, width = 10) {
63
53
  write(process.stdout, [`${BAR} ${label.padEnd(width - 2)} ${text}`]);
64
54
  }
65
55
 
66
- /** `└ 87 nodes · 124 edges (6.2s)` — closes a run. */
56
+ /** `└ authenticated with ada@example.com` — closes a run. */
67
57
  export function footer(text: string) {
68
58
  write(process.stdout, [`${pc.gray("└")} ${text}`, ""]);
69
59
  }
package/tsconfig.json CHANGED
@@ -5,8 +5,7 @@
5
5
  // Environment setup & latest features
6
6
  "lib": ["ESNext"],
7
7
  // Named explicitly: auto-inclusion of @types/* is an editor-dependent
8
- // heuristic, and `Bun`/`Response` come from here. See @cortex/contracts'
9
- // tsconfig for the same fix.
8
+ // heuristic, and `Bun`/`Response` come from here.
10
9
  "types": ["bun"],
11
10
  "target": "ESNext",
12
11
  "module": "Preserve",
@@ -18,7 +17,7 @@
18
17
  "resolveJsonModule": true,
19
18
  "noEmit": true,
20
19
  "paths": {
21
- // The graph contract is @cortex/contracts, a private workspace package
20
+ // @cortex/contracts is a private workspace package
22
21
  // that never publishes, so the CLI carries its own copy: `prepack`
23
22
  // vendors it into src/contracts, where the first candidate finds it in
24
23
  // the published tarball. Inside the repo that copy doesn't exist and