@m6d/cortex-cli 0.12.1 → 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.
- package/README.md +135 -130
- package/package.json +16 -5
- package/src/cli.ts +17 -12
- package/src/commands/apply.ts +169 -0
- package/src/commands/{cc.ts → management.ts} +150 -100
- package/src/commands/new.ts +50 -74
- 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 +932 -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 +247 -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 +61 -32
- package/src/{cc → management}/client.ts +19 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +5 -5
- package/src/scaffold/files.ts +362 -237
- package/src/scaffold/ports.ts +13 -24
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +7 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +2 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +52 -0
- package/src/ui/prompts.ts +4 -31
- 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
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# @m6d/cortex-cli
|
|
2
2
|
|
|
3
|
-
The `cortex` command line: scaffold Cortex
|
|
4
|
-
|
|
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
|
|
9
|
-
bunx cortex
|
|
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
|
|
13
|
-
resource
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
47
|
+
bunx @m6d/cortex-cli new shop --datasets
|
|
31
48
|
```
|
|
32
49
|
|
|
33
|
-
On a TTY it asks only what the flags left open
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
38
|
-
|
|
|
39
|
-
| `--
|
|
40
|
-
| `--
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
##
|
|
64
|
+
## Management commands
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
a
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
106
|
+
## `cortex apply`
|
|
74
107
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
114
|
+
cortex apply # ./cortex.manifest.ts
|
|
115
|
+
cortex apply deploy/manifest.json --dry-run
|
|
116
|
+
cortex apply -p prod
|
|
82
117
|
```
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
129
|
-
|
|
|
130
|
-
| `-v, --verbose`
|
|
131
|
-
| `-
|
|
132
|
-
| `-
|
|
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
|
|
167
|
-
|
|
|
168
|
-
| `new`
|
|
169
|
-
| `
|
|
170
|
-
|
|
|
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.
|
|
4
|
-
"description": "Scaffold and
|
|
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
|
-
"
|
|
39
|
-
"packages/cortex-
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
|
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(
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
await
|
|
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
|
|
40
|
-
// bug — in the CLI, or in
|
|
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
|
+
}
|