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