@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.
- package/README.md +193 -128
- package/package.json +16 -5
- package/src/cli.ts +19 -12
- package/src/commands/apply.ts +186 -0
- package/src/commands/{cc.ts → management.ts} +173 -112
- package/src/commands/new.ts +83 -84
- package/src/commands/setup.ts +610 -0
- 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 +973 -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 +349 -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 +67 -33
- package/src/{cc → management}/client.ts +33 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +10 -8
- package/src/scaffold/files.ts +448 -284
- package/src/scaffold/ports.ts +12 -24
- package/src/scaffold/templates/cspell.json.txt +41 -0
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +6 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +1 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +44 -0
- package/src/ui/prompts.ts +24 -38
- 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/src/scaffold/files.ts
CHANGED
|
@@ -1,61 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every file `cortex new` writes, as one pure function. Files with nothing to
|
|
3
3
|
* fill in are verbatim text under templates/; the ones that vary with the
|
|
4
|
-
*
|
|
4
|
+
* project are assembled here. The tooling configs are inlined rather than
|
|
5
5
|
* pulled from `@m6d/eslint-config`, `@m6d/prettier-config` and `@m6d/tsconfig`:
|
|
6
6
|
* those are `private: true` at `0.0.0` and resolvable only through this
|
|
7
7
|
* workspace, and a standalone scaffold has no workspace.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
10
|
+
import pkg from "../../package.json";
|
|
11
|
+
import serverTs from "@/scaffold/templates/server.ts.txt";
|
|
12
|
+
import setupTs from "@/scaffold/templates/setup.ts.txt";
|
|
13
|
+
import datasetsTs from "@/scaffold/templates/datasets.ts.txt";
|
|
13
14
|
import tsconfigJson from "@/scaffold/templates/tsconfig.json.txt";
|
|
14
15
|
import gitignoreText from "@/scaffold/templates/gitignore.txt";
|
|
16
|
+
import dockerignoreText from "@/scaffold/templates/dockerignore.txt";
|
|
15
17
|
import eslintConfig from "@/scaffold/templates/eslint.config.mjs.txt";
|
|
16
18
|
import prettierConfig from "@/scaffold/templates/prettier.config.mjs.txt";
|
|
17
19
|
import fallowrc from "@/scaffold/templates/fallowrc.jsonc.txt";
|
|
20
|
+
// Project jargon is the project's to add. Only `mprocs`, which its own
|
|
21
|
+
// package.json names, is seeded: a stranger's vocabulary is noise in a repo.
|
|
22
|
+
import cspellJson from "@/scaffold/templates/cspell.json.txt";
|
|
18
23
|
import { applyPorts, type Ports } from "@/scaffold/ports";
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The release of the Cortex image the generated stack runs. It and the
|
|
27
|
+
* `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
|
|
28
|
+
* before the CLI publishes, so a scaffold pins what was current then.
|
|
29
|
+
*/
|
|
30
|
+
const CORTEX_VERSION = "0.14.1";
|
|
31
|
+
|
|
32
|
+
/** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
|
|
33
|
+
export const DOCKER_HOST = "host.docker.internal";
|
|
34
|
+
|
|
35
|
+
/** How Cortex's containers reach the backend on this machine, at the port drawn for it. */
|
|
36
|
+
const SERVER_URL = `http://${DOCKER_HOST}:{{port.server}}`;
|
|
37
|
+
|
|
38
|
+
/** The longest server name Cortex accepts. */
|
|
39
|
+
const SERVER_NAME_MAX = 100;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The agent the manifest declares and the README sends you to chat with.
|
|
43
|
+
* Not `assistant`: Cortex holds that slug for its console's own assistant.
|
|
44
|
+
*/
|
|
45
|
+
export const AGENT_SLUG = "helper";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The embedding model the stack asks for, and the dimension the image's
|
|
49
|
+
* migrations give the vector columns. `cortex setup` probes a provider with
|
|
50
|
+
* the same pair before reusing it for embeddings.
|
|
51
|
+
*/
|
|
52
|
+
export const EMBEDDING = { model: "text-embedding-3-small", dimension: 768 } as const;
|
|
38
53
|
|
|
39
54
|
const DEV_DEPENDENCIES = {
|
|
55
|
+
// `cortex apply`, `cortex setup` and the management commands, at the release that wrote the project.
|
|
56
|
+
"@m6d/cortex-cli": `~${pkg.version}`,
|
|
40
57
|
"@types/bun": "latest",
|
|
58
|
+
// The two runners of `bun run dev` and `bun run dev:agent`.
|
|
59
|
+
concurrently: "^10.0.6",
|
|
41
60
|
cspell: "^9.6.4",
|
|
42
61
|
eslint: "^9",
|
|
43
62
|
"eslint-config-prettier": "^10.1.8",
|
|
44
63
|
"eslint-plugin-prettier": "^5.5.5",
|
|
45
64
|
fallow: "^3.9.1",
|
|
65
|
+
mprocs: "^0.9.6",
|
|
46
66
|
prettier: "^3.6.2",
|
|
47
67
|
typescript: "^5",
|
|
48
68
|
"typescript-eslint": "^8.65.0",
|
|
49
69
|
};
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
/**
|
|
72
|
+
* `setup` puts `cortex setup` among the processes `bun run dev` starts; `env`
|
|
73
|
+
* holds values `cortex new` was given for `.env`, such as the model provider,
|
|
74
|
+
* which is then written beside `.env.example` with them filled in.
|
|
75
|
+
*/
|
|
76
|
+
type Input = {
|
|
77
|
+
name: string;
|
|
78
|
+
datasets: boolean;
|
|
79
|
+
setup: boolean;
|
|
80
|
+
ports: Ports;
|
|
81
|
+
env?: Record<string, string>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** The input, and the name the backend is registered under in Cortex. */
|
|
85
|
+
type Project = Input & { server: string };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `text` with each `KEY=` line named in `values` set to its value, and every
|
|
89
|
+
* other line left as written. `cortex new` fills `.env` with it, and
|
|
90
|
+
* `cortex setup` the `.env` it asks the model provider for.
|
|
91
|
+
*/
|
|
92
|
+
export function withEnvValues(text: string, values: Record<string, string>) {
|
|
93
|
+
return Object.entries(values).reduce(
|
|
94
|
+
(filled, [key, value]) =>
|
|
95
|
+
filled.replace(new RegExp(`^${key}=.*$`, "m"), () => `${key}=${value}`),
|
|
96
|
+
text,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** What `bun run dev` starts side by side, named as the runners label them. */
|
|
101
|
+
function devProcesses(setup: boolean) {
|
|
102
|
+
return {
|
|
103
|
+
cortex: "docker compose up",
|
|
104
|
+
server: "bun run dev:server",
|
|
105
|
+
...(setup ? { setup: "bun cortex setup --wait --yes" } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One runner's command line: the process names, then each command quoted. */
|
|
110
|
+
function runAll(runner: "mprocs" | "concurrently", processes: Record<string, string>) {
|
|
111
|
+
const commands = Object.values(processes).map((command) => `"${command}"`);
|
|
112
|
+
return `${runner} --names ${Object.keys(processes).join(",")} ${commands.join(" ")}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The name as the manifest `id` takes it: lowercase words and hyphens. */
|
|
116
|
+
export function manifestIdOf(name: string) {
|
|
117
|
+
return name
|
|
118
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
119
|
+
.slice(0, 64)
|
|
120
|
+
.replace(/^-+|-+$/g, "");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function packageJson({ name, datasets, setup }: Project) {
|
|
124
|
+
const processes = devProcesses(setup);
|
|
57
125
|
|
|
58
|
-
function packageJson(name: string) {
|
|
59
126
|
return `${JSON.stringify(
|
|
60
127
|
{
|
|
61
128
|
name,
|
|
@@ -69,14 +136,26 @@ function packageJson(name: string) {
|
|
|
69
136
|
// analyses — dead code, duplication, complexity — over everything.
|
|
70
137
|
audit: "fallow",
|
|
71
138
|
check: "tsc --noEmit",
|
|
72
|
-
|
|
139
|
+
// The same processes twice: a dashboard for people, and one
|
|
140
|
+
// labelled log for coding agents and CI.
|
|
141
|
+
dev: runAll("mprocs", processes),
|
|
142
|
+
"dev:agent": runAll("concurrently", processes),
|
|
143
|
+
"dev:server": "bun run --hot index.ts",
|
|
144
|
+
// A no-op where setup.ts is not copied in, as in an image build.
|
|
145
|
+
postinstall: "bun run --if-present setup.ts",
|
|
73
146
|
format: "prettier --write .",
|
|
74
147
|
"format:check": "prettier --check .",
|
|
75
148
|
lint: "eslint",
|
|
76
149
|
spellcheck: 'cspell "**/*.{ts,mjs,json,md}"',
|
|
77
150
|
verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
|
|
78
151
|
},
|
|
79
|
-
dependencies:
|
|
152
|
+
dependencies: {
|
|
153
|
+
"@m6d/cortex": "~0.2.0",
|
|
154
|
+
// drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
|
|
155
|
+
...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
|
|
156
|
+
hono: "^4.12.3",
|
|
157
|
+
zod: "^4.6.5",
|
|
158
|
+
},
|
|
80
159
|
devDependencies: DEV_DEPENDENCIES,
|
|
81
160
|
},
|
|
82
161
|
null,
|
|
@@ -84,275 +163,359 @@ function packageJson(name: string) {
|
|
|
84
163
|
)}\n`;
|
|
85
164
|
}
|
|
86
165
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*/
|
|
92
|
-
const NEVER_SCAFFOLDED = [
|
|
93
|
-
[
|
|
94
|
-
"Re-ranks knowledge-graph retrieval when it comes back noisy.",
|
|
95
|
-
`reranker: {
|
|
96
|
-
url: process.env["CORTEX_RERANKER_URL"]!,
|
|
97
|
-
apiKey: process.env["CORTEX_RERANKER_KEY"]!,
|
|
98
|
-
},`,
|
|
99
|
-
],
|
|
100
|
-
[
|
|
101
|
-
"Context-window tuning. Every field is defaulted, so override only what you\nhave measured a reason to change.",
|
|
102
|
-
`context: {
|
|
103
|
-
maxContextTokens: 120_000,
|
|
104
|
-
recentMessagesToKeep: 6,
|
|
105
|
-
},`,
|
|
106
|
-
],
|
|
107
|
-
] satisfies [note: string, config: string][];
|
|
108
|
-
|
|
109
|
-
/** Fragments are authored at zero indentation and placed by these two. */
|
|
110
|
-
function indented(block: string) {
|
|
111
|
-
return block.replace(/^(?!$)/gm, " ");
|
|
112
|
-
}
|
|
166
|
+
function indexTs({ datasets }: Project) {
|
|
167
|
+
const routes = datasets
|
|
168
|
+
? "its server protocol under /cortex and its\n * dataset host under /datasets; your own routes go beside them"
|
|
169
|
+
: "its server protocol under /cortex; your own routes go beside it";
|
|
113
170
|
|
|
114
|
-
|
|
115
|
-
return block.replace(/^(?!$)/gm, " // ");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** A note, then its fragment — written live, or written disabled. */
|
|
119
|
-
function block(note: string, config: string, enabled: boolean) {
|
|
120
|
-
return `\n${commented(note)}\n${enabled ? indented(config) : commented(config)}\n`;
|
|
121
|
-
}
|
|
171
|
+
return `import { Hono } from "hono";
|
|
122
172
|
|
|
123
|
-
|
|
124
|
-
const fragment = (feature: Feature) =>
|
|
125
|
-
block(FEATURES[feature].note, FEATURES[feature].config, features.includes(feature));
|
|
126
|
-
|
|
127
|
-
const on = features.map(fragment).join("");
|
|
128
|
-
const off = FEATURE_NAMES.filter((feature) => !features.includes(feature))
|
|
129
|
-
.map(fragment)
|
|
130
|
-
.join("");
|
|
131
|
-
const never = NEVER_SCAFFOLDED.map(([note, config]) => block(note, config, false)).join("");
|
|
132
|
-
|
|
133
|
-
// cspell.json ships an empty word list on purpose, so the Cortex jargon the
|
|
134
|
-
// config names teaches the checker from the file that uses it.
|
|
135
|
-
return `import { defineAgent } from "@m6d/cortex-server";
|
|
136
|
-
import type { CortexConfig } from "@m6d/cortex-server";
|
|
137
|
-
|
|
138
|
-
// cspell:words reranker
|
|
173
|
+
${datasets ? `import { datasets } from "./datasets";\n` : ""}import { server } from "./server";
|
|
139
174
|
|
|
140
175
|
/**
|
|
141
|
-
*
|
|
142
|
-
* it never opens a connection or binds a port. Construction lives in index.ts,
|
|
143
|
-
* which is what lets tooling read this config without booting the server.
|
|
176
|
+
* This backend. Cortex reaches ${routes}.
|
|
144
177
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
connectionString: process.env["CORTEX_DATABASE_URL"]!,
|
|
150
|
-
},
|
|
151
|
-
model: {
|
|
152
|
-
baseURL: process.env["CORTEX_MODEL_URL"]!,
|
|
153
|
-
apiKey: process.env["CORTEX_MODEL_KEY"]!,
|
|
154
|
-
modelName: process.env["CORTEX_MODEL_NAME"]!,
|
|
155
|
-
},
|
|
156
|
-
${on === "" ? "" : `${on}\n`} agents: {
|
|
157
|
-
assistant: defineAgent({
|
|
158
|
-
systemPrompt: "You are a helpful assistant.",
|
|
159
|
-
// Applications that serve datasets for grouped questions (see the
|
|
160
|
-
// README's "Dataset hosts"): the host applies its own access rules
|
|
161
|
-
// under the run's token the list below forwards, the agent only
|
|
162
|
-
// picks which datasets to expose and pin.
|
|
163
|
-
// datasetHosts: [{ name: "store", url: "https://store.example/api/datasets", credentials: [{ header: "X-End-User-Token", credential: { kind: "run-token" } }], pinned: ["orders"] }],
|
|
164
|
-
}),
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
// Everything below is off, written out so the file documents what else the
|
|
168
|
-
// server does. Uncomment a block and fill in its keys in .env to enable it.
|
|
169
|
-
${off}${never}} satisfies CortexConfig;
|
|
178
|
+
const app = new Hono();
|
|
179
|
+
app.route("/cortex", server);
|
|
180
|
+
${datasets ? `app.route("/datasets", datasets);\n` : ""}
|
|
181
|
+
export default { port: {{port.server}}, fetch: app.fetch };
|
|
170
182
|
`;
|
|
171
183
|
}
|
|
172
184
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
function manifestTs({ name, datasets }: Project) {
|
|
186
|
+
const fromEnv = datasets
|
|
187
|
+
? `/** The server this backend is registered as, and where Cortex reaches it, from .env. */
|
|
188
|
+
const { CORTEX_SERVER_NAME: server, CORTEX_SERVER_URL: serverUrl } = process.env;
|
|
189
|
+
if (!server || !serverUrl) {
|
|
190
|
+
throw new Error("Apply from this project: its .env names the server.");
|
|
191
|
+
}`
|
|
192
|
+
: `/** The server this backend is registered as, from .env. */
|
|
193
|
+
const server = process.env["CORTEX_SERVER_NAME"];
|
|
194
|
+
if (!server) throw new Error("Apply from this project: its .env names the server.");`;
|
|
195
|
+
const datasetHosts = `
|
|
196
|
+
// The orders this backend serves (datasets.ts), for every agent on its server.
|
|
197
|
+
datasetHosts: [
|
|
198
|
+
{
|
|
199
|
+
name: "store",
|
|
200
|
+
url: \`\${serverUrl}/datasets\`,
|
|
201
|
+
credentials: [],
|
|
202
|
+
expose: ["orders"],
|
|
203
|
+
pinned: ["orders"],
|
|
204
|
+
audience: { server },
|
|
205
|
+
},
|
|
206
|
+
],`;
|
|
207
|
+
|
|
208
|
+
return `import { defineManifest } from "@m6d/cortex";
|
|
209
|
+
|
|
210
|
+
/*
|
|
211
|
+
* What this project declares in Cortex, which \`cortex apply\` makes Cortex
|
|
212
|
+
* match. The agent runs on this backend's server, whose variable its prompt
|
|
213
|
+
* reads and whose native tool it calls.
|
|
176
214
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
215
|
+
|
|
216
|
+
${fromEnv}
|
|
217
|
+
|
|
218
|
+
export default defineManifest({
|
|
219
|
+
id: ${JSON.stringify(manifestIdOf(name))},
|
|
220
|
+
agents: [
|
|
221
|
+
{
|
|
222
|
+
slug: "${AGENT_SLUG}",
|
|
223
|
+
name: "Helper",
|
|
224
|
+
server,
|
|
225
|
+
prompt: "You are a helpful assistant. Address the user as {{userName}}.",
|
|
226
|
+
// No identity provider yet: every caller is one shared identity.
|
|
227
|
+
auth: { kind: "anonymous" },
|
|
228
|
+
},
|
|
229
|
+
],${datasets ? datasetHosts : ""}
|
|
230
|
+
});
|
|
231
|
+
`;
|
|
182
232
|
}
|
|
183
233
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const note = hasServices(database, features)
|
|
190
|
-
? `#
|
|
191
|
-
# The service ports below are not the canonical ones. They were picked when this
|
|
192
|
-
# project was scaffolded, from what was free on the machine, so it does not
|
|
193
|
-
# collide with anything else you run. Change them freely — in step with
|
|
194
|
-
# docker-compose.yml.
|
|
195
|
-
`
|
|
196
|
-
: "";
|
|
197
|
-
|
|
198
|
-
return `# Environment for this Cortex server. Bun loads this file from the directory you
|
|
199
|
-
# run in, so it has to sit next to cortex.config.ts.
|
|
234
|
+
/** Every setting the project has, which setup.ts copies into .env, generating the secrets. */
|
|
235
|
+
function envExample({ server }: Project) {
|
|
236
|
+
return `# Every setting this project has. Bun loads .env from the directory you run
|
|
237
|
+
# in, into the backend and the \`cortex\` CLI; docker compose hands it to the
|
|
238
|
+
# Cortex containers and reads COMPOSE_PROJECT_NAME from it.
|
|
200
239
|
#
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
240
|
+
# This is the committed copy. \`bun install\` writes .env from it, generating
|
|
241
|
+
# the secrets it leaves blank, and never changes a value .env already holds.
|
|
242
|
+
|
|
243
|
+
# The Compose project this checkout's Cortex stack runs as, drawn for each
|
|
244
|
+
# checkout, so a clone never shares another checkout's containers or volumes.
|
|
245
|
+
COMPOSE_PROJECT_NAME=
|
|
246
|
+
|
|
247
|
+
# Cortex's first admin, created when the stack first starts, and its
|
|
248
|
+
# management key.
|
|
249
|
+
DEFAULT_ADMIN_EMAIL=admin@example.com
|
|
250
|
+
DEFAULT_ADMIN_PASSWORD=
|
|
251
|
+
BOOTSTRAP_MANAGEMENT_KEY=
|
|
252
|
+
# Session signing, and the key the secrets store is encrypted with.
|
|
253
|
+
BETTER_AUTH_SECRET=
|
|
254
|
+
SECRETS_MASTER_KEY=
|
|
255
|
+
# Any OpenAI-compatible embeddings endpoint and model. Tool search and
|
|
256
|
+
# knowledge collections embed with it, and stay off while the key is blank.
|
|
257
|
+
EMBEDDING_BASE_URL=https://api.openai.com/v1
|
|
258
|
+
EMBEDDING_API_KEY=
|
|
259
|
+
EMBEDDING_MODEL=${EMBEDDING.model}
|
|
260
|
+
# The private hosts Cortex's containers may call, and send a secret to over
|
|
261
|
+
# plain http, comma-separated: this machine, for the backend. A model provider
|
|
262
|
+
# on a private network goes here too. The policy admits a host by name,
|
|
263
|
+
# whatever the port: see README.md.
|
|
264
|
+
EGRESS_ALLOWED_HOSTS=${DOCKER_HOST}
|
|
265
|
+
|
|
266
|
+
# The Cortex \`cortex apply\` and \`cortex setup\` talk to: this checkout's,
|
|
267
|
+
# under its management key, so neither needs \`cortex login\`. \`-p <profile>\`
|
|
268
|
+
# still picks a saved one.
|
|
269
|
+
CORTEX_URL=http://localhost:{{port.cortex}}
|
|
270
|
+
CORTEX_KEY=\${BOOTSTRAP_MANAGEMENT_KEY}
|
|
271
|
+
|
|
272
|
+
# This backend as Cortex registers it: its name, where Cortex's containers
|
|
273
|
+
# reach it, and the key Cortex presents on every call to it.
|
|
274
|
+
CORTEX_SERVER_NAME=${server}
|
|
275
|
+
CORTEX_SERVER_URL=${SERVER_URL}
|
|
276
|
+
CORTEX_SERVER_KEY=
|
|
277
|
+
|
|
278
|
+
# The model provider \`cortex setup\` registers and the server's agents run on.
|
|
279
|
+
# While these are blank, setup registers the server and applies nothing.
|
|
280
|
+
MODEL_PROVIDER_NAME=
|
|
281
|
+
MODEL_PROVIDER_URL=
|
|
282
|
+
MODEL_PROVIDER_API_KEY=
|
|
283
|
+
MODEL_PROVIDER_MODEL=
|
|
284
|
+
`;
|
|
213
285
|
}
|
|
214
286
|
|
|
215
|
-
/**
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
287
|
+
/** Cortex itself: its app, worker and engine, all on the one Cortex image, and their stores. */
|
|
288
|
+
function dockerCompose() {
|
|
289
|
+
return `# Cortex, for this project's agents to run on: the console at
|
|
290
|
+
# http://localhost:{{port.cortex}}, the engine widgets and trigger callers reach at
|
|
291
|
+
# http://localhost:{{port.engine}}, and the stores they keep state in. \`bun run dev\`
|
|
292
|
+
# starts it; .env holds its secrets.
|
|
293
|
+
#
|
|
294
|
+
# The host ports were picked at scaffold time from what was free on this
|
|
295
|
+
# machine. Move one and the URLs that name it here, in .env and in README.md
|
|
296
|
+
# follow.
|
|
297
|
+
# CORTEX_VERSION, set in the shell, runs another release of the image.
|
|
298
|
+
#
|
|
299
|
+
# The console's browser uploads (knowledge documents) go straight to
|
|
300
|
+
# S3_ENDPOINT, which only this stack resolves. Point it at storage the
|
|
301
|
+
# browser reaches too before relying on them.
|
|
302
|
+
|
|
303
|
+
# The stack runs as .env's COMPOSE_PROJECT_NAME, drawn for each checkout by
|
|
304
|
+
# setup.ts, so no two checkouts share containers or volumes, whatever their
|
|
305
|
+
# directories are called.
|
|
306
|
+
|
|
307
|
+
x-cortex: &cortex
|
|
308
|
+
image: syscodeia/cortex:\${CORTEX_VERSION:-${CORTEX_VERSION}}
|
|
309
|
+
working_dir: /repo/apps/cortex
|
|
310
|
+
# The whole .env, the backend's settings too. What \`environment\` sets
|
|
311
|
+
# wins over it, so nothing there can move these containers' ports or mode.
|
|
312
|
+
env_file: .env
|
|
313
|
+
environment: &cortex-environment
|
|
314
|
+
NODE_ENV: production
|
|
315
|
+
PORT: "3000"
|
|
316
|
+
DATABASE_URL: postgres://postgres:postgres@postgres:5432/cortex
|
|
317
|
+
REDIS_URL: redis://redis:6379
|
|
318
|
+
S3_ENDPOINT: http://minio:9000
|
|
319
|
+
S3_REGION: us-east-1
|
|
320
|
+
S3_ACCESS_KEY_ID: minio
|
|
321
|
+
S3_SECRET_ACCESS_KEY: minio-secret-key
|
|
322
|
+
S3_BUCKET: cortex
|
|
323
|
+
BETTER_AUTH_ALLOWED_HOSTS: localhost:{{port.cortex}},127.0.0.1:{{port.cortex}}
|
|
324
|
+
# The dimension the image's migrations give the vector columns.
|
|
325
|
+
EMBEDDING_DIMENSION: "${EMBEDDING.dimension}"
|
|
326
|
+
# The engine's public origin (browsers, trigger callers) and the
|
|
327
|
+
# address the app reaches it at inside the stack.
|
|
328
|
+
ENGINE_PUBLIC_URL: http://localhost:{{port.engine}}
|
|
329
|
+
ENGINE_INTERNAL_URL: http://engine:3001
|
|
330
|
+
# Docker Desktop resolves host.docker.internal on its own; Linux needs this.
|
|
331
|
+
extra_hosts:
|
|
332
|
+
- host.docker.internal:host-gateway
|
|
224
333
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
image:
|
|
334
|
+
services:
|
|
335
|
+
postgres:
|
|
336
|
+
image: pgvector/pgvector:pg17
|
|
337
|
+
restart: unless-stopped
|
|
228
338
|
environment:
|
|
229
339
|
POSTGRES_USER: postgres
|
|
230
340
|
POSTGRES_PASSWORD: postgres
|
|
231
341
|
POSTGRES_DB: cortex
|
|
232
|
-
ports:
|
|
233
|
-
- "{{port.postgres}}:5432"
|
|
234
342
|
volumes:
|
|
235
|
-
- postgres-data:/var/lib/postgresql/data
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
343
|
+
- postgres-data:/var/lib/postgresql/data
|
|
344
|
+
healthcheck:
|
|
345
|
+
test: ["CMD-SHELL", "pg_isready -U postgres -d cortex"]
|
|
346
|
+
interval: 5s
|
|
347
|
+
timeout: 5s
|
|
348
|
+
retries: 10
|
|
349
|
+
|
|
350
|
+
redis:
|
|
351
|
+
image: redis:7-alpine
|
|
352
|
+
restart: unless-stopped
|
|
353
|
+
healthcheck:
|
|
354
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
355
|
+
interval: 5s
|
|
356
|
+
timeout: 5s
|
|
357
|
+
retries: 10
|
|
358
|
+
|
|
359
|
+
minio:
|
|
360
|
+
# A community rebuild of MinIO's last free release, pinned by digest:
|
|
361
|
+
# MinIO's own images left Docker Hub. It runs as nobody, so it takes
|
|
362
|
+
# root to reuse a volume another image wrote.
|
|
363
|
+
image: sourcemation/minio@sha256:538065d2dd0ef360fe3f22f25d2384e6bae9075b6d2603cb5587193bd73e6276
|
|
364
|
+
user: "0:0"
|
|
365
|
+
restart: unless-stopped
|
|
366
|
+
command: server /data
|
|
367
|
+
environment:
|
|
368
|
+
MINIO_ROOT_USER: minio
|
|
369
|
+
MINIO_ROOT_PASSWORD: minio-secret-key
|
|
370
|
+
volumes:
|
|
371
|
+
- minio-data:/data
|
|
372
|
+
healthcheck:
|
|
373
|
+
test: ["CMD", "bash", "-c", "</dev/tcp/127.0.0.1/9000"]
|
|
374
|
+
interval: 5s
|
|
375
|
+
timeout: 5s
|
|
376
|
+
retries: 10
|
|
377
|
+
|
|
378
|
+
minio-init:
|
|
379
|
+
image: chainguard/minio-client@sha256:b2bd7824d23d3e3b15bedd7e87fbc3be29d2e213307b4f901e4a1d92356dc20f
|
|
380
|
+
depends_on:
|
|
381
|
+
minio:
|
|
382
|
+
condition: service_healthy
|
|
383
|
+
environment:
|
|
384
|
+
MC_HOST_local: http://minio:minio-secret-key@minio:9000
|
|
385
|
+
command: ["mb", "--ignore-existing", "local/cortex"]
|
|
386
|
+
|
|
387
|
+
# One-shot: migrate the database, then create the first admin and its key.
|
|
388
|
+
migrator:
|
|
389
|
+
<<: *cortex
|
|
390
|
+
command: ["bun", "run", "db:migrate"]
|
|
391
|
+
depends_on:
|
|
392
|
+
postgres:
|
|
393
|
+
condition: service_healthy
|
|
394
|
+
|
|
395
|
+
seeder:
|
|
396
|
+
<<: *cortex
|
|
397
|
+
command: ["bun", "run", "db:seed:admin"]
|
|
398
|
+
depends_on:
|
|
399
|
+
migrator:
|
|
400
|
+
condition: service_completed_successfully
|
|
401
|
+
|
|
402
|
+
# The console and the management API.
|
|
403
|
+
app:
|
|
404
|
+
<<: *cortex
|
|
405
|
+
restart: unless-stopped
|
|
406
|
+
ports:
|
|
407
|
+
- "127.0.0.1:{{port.cortex}}:3000"
|
|
408
|
+
depends_on:
|
|
409
|
+
seeder:
|
|
410
|
+
condition: service_completed_successfully
|
|
411
|
+
minio-init:
|
|
412
|
+
condition: service_completed_successfully
|
|
413
|
+
|
|
414
|
+
worker:
|
|
415
|
+
<<: *cortex
|
|
416
|
+
restart: unless-stopped
|
|
417
|
+
command: ["bun", "src/jobs/worker.ts"]
|
|
418
|
+
depends_on:
|
|
419
|
+
seeder:
|
|
420
|
+
condition: service_completed_successfully
|
|
421
|
+
minio-init:
|
|
422
|
+
condition: service_completed_successfully
|
|
423
|
+
redis:
|
|
424
|
+
condition: service_healthy
|
|
425
|
+
|
|
426
|
+
# Runs the agents: widgets, trigger callers and the console's playground
|
|
427
|
+
# and assistant talk to it.
|
|
428
|
+
engine:
|
|
429
|
+
<<: *cortex
|
|
430
|
+
restart: unless-stopped
|
|
431
|
+
command: ["bun", "src/engine/server.ts"]
|
|
432
|
+
environment:
|
|
433
|
+
<<: *cortex-environment
|
|
434
|
+
PORT: "3001"
|
|
435
|
+
# Browser origins allowed to call the engine: the console's, for its playground and assistant.
|
|
436
|
+
ENGINE_CORS_ORIGINS: http://localhost:{{port.cortex}},http://127.0.0.1:{{port.cortex}}
|
|
437
|
+
ports:
|
|
438
|
+
- "127.0.0.1:{{port.engine}}:3001"
|
|
439
|
+
depends_on:
|
|
440
|
+
seeder:
|
|
441
|
+
condition: service_completed_successfully
|
|
442
|
+
redis:
|
|
443
|
+
condition: service_healthy
|
|
444
|
+
|
|
445
|
+
volumes:
|
|
446
|
+
postgres-data:
|
|
447
|
+
minio-data:
|
|
448
|
+
`;
|
|
268
449
|
}
|
|
269
450
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"*.sqlite",
|
|
291
|
-
"scratch.*",
|
|
292
|
-
".env*",
|
|
293
|
-
"**/*.min.js",
|
|
294
|
-
"**/*.min.css",
|
|
295
|
-
"**/db/migrations/**",
|
|
296
|
-
"**/i18n/**",
|
|
297
|
-
"docs",
|
|
298
|
-
".claude",
|
|
299
|
-
".angular",
|
|
300
|
-
".vercel/**",
|
|
301
|
-
"vendor/**",
|
|
302
|
-
".gitignore",
|
|
303
|
-
"**/domains/**/*.concept.ts",
|
|
304
|
-
],
|
|
305
|
-
dictionaries: [
|
|
306
|
-
"typescript",
|
|
307
|
-
"node",
|
|
308
|
-
"npm",
|
|
309
|
-
"html",
|
|
310
|
-
"css",
|
|
311
|
-
"bash",
|
|
312
|
-
"companies",
|
|
313
|
-
"softwareTerms",
|
|
314
|
-
],
|
|
315
|
-
},
|
|
316
|
-
null,
|
|
317
|
-
4,
|
|
318
|
-
)}\n`;
|
|
319
|
-
}
|
|
451
|
+
function readme({ name, server, datasets, setup }: Project) {
|
|
452
|
+
const gettingStarted = setup
|
|
453
|
+
? `That starts Cortex (\`docker compose up\`), this backend, and \`cortex setup\` in one dashboard.
|
|
454
|
+
Setup waits for Cortex, registers this backend as the server \`${server}\`, adds the model provider
|
|
455
|
+
in \`.env\`, and applies the manifest. Then it prints where to chat with \`${AGENT_SLUG}\`, and the first
|
|
456
|
+
time, how to sign in. It adds only what is missing, so every \`bun run dev\` runs it again.
|
|
457
|
+
|
|
458
|
+
While \`MODEL_PROVIDER_*\` in \`.env\` is blank, setup registers the server and applies nothing:
|
|
459
|
+
Cortex publishes no agent without a model. Run \`bun cortex setup\` in a terminal to be asked for
|
|
460
|
+
one.`
|
|
461
|
+
: `That starts Cortex (\`docker compose up\`) and this backend in one dashboard. Then register the
|
|
462
|
+
backend and apply the manifest, either way:
|
|
463
|
+
|
|
464
|
+
- \`bun cortex setup\` in a terminal asks for a model provider, registers the server
|
|
465
|
+
\`${server}\`, and applies the manifest.
|
|
466
|
+
- By hand: open the console at http://localhost:{{port.cortex}} and sign in with
|
|
467
|
+
\`DEFAULT_ADMIN_EMAIL\` and \`DEFAULT_ADMIN_PASSWORD\` from \`.env\`. Add a model provider on the
|
|
468
|
+
**Model providers** page, and store \`CORTEX_SERVER_KEY\` from \`.env\` on the **Secrets** page.
|
|
469
|
+
On the **Servers** page, add a server named \`${server}\` at \`${SERVER_URL}/cortex\` with that
|
|
470
|
+
secret as its key, and pick the model its agents run on. Then run \`bun cortex apply\`.`;
|
|
320
471
|
|
|
321
|
-
function readme(name: string, database: Database, features: Feature[], compose: boolean) {
|
|
322
472
|
return `# ${name}
|
|
323
473
|
|
|
324
|
-
A
|
|
325
|
-
|
|
326
|
-
|
|
474
|
+
A backend for Cortex agents, and Cortex itself to run them on.
|
|
475
|
+
|
|
476
|
+
- \`server.ts\` is this backend's Cortex server: the \`userName\` variable the agents' prompts read,
|
|
477
|
+
and the \`rollDice\` tool it runs for them.${
|
|
478
|
+
datasets
|
|
479
|
+
? "\n- `datasets.ts` serves an `orders` table as a dataset host, for agents to ask grouped\n questions of."
|
|
480
|
+
: ""
|
|
481
|
+
}
|
|
482
|
+
- \`cortex.manifest.ts\` declares the \`${AGENT_SLUG}\` agent on this server${
|
|
483
|
+
datasets ? " and the dataset host" : ""
|
|
484
|
+
}. \`cortex apply\` makes Cortex match it.
|
|
485
|
+
- \`docker-compose.yml\` runs Cortex: its app, worker and engine, and the stores they use.
|
|
486
|
+
- \`.env\` holds every setting: Cortex's secrets, this backend's key, and the model provider.
|
|
487
|
+
\`setup.ts\` runs on every \`bun install\`: it writes \`.env\` from the committed \`.env.example\` and
|
|
488
|
+
generates each secret left blank, so a fresh clone gets its own secrets and its own Compose stack.
|
|
489
|
+
It never changes a value already set. \`.dockerignore\` keeps both files out of any image built
|
|
490
|
+
here.
|
|
327
491
|
|
|
328
492
|
## Getting started
|
|
329
493
|
|
|
494
|
+
On a fresh clone, run \`bun install\` first (\`cortex new\` already did): it writes \`.env\`.
|
|
495
|
+
|
|
330
496
|
\`\`\`sh
|
|
331
|
-
bun install
|
|
332
|
-
${compose ? "docker compose up -d\n" : ""}# fill in CORTEX_MODEL_* in .env
|
|
333
497
|
bun run dev
|
|
334
498
|
\`\`\`
|
|
335
499
|
|
|
336
|
-
|
|
337
|
-
compose
|
|
338
|
-
? `
|
|
500
|
+
${gettingStarted}
|
|
339
501
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
\`.env
|
|
343
|
-
|
|
344
|
-
}
|
|
502
|
+
Then chat with \`${AGENT_SLUG}\` on the console's **Playground** page. Edit \`cortex.manifest.ts\` and run
|
|
503
|
+
\`bun cortex apply\` to change it; an apply changes only what the edit changed. The CLI reaches this
|
|
504
|
+
checkout's Cortex through \`CORTEX_URL\` and \`CORTEX_KEY\` in \`.env\`, so it needs no login, and
|
|
505
|
+
\`-p <profile>\` picks a saved one instead.
|
|
345
506
|
|
|
346
|
-
|
|
507
|
+
\`bun run dev:agent\` runs the same processes without the dashboard, each log line labelled with
|
|
508
|
+
its process, for coding agents and CI.
|
|
347
509
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
- \`index.ts\` — construction. It reads the config, calls \`createCortex\`, serves, and is where your
|
|
351
|
-
own routes go.
|
|
352
|
-
- \`.env\` — local secrets, loaded by Bun from the directory you run in. \`.env.example\` is the
|
|
353
|
-
committed copy.
|
|
510
|
+
The backend listens on http://localhost:{{port.server}}. Move it in \`index.ts\`, and
|
|
511
|
+
\`CORTEX_SERVER_URL\` and the server's URL in Cortex with it.
|
|
354
512
|
|
|
355
|
-
|
|
513
|
+
## Reaching this machine
|
|
514
|
+
|
|
515
|
+
Cortex's containers reach the backend at \`host.docker.internal\`, which \`EGRESS_ALLOWED_HOSTS\` in
|
|
516
|
+
\`.env\` admits. The policy admits a host by name, not by port, so every HTTP tool, MCP
|
|
517
|
+
server and credential you define in Cortex can reach any port this machine listens
|
|
518
|
+
on, over plain http. Keep the stack on your own machine, and define only tools you trust.
|
|
356
519
|
|
|
357
520
|
## Checks
|
|
358
521
|
|
|
@@ -363,36 +526,37 @@ pre-commit hook or CI job should run. Each is also available on its own: \`forma
|
|
|
363
526
|
}
|
|
364
527
|
|
|
365
528
|
/**
|
|
366
|
-
* Relative path → contents. Nothing here touches the filesystem
|
|
367
|
-
*
|
|
529
|
+
* Relative path → contents. Nothing here touches the filesystem; the ports
|
|
530
|
+
* are drawn by the caller.
|
|
368
531
|
*/
|
|
369
|
-
export function scaffoldFiles(
|
|
370
|
-
const
|
|
371
|
-
const envFile = env(database, features);
|
|
372
|
-
const compose = dockerCompose(name, database, features);
|
|
373
|
-
|
|
532
|
+
export function scaffoldFiles(input: Input) {
|
|
533
|
+
const project = { ...input, server: input.name.slice(0, SERVER_NAME_MAX) };
|
|
374
534
|
const files = {
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
535
|
+
"server.ts": serverTs,
|
|
536
|
+
"setup.ts": setupTs,
|
|
537
|
+
...(project.datasets ? { "datasets.ts": datasetsTs } : {}),
|
|
538
|
+
"index.ts": indexTs(project),
|
|
539
|
+
"cortex.manifest.ts": manifestTs(project),
|
|
540
|
+
"docker-compose.yml": dockerCompose(),
|
|
541
|
+
"package.json": packageJson(project),
|
|
378
542
|
"tsconfig.json": tsconfigJson,
|
|
379
|
-
".env":
|
|
380
|
-
".env
|
|
543
|
+
".env.example": envExample(project),
|
|
544
|
+
...(project.env ? { ".env": withEnvValues(envExample(project), project.env) } : {}),
|
|
381
545
|
".gitignore": gitignoreText,
|
|
382
|
-
"
|
|
546
|
+
".dockerignore": dockerignoreText,
|
|
547
|
+
"README.md": readme(project),
|
|
383
548
|
"eslint.config.mjs": eslintConfig,
|
|
384
549
|
"prettier.config.mjs": prettierConfig,
|
|
385
|
-
"cspell.json":
|
|
550
|
+
"cspell.json": cspellJson,
|
|
386
551
|
".fallowrc.jsonc": fallowrc,
|
|
387
|
-
...(compose === undefined ? {} : { "docker-compose.yml": compose }),
|
|
388
|
-
// The directory `domainsDir` defaults to, which makes the convention true
|
|
389
|
-
// by construction: `cortex swagger sync` finds it with no config key.
|
|
390
|
-
...(features.includes("knowledge") ? { "src/domains/README.md": domainsReadme } : {}),
|
|
391
552
|
} satisfies Record<string, string>;
|
|
392
553
|
|
|
393
554
|
// One pass at the end, over everything: no generator holds a port number of
|
|
394
|
-
// its own, so compose
|
|
555
|
+
// its own, so compose and the README cannot disagree about one.
|
|
395
556
|
return Object.fromEntries(
|
|
396
|
-
Object.entries(files).map(([path, contents]) => [
|
|
557
|
+
Object.entries(files).map(([path, contents]) => [
|
|
558
|
+
path,
|
|
559
|
+
applyPorts(contents, project.ports),
|
|
560
|
+
]),
|
|
397
561
|
);
|
|
398
562
|
}
|