@m6d/cortex-cli 0.12.0 → 0.13.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 (74) hide show
  1. package/README.md +135 -130
  2. package/package.json +16 -5
  3. package/src/cli.ts +17 -12
  4. package/src/commands/apply.ts +169 -0
  5. package/src/commands/{cc.ts → management.ts} +150 -100
  6. package/src/commands/new.ts +50 -74
  7. package/src/contracts/charts/chart-spec.ts +1 -1
  8. package/src/contracts/client-tools/index.ts +3 -3
  9. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  10. package/src/contracts/datasets/index.ts +3 -0
  11. package/src/contracts/egress-policy/index.ts +16 -0
  12. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  13. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  14. package/src/contracts/errors/index.ts +38 -0
  15. package/src/contracts/json/index.ts +5 -0
  16. package/src/contracts/manifest/index.ts +932 -0
  17. package/src/contracts/manifest-file/index.ts +16 -0
  18. package/src/contracts/markdown/index.ts +1 -1
  19. package/src/contracts/pattern/index.ts +276 -0
  20. package/src/contracts/server/index.ts +247 -0
  21. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  22. package/src/contracts/triggers/index.ts +112 -0
  23. package/src/contracts/wire/index.ts +61 -32
  24. package/src/{cc → management}/client.ts +19 -7
  25. package/src/{cc → management}/flags.ts +2 -1
  26. package/src/{cc → management}/profiles.ts +5 -5
  27. package/src/scaffold/files.ts +362 -237
  28. package/src/scaffold/ports.ts +13 -24
  29. package/src/scaffold/templates/datasets.ts.txt +52 -0
  30. package/src/scaffold/templates/dockerignore.txt +7 -0
  31. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  32. package/src/scaffold/templates/gitignore.txt +2 -1
  33. package/src/scaffold/templates/server.ts.txt +40 -0
  34. package/src/scaffold/templates/setup.ts.txt +52 -0
  35. package/src/ui/prompts.ts +4 -31
  36. package/src/ui/report.ts +5 -15
  37. package/tsconfig.json +2 -3
  38. package/src/commands/graph.ts +0 -228
  39. package/src/commands/swagger.ts +0 -247
  40. package/src/config/load.ts +0 -122
  41. package/src/config/validate.ts +0 -46
  42. package/src/contracts/credentials/index.ts +0 -183
  43. package/src/contracts/egress/index.ts +0 -120
  44. package/src/contracts/graph/clients/embed.ts +0 -45
  45. package/src/contracts/graph/clients/neo4j.ts +0 -85
  46. package/src/contracts/graph/helpers.ts +0 -55
  47. package/src/contracts/graph/index.ts +0 -30
  48. package/src/contracts/graph/schema.ts +0 -65
  49. package/src/contracts/graph/types.ts +0 -125
  50. package/src/contracts/rich-text/index.ts +0 -220
  51. package/src/contracts/runtime/api.ts +0 -345
  52. package/src/contracts/runtime/auth.ts +0 -41
  53. package/src/contracts/runtime/grammar.ts +0 -169
  54. package/src/contracts/runtime/index.ts +0 -15
  55. package/src/contracts/runtime/models.ts +0 -50
  56. package/src/contracts/runtime/signature.ts +0 -255
  57. package/src/contracts/runtime/sync.ts +0 -196
  58. package/src/contracts/runtime/triggers.ts +0 -267
  59. package/src/graph/expand-domains.ts +0 -286
  60. package/src/graph/generate-cypher.ts +0 -201
  61. package/src/graph/seed.ts +0 -199
  62. package/src/graph/validate.ts +0 -78
  63. package/src/scaffold/features.ts +0 -190
  64. package/src/scaffold/templates/domains-README.md.txt +0 -16
  65. package/src/scaffold/templates/index.ts.txt +0 -18
  66. package/src/swagger/extract-endpoints.ts +0 -269
  67. package/src/swagger/generated-block.ts +0 -74
  68. package/src/swagger/openapi-schema.ts +0 -168
  69. package/src/swagger/parse-response.ts +0 -88
  70. package/src/swagger/types.ts +0 -25
  71. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  72. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  73. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  74. /package/src/{cc → management}/output.ts +0 -0
package/README.md CHANGED
@@ -1,16 +1,17 @@
1
1
  # @m6d/cortex-cli
2
2
 
3
- The `cortex` command line: scaffold Cortex servers, and run the generators that otherwise need a
4
- hand-written script.
3
+ The `cortex` command line: scaffold a Cortex project, and manage a Cortex deployment from the
4
+ terminal.
5
5
 
6
6
  ```sh
7
7
  bunx @m6d/cortex-cli new my-app # from nowhere — scaffolds a project
8
- bunx cortex swagger sync # inside a project — reads its cortex.config.ts
9
- bunx cortex graph seed
8
+ bunx cortex agents list # against the Cortex you logged in to
9
+ bunx cortex apply # make it match ./cortex.manifest.ts
10
10
  ```
11
11
 
12
- Commands are noun-first — `cortex <resource> <verb>` — except `new`, which acts on no existing
13
- resource. Run `cortex --help`, then `cortex <resource> --help`.
12
+ Commands are noun-first, `cortex <resource> <procedure>`, except `new`, which acts on no existing
13
+ resource, `apply`, which acts on a whole deployment, and `login`, `logout` and `status`, which act
14
+ on your profiles. Run `cortex --help`, then `cortex <resource> --help`.
14
15
 
15
16
  ```sh
16
17
  bun add -d @m6d/cortex-cli
@@ -21,155 +22,159 @@ you how to install Bun when it is genuinely absent.
21
22
 
22
23
  ## `cortex new`
23
24
 
24
- Scaffolds `./<name>`: a standalone project with `cortex.config.ts`, `index.ts`, `.env` and
25
- `.env.example`, inlined eslint/prettier/cspell/fallow config, and a `docker-compose.yml` when a
26
- selection contributes a service. It then runs `bun install`, and `git init` unless the target is
27
- already inside a work tree. An existing non-empty directory is an error.
25
+ Scaffolds `./<name>`: a backend for Cortex agents, and Cortex itself to run them on.
26
+
27
+ - `server.ts` is a Cortex server built with `defineServer` from `@m6d/cortex`: one variable
28
+ (`userName`) and one native tool (`rollDice`), mounted at `/cortex` by `index.ts`.
29
+ - `cortex.manifest.ts` declares a `helper` agent on that server, for `cortex apply`.
30
+ - `docker-compose.yml` runs Cortex's app, worker and engine on the one `syscodeia/cortex` image,
31
+ with Postgres, Redis and MinIO. Only the app and the engine publish host ports, picked at scaffold
32
+ time from what was free.
33
+ - `.env` holds the backend's own key and the checkout's Compose project name, and `cortex.env`
34
+ Cortex's secrets, which only Compose reads. Neither is committed: `setup.ts` writes both from
35
+ their `.example` copies on every `bun install`, generating each value left blank, so a fresh
36
+ clone gets its own secrets and a stack that never shares the original's volumes. `.dockerignore`
37
+ keeps all four out of images.
38
+ - `README.md` lists the steps: start the stack and the backend, register the server in the
39
+ console, then `cortex apply`.
40
+ - eslint, prettier, cspell, fallow and tsconfig are inlined, so `bun run verify` works standalone.
41
+
42
+ It then runs `bun install`, and `git init` unless the target is already inside a work tree. An
43
+ existing non-empty directory is an error. So is a name like `cortex` or `cortex.`, since the
44
+ manifest takes its id from the name and Cortex keeps the id `cortex` for its own.
28
45
 
29
46
  ```sh
30
- bunx @m6d/cortex-cli new widget-backend --knowledge --redis
47
+ bunx @m6d/cortex-cli new shop --datasets
31
48
  ```
32
49
 
33
- On a TTY it asks only what the flags left open — the name, the database, and one multi-select over
34
- the undecided features. With no TTY it never prompts and behaves as `--yes`, so a missing name is a
35
- hard error rather than a hang.
50
+ On a TTY it asks only what the flags left open: the name, and whether to add the dataset host. With
51
+ no TTY it never prompts and behaves as `--yes`, so a missing name is a hard error rather than a
52
+ hang.
36
53
 
37
- | Flag | Default | Writes |
38
- | ------------------------------------------ | ---------- | ------------------------------------------------------------------------- |
39
- | `--database <postgres\|mssql>` | `postgres` | The `database` block. `postgres` also gets a compose service; `mssql` not |
40
- | `--attachments` / `--no-attachments` | off | `storage` + `vision` — MinIO uploads, described by a vision model |
41
- | `--redis` / `--no-redis` | off | `redis` — resumable streams across restarts |
42
- | `--auth` / `--no-auth` | off | `auth` — JWT verification via JWKS |
43
- | `--knowledge` / `--no-knowledge` | off | `neo4j` + `embedding` + `knowledge`, plus a `src/domains/` tree |
44
- | `--control-center` / `--no-control-center` | off | `controlCenter` |
45
- | `--yes` | | Accept the defaults instead of asking |
54
+ | Flag | Default | Writes |
55
+ | ------------------------------ | ------- | ------------------------------------------------------------------------------------------- |
56
+ | `--datasets` / `--no-datasets` | off | `datasets.ts`, an in-memory `orders` table served as a dataset host, and its manifest entry |
57
+ | `--yes` | | Accept the defaults instead of asking |
46
58
 
47
- Unselected features are still written to `cortex.config.ts`, commented out, alongside `reranker`
48
- and `context` — the generated config documents what else exists. Every credential and
49
- URL is a placeholder in `.env`; none of them is prompted for.
59
+ The image runs at the Cortex release that was current when this CLI was published
60
+ (`CORTEX_VERSION` in the shell picks another); the backend depends on the `@m6d/cortex` release of
61
+ that time. A server name stops at Cortex's 100 characters, so a longer project name is
62
+ registered cut to that length.
50
63
 
51
- ## `cortex swagger sync` / `check`
64
+ ## Management commands
52
65
 
53
- Fetches every Swagger URL the config names — server-level and per-agent, deduped — and reconciles
54
- the `.endpoint.ts` files under the domains directory against it.
66
+ Manage a Cortex deployment from the terminal. The commands come from
67
+ Cortex's own management API (`/api/management/openapi.json`), fetched
68
+ on each run and cached, so the tree always matches the deployment you point
69
+ at and a new procedure needs no CLI release.
55
70
 
56
71
  ```sh
57
- bunx cortex swagger sync
58
- bunx cortex swagger check
59
- bunx cortex swagger sync --domains-dir ./server/src/domains
72
+ cortex login # asks for the URL and a management key
73
+ cortex status
74
+ cortex agents list
75
+ cortex agents create --slug support --name Support --serverId <uuid>
76
+ cortex tools create --json tool.json --publish
77
+ cortex tools list -o json | jq '.items[].name'
78
+ cortex logout
60
79
  ```
61
80
 
62
- `sync` rewrites each endpoint's params, body, and response schemas in place and prints only the
63
- files it changed; unchanged ones live in the footer count. `check` writes nothing, reports the drift
64
- it found, and exits 2 — which is what makes it a CI step.
81
+ Profiles live in `~/.cortex/profiles.json` (owner-only). `-p, --profile <name>`,
82
+ a global option, picks one for any command, and names the one `login` saves
83
+ and `logout` forgets (`default` otherwise); `CORTEX_URL` and
84
+ `CORTEX_KEY`, both set, win over the file for CI. `CORTEX_HOME` relocates
85
+ the directory.
65
86
 
66
- An endpoint file pointing at a route the spec no longer serves is `MISSING`. `sync` prints those
67
- last, so they are the thing you read, and still exits 0: writing is its job. `check` counts them as
68
- a finding, same as drift.
87
+ The grammar is `cortex <router> <procedure> [id] [flags]`, with routers as
88
+ the API names them (`datasetHosts`, `modelProviders`). Each top-level input
89
+ field is a flag typed from the API's schema; nested values take JSON, and
90
+ `--json <file|->` supplies a whole body that flags then override. A required
91
+ field left out, `id` included, is asked for on a terminal and named as the
92
+ missing flag or positional otherwise. Listings take `--pageIndex`, `--pageSize`, `--q`, and `--all` to
93
+ walk every page. `-o json` prints the raw result for pipes.
69
94
 
70
- `-d, --domains-dir <path>` is declared on the `swagger` group, so it works on either side of the
71
- verb. It defaults to `src/domains`, resolved relative to the config file.
95
+ Writes to agents, tools, services, skills, triggers, and dataset hosts save
96
+ drafts.
97
+ `--publish` on a write the API marks publishable (`update`, and a `create`
98
+ whose result is complete enough) runs that entity's publish procedure right
99
+ after; without it the command says so and names the command. Fields the API
100
+ marks live (`x-live-fields`) take effect without a publish, from agents'
101
+ next turn, and the command names them: a trigger's
102
+ `credentialName`, `fireCredentialName` and model, say, or a dataset host's
103
+ `url`, `credentials` and `timeoutMs`. `cortex datasetHosts sync <id>` reads a host's manifest, and a
104
+ draft host publishes only after `datasetHosts test` reached it once.
72
105
 
73
- ## `cortex graph seed`
106
+ ## `cortex apply`
74
107
 
75
- Loads every domain the config declares — server-level `knowledge.domains` and per-agent ones,
76
- deduped — and seeds Neo4j with the concepts, endpoints, services, and rules they declare, then
77
- embeds them. Reports per domain, because a domain is the unit you edit. No
78
- flags.
108
+ Makes Cortex match a manifest, in one transaction: what is new is
109
+ created, what changed is updated and republished, and what the manifest
110
+ created and does not list is deleted. Rows it did not create are never
111
+ touched, and a name one of them holds fails the apply.
79
112
 
80
113
  ```sh
81
- bunx cortex graph seed
114
+ cortex apply # ./cortex.manifest.ts
115
+ cortex apply deploy/manifest.json --dry-run
116
+ cortex apply -p prod
82
117
  ```
83
118
 
84
- Seeding is `MERGE`-based, so re-running is safe. If any statement failed, the graph is partially
85
- seeded and the command exits 1 — a half-seeded graph should not pass as success.
86
-
87
- ## `cortex cc`
88
-
89
- Manage a Control Center without its console. The commands come from the
90
- Control Center's own management API (`/api/management/openapi.json`), fetched
91
- on each run and cached, so the tree always matches the deployment you point
92
- at and a new procedure needs no CLI release.
93
-
94
- ```sh
95
- cortex cc login # asks for the URL and a management key
96
- cortex cc status
97
- cortex cc agents list
98
- cortex cc agents create --slug support --name Support --serverId <uuid>
99
- cortex cc tools create --json tool.json --publish
100
- cortex cc tools list -o json | jq '.items[].name'
101
- cortex cc logout
119
+ The path defaults to exactly `./cortex.manifest.ts` in the directory you run
120
+ in. TypeScript and JSON load the same way: the file's default export is the
121
+ manifest. `defineManifest` from `@m6d/cortex` types one as you write it:
122
+
123
+ ```ts
124
+ import { defineManifest } from "@m6d/cortex";
125
+
126
+ export default defineManifest({
127
+ id: "shop",
128
+ tools: [
129
+ {
130
+ type: "http",
131
+ name: "findOrder",
132
+ description: "Find one order by id.",
133
+ url: "https://shop.example.com/orders/{orderId}",
134
+ method: "GET",
135
+ readOnly: true,
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: { orderId: { type: "string" } },
139
+ required: ["orderId"],
140
+ },
141
+ audience: { agents: ["support"] },
142
+ },
143
+ ],
144
+ });
102
145
  ```
103
146
 
104
- Profiles live in `~/.cortex/cc.json` (owner-only). `--profile <name>` picks
105
- one; `CORTEX_CC_URL` and `CORTEX_CC_KEY`, both set, win over the file for CI.
106
- `CORTEX_HOME` relocates the directory.
107
-
108
- The grammar is `cortex cc <router> <procedure> [id] [flags]`. Each top-level
109
- input field is a flag typed from the API's schema; nested values take JSON,
110
- and `--json <file|->` supplies a whole body that flags then override. A
111
- required field left out is asked for on a terminal and named as the missing
112
- flag otherwise. Listings take `--pageIndex`, `--pageSize`, `--q`, and `--all`
113
- to walk every page. `-o json` prints the raw result for pipes.
114
-
115
- Writes to agents, tools, services, skills, triggers, and dataset hosts save
116
- drafts.
117
- `--publish` on a write the API marks publishable (`update`, and a `create`
118
- whose result is complete enough) runs that entity's publish procedure right
119
- after; without it the command says so and names the command. Live fields bind
120
- on the server's next config revalidation without a publish: a trigger's
121
- `credential` (a secret, so it is prompted for or piped in with `--json -`
122
- rather than passed as a flag) and a dataset host's url, access fields, and
123
- timeout. `cortex cc dataset-hosts sync <id>` reads a host's manifest, and a
124
- draft host publishes only after `dataset-hosts test` reached it once.
147
+ The manifest is checked against the contract before anything is sent, then
148
+ sent as written, so Cortex fills its own defaults. It goes to the
149
+ management API (`manifests/sync`, or `manifests/preview` with `--dry-run`,
150
+ which changes nothing) under the same profile the management commands use, so
151
+ the key needs `publishing:publish` and the write permission of every kind the
152
+ apply changes. The output lists each changed entry under its kind: `+`
153
+ created, `~` updated, `-` deleted. A trigger the apply creates, or whose
154
+ `fireCredential` it drops, gets a fire secret, printed once below the summary
155
+ and never again.
156
+
157
+ References between entries (mentions, service scopes, which role a
158
+ credential or secret plays) are judged on the state the apply leaves, so
159
+ the order the manifest lists them in does not matter. A dataset host's
160
+ credentials are the exception: its catalog is read with them before the
161
+ apply, so they are judged on the deployment as it stands. A credential a
162
+ trigger fires with cannot go to a host in the same apply that drops
163
+ it; drop it first, then apply again.
125
164
 
126
165
  ## Global options
127
166
 
128
- | Option | Description |
129
- | --------------- | -------------------------------------------- |
130
- | `-v, --verbose` | Print full errors and the commands being run |
131
- | `-V, --version` | Output the version number |
132
- | `-h, --help` | Display help for command |
133
-
134
- `swagger` and `graph` read the project and take `-c, --config <path>` (default
135
- `./cortex.config.ts`) on the group or the verb. `new` and `cc` do not touch a
136
- project and have no such flag.
137
-
138
- Config discovery is that exact filename in the directory you run in, with no upward search. There is
139
- no `--cwd`: Bun auto-loads `.env` relative to the process cwd before any of our code runs, so
140
- reading another directory's config from here would pair it with the wrong environment. `cd` first,
141
- or point `--config` at the file.
142
-
143
- The config is inert data — a plain object literal the CLI imports. Any key that is present but
144
- evaluates to `undefined`, usually a missing environment variable, is listed by path and stops the
145
- run.
146
-
147
- `graph seed` runs standalone: it never constructs or serves your `@m6d/cortex-server` — it only
148
- imports your `cortex.config.ts` (whose own imports may pull the server's modules in, inertly).
149
- Its Neo4j client, its embedder and its `GRAPH_SCHEMA` come from the graph contract the server
150
- reads the graph back with (each package vendors its own copy at pack time; the server re-exports
151
- it) — one contract, shared from both ends. Seeding stamps the schema
152
- version it wrote onto the graph; if that graph already carries a different one, the CLI says so and
153
- seeds anyway, noting that nodes from the old schema are not removed. Skew is caught on the reading
154
- side, where it matters: the server checks the stamp on its first resolve and refuses a graph it
155
- cannot read.
156
-
157
- `swagger` writes `.endpoint.ts` source whose generated block has to satisfy the server's own
158
- `EndpointDef`, so a codegen that outran the installed version is a loud type error in your
159
- `bun run check`.
160
-
161
- Your `cortex.config.ts` imports `@m6d/cortex-server` for `defineAgent`, so a project without it
162
- installed fails on the import — reported as an unloadable config, with the missing package named.
167
+ | Option | Description |
168
+ | ---------------------- | -------------------------------------------- |
169
+ | `-v, --verbose` | Print full errors and the commands being run |
170
+ | `-p, --profile <name>` | Which saved profile to use |
171
+ | `-V, --version` | Output the version number |
172
+ | `-h, --help` | Display help for command |
163
173
 
164
174
  ## Exit codes
165
175
 
166
- | Command | 0 | 1 | 2 |
167
- | --------------- | --------------------------------------------- | ----------------------------------- | ---------------------- |
168
- | `new` | Files written — even if `bun install` failed | Bad target, missing name, cancelled | — |
169
- | `swagger sync` | Wrote what it could, `MISSING` files included | Couldn't run | — |
170
- | `swagger check` | Every endpoint file matched and in sync | Couldn't run | Drift and/or `MISSING` |
171
- | `graph seed` | Everything seeded | A statement failed, or couldn't run | — |
172
-
173
- _Couldn't run_ means no config file, an unloadable config (a missing `@m6d/cortex-server` in the
174
- project reads as one), undefined values in it, an absent domains directory, or an unreachable
175
- Swagger host or Neo4j.
176
+ | Command | 0 | 1 |
177
+ | ------- | -------------------------------------------- | ----------------------------------- |
178
+ | `new` | Files written — even if `bun install` failed | Bad target, missing name, cancelled |
179
+ | `apply` | Applied, or previewed with `--dry-run` | Invalid manifest, or refused |
180
+ | others | The procedure ran | It failed, or couldn't run |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@m6d/cortex-cli",
3
- "version": "0.12.0",
4
- "description": "Scaffold and operate Cortex servers",
3
+ "version": "0.13.0",
4
+ "description": "Scaffold Cortex projects and manage a Cortex deployment",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "@clack/prompts": "^1.7.0",
25
25
  "@commander-js/extra-typings": "^15.0.0",
26
26
  "commander": "^15.0.0",
27
- "picocolors": "^1.1.1"
27
+ "picocolors": "^1.1.1",
28
+ "zod": "^4.6.5"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@cortex/contracts": "workspace:*",
@@ -35,7 +36,17 @@
35
36
  "access": "public"
36
37
  },
37
38
  "releaseWatchPaths": [
38
- "internal/contracts",
39
- "packages/cortex-server"
39
+ "apps/cortex",
40
+ "packages/cortex-typescript",
41
+ "internal/contracts/src/client-tools",
42
+ "internal/contracts/src/datasets",
43
+ "internal/contracts/src/egress-policy",
44
+ "internal/contracts/src/errors",
45
+ "internal/contracts/src/json",
46
+ "internal/contracts/src/manifest",
47
+ "internal/contracts/src/manifest-file",
48
+ "internal/contracts/src/server",
49
+ "internal/contracts/src/triggers",
50
+ "internal/contracts/src/wire"
40
51
  ]
41
52
  }
package/src/cli.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { Command, Help } from "@commander-js/extra-typings";
2
2
 
3
3
  import pkg from "../package.json";
4
- import { registerCc } from "@/commands/cc";
5
- import { registerGraph } from "@/commands/graph";
4
+ import { registerApply } from "@/commands/apply";
5
+ import { registerManagement } from "@/commands/management";
6
6
  import { registerNew } from "@/commands/new";
7
- import { registerSwagger } from "@/commands/swagger";
8
7
  import * as report from "@/ui/report";
9
8
 
10
9
  const program = new Command()
11
10
  .name("cortex")
12
- .description("Scaffold and operate Cortex servers.")
11
+ .description("Scaffold Cortex projects and manage a Cortex deployment.")
13
12
  .option("-v, --verbose", "Print full errors and the commands being run")
13
+ .option("-p, --profile <name>", "Which saved profile to use")
14
14
  .version(pkg.version, "-V, --version", "Output the version number")
15
15
  .helpOption("-h, --help", "Display help for command")
16
16
  // The implicit `help [command]` would print as a third, ungrouped section and
@@ -19,25 +19,30 @@ const program = new Command()
19
19
  // Commander appends ` [options]` to any command that has some. Left in, the
20
20
  // first command to grow a flag widens the whole list and the two-group screen
21
21
  // stops matching. `cortex <command> --help` lists them.
22
+ // Every command's help lists the root's options too, `--profile` among them.
22
23
  .configureHelp({
24
+ showGlobalOptions: true,
23
25
  subcommandTerm: function (cmd) {
24
26
  return new Help().subcommandTerm(cmd).replace(" [options]", "");
25
27
  },
26
28
  })
27
- .addHelpText("after", "\nRun `cortex <resource> --help` to see what you can do with one.");
29
+ .addHelpText(
30
+ "after",
31
+ "\nEvery other command comes from the Cortex you logged in to:\n`cortex <resource> <procedure>`. Run `cortex <resource> --help` to see a resource's.",
32
+ );
28
33
 
29
34
  // Each command owns its help group, so the two headings survive these files being
30
35
  // rewritten. Registration order fixes the order the groups print in.
31
36
  registerNew(program);
32
- registerSwagger(program);
33
- registerGraph(program);
34
- // The Control Center's commands come from its API, so this one may fetch
35
- // before parsing; it looks at argv to decide whether it needs to.
36
- await registerCc(program, process.argv.slice(2));
37
+ registerApply(program);
38
+ // The management commands come from Cortex's API, so this one may fetch
39
+ // before parsing; it looks at argv to decide whether it needs to, which it
40
+ // does only when argv names none of the commands registered before it.
41
+ await registerManagement(program, process.argv.slice(2));
37
42
 
38
43
  // Commander's own errors print and exit on their own, and every failure the
39
- // commands predict goes through `report.fail`. Anything still reaching here is a
40
- // bug — in the CLI, or in the project's server — and it gets the same grammar
44
+ // commands predict goes through `report.fail`. Anything reaching here is a
45
+ // bug — in the CLI, or in Cortex — and it gets the same grammar
41
46
  // rather than a raw Bun stack dump.
42
47
  await program.parseAsync().catch(function (error: unknown) {
43
48
  const verbose = program.opts().verbose === true;
@@ -0,0 +1,169 @@
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
+ 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
+ const manifest = await loadManifest(file, verbose);
44
+ report.input("Manifest", label(file));
45
+ report.input("Cortex", profile.url);
46
+
47
+ const answer = await callOrFail(profile, () =>
48
+ callManagement(
49
+ profile,
50
+ "post",
51
+ dryRun ? "/manifests/preview" : "/manifests/sync",
52
+ manifest,
53
+ ),
54
+ );
55
+ // The secrets are read apart from the changes: a sync has committed by
56
+ // now, and a summary this CLI is too old to read must not lose them.
57
+ const { triggerSecrets = {} } = appliedManifestSchema
58
+ .pick({ triggerSecrets: true })
59
+ .partial()
60
+ .parse(answer);
61
+ const applied = manifestChangesSchema.safeParse(answer);
62
+ if (!applied.success) {
63
+ printTriggerSecrets(triggerSecrets);
64
+ report.fail({
65
+ what: `${profile.url} answered with a summary this CLI cannot read`,
66
+ blocks: [
67
+ `The ${dryRun ? "preview" : "apply"} went through. Update the CLI to read its summary:`,
68
+ " bun add -d @m6d/cortex-cli@latest",
69
+ ],
70
+ });
71
+ }
72
+ printChanges(applied.data);
73
+ const created = countNames(applied.data.create);
74
+ const updated = countNames(applied.data.update);
75
+ const deleted = countNames(applied.data.delete);
76
+ report.footer(
77
+ created + updated + deleted === 0
78
+ ? "no changes"
79
+ : dryRun
80
+ ? `would create ${created}, update ${updated}, delete ${deleted}`
81
+ : `created ${created}, updated ${updated}, deleted ${deleted}`,
82
+ );
83
+ printTriggerSecrets(triggerSecrets);
84
+ });
85
+ }
86
+
87
+ /** How a manifest path prints: relative to the cwd, `./`-prefixed. */
88
+ function label(path: string) {
89
+ const relativePath = relative(process.cwd(), path);
90
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
91
+ }
92
+
93
+ /** The manifest's absolute path; a missing file ends the run before anything prints. */
94
+ function findManifest(path: string) {
95
+ const absolute = resolve(process.cwd(), path);
96
+ if (existsSync(absolute)) return absolute;
97
+ return report.fail({
98
+ what: `No manifest at ${path}`,
99
+ blocks:
100
+ path === DEFAULT_MANIFEST
101
+ ? [
102
+ "cortex apply reads exactly ./cortex.manifest.ts in the directory you run in.\nIt does not search parent directories.",
103
+ "Run it from the project root, or point at the file:",
104
+ " cortex apply ./deploy/cortex.manifest.ts",
105
+ ]
106
+ : ["The path is resolved from the directory you run in."],
107
+ });
108
+ }
109
+
110
+ /**
111
+ * The file's default export, checked against the manifest contract here so
112
+ * a mistake is named before anything is sent, then sent as written: Cortex
113
+ * fills its own defaults, which may be newer than this CLI's.
114
+ */
115
+ async function loadManifest(path: string, verbose: boolean) {
116
+ const { written, parsed } = await loadManifestFile(path).catch((error: unknown) =>
117
+ report.fail({
118
+ what: `Could not load ${label(path)}`,
119
+ error,
120
+ blocks: [
121
+ "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`).",
122
+ ],
123
+ verbose,
124
+ }),
125
+ );
126
+ if (!parsed.success) {
127
+ report.fail({
128
+ what: `${label(path)} is not a valid manifest`,
129
+ blocks: [z.prettifyError(parsed.error)],
130
+ });
131
+ }
132
+ return written;
133
+ }
134
+
135
+ /** One row per changed entry under its kind: `+` created, `~` updated, `-` deleted. */
136
+ function printChanges(changes: Record<(typeof CHANGES)[number], Record<string, string[]>>) {
137
+ const rows = new Map<string, string[]>();
138
+ for (const change of CHANGES) {
139
+ for (const [kind, names] of Object.entries(changes[change])) {
140
+ const marked = names.map((name) => `${MARKS[change]} ${name}`);
141
+ rows.set(kind, [...(rows.get(kind) ?? []), ...marked]);
142
+ }
143
+ }
144
+ for (const [kind, lines] of rows) {
145
+ for (const [index, line] of lines.entries()) {
146
+ report.row(index === 0 ? kind : "", line, 14);
147
+ }
148
+ }
149
+ }
150
+
151
+ function countNames(byKind: Record<string, string[]>) {
152
+ return Object.values(byKind).flat().length;
153
+ }
154
+
155
+ /**
156
+ * The fire secrets this apply minted, for each trigger it created or whose
157
+ * `fireCredential` it dropped; nothing shows them again.
158
+ */
159
+ function printTriggerSecrets(secrets: Record<string, string>) {
160
+ const entries = Object.entries(secrets);
161
+ if (entries.length === 0) return;
162
+ report.note(
163
+ [
164
+ "Trigger fire secrets, shown this once. Store them now: nothing shows them again,",
165
+ "and only rotating a trigger's secret gives it a new one.",
166
+ ...entries.map(([name, secret]) => ` ${name} ${secret}`),
167
+ ].join("\n"),
168
+ );
169
+ }