@m6d/cortex-cli 0.13.0 → 0.15.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.
@@ -6,7 +6,8 @@ import type { Command } from "@commander-js/extra-typings";
6
6
  import { CORTEX_MANIFEST_ID } from "@cortex/contracts/manifest";
7
7
 
8
8
  import pkg from "../../package.json";
9
- import { manifestIdOf, scaffoldFiles } from "@/scaffold/files";
9
+ import { askProvider } from "@/commands/setup";
10
+ import { AGENT_SLUG, manifestIdOf, scaffoldFiles } from "@/scaffold/files";
10
11
  import { allocatePorts, type Ports } from "@/scaffold/ports";
11
12
  import { askConfirm, askName, intro, log, outro, runSteps } from "@/ui/prompts";
12
13
  import * as report from "@/ui/report";
@@ -24,12 +25,13 @@ export function registerNew(program: Command<[], { verbose?: true }>) {
24
25
  .helpGroup("Project:")
25
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
- // Both halves of the toggle, so the flag can answer its question either
28
- // way and `--yes` is not the only way to say "no". Declaring the positive
29
- // first leaves the value `undefined` when neither is passed, which is
30
- // what marks the question as still unanswered.
31
28
  .option("--datasets", "Serve an example table as a dataset host")
32
- .option("--no-datasets", "Leave the dataset host out")
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")
33
35
  .option("--yes", "Accept the defaults instead of asking");
34
36
 
35
37
  command.action(async function (name, options) {
@@ -39,7 +41,7 @@ export function registerNew(program: Command<[], { verbose?: true }>) {
39
41
  });
40
42
  }
41
43
 
42
- type Options = { datasets?: boolean; yes?: boolean; verbose: boolean };
44
+ type Options = { datasets?: true; setup?: boolean; yes?: true; verbose: boolean };
43
45
 
44
46
  async function scaffold(name: string | undefined, options: Options) {
45
47
  const { verbose } = options;
@@ -61,17 +63,15 @@ async function scaffold(name: string | undefined, options: Options) {
61
63
 
62
64
  const project = name ?? (await askName(checkName)).trim();
63
65
 
64
- // A flag settles the question; otherwise it is asked, or under `--yes` left out.
65
- const datasets =
66
- options.datasets ??
67
- (interactive && (await askConfirm("Serve an example table as a dataset host?")));
66
+ const datasets = options.datasets === true;
67
+ const { setup, provider } = await askSetup(options.setup, interactive);
68
68
 
69
69
  const target = resolve(process.cwd(), project);
70
70
 
71
71
  // Drawn here rather than inside the generators, which stay pure: picking a
72
72
  // port means probing the machine for a free one.
73
73
  const ports = allocatePorts();
74
- const files = scaffoldFiles({ name: project, datasets, ports });
74
+ const files = scaffoldFiles({ name: project, datasets, setup, ports, env: provider?.env });
75
75
  let failedInstall: string | undefined;
76
76
 
77
77
  await runSteps([
@@ -81,7 +81,8 @@ async function scaffold(name: string | undefined, options: Options) {
81
81
  for (const [path, contents] of Object.entries(files)) {
82
82
  const file = join(target, path);
83
83
  mkdirSync(dirname(file), { recursive: true });
84
- writeFileSync(file, contents);
84
+ // .env holds the provider's key before setup.ts adds the rest.
85
+ writeFileSync(file, contents, { mode: path === ".env" ? 0o600 : 0o644 });
85
86
  }
86
87
 
87
88
  const summary = [
@@ -89,6 +90,7 @@ async function scaffold(name: string | undefined, options: Options) {
89
90
  ...(datasets ? ["dataset host"] : []),
90
91
  "manifest",
91
92
  "Cortex",
93
+ ...(setup ? ["first-run setup"] : []),
92
94
  ];
93
95
  return `Wrote ${Object.keys(files).length} files to ./${project}\n${summary.join(" · ")}`;
94
96
  },
@@ -136,7 +138,25 @@ async function scaffold(name: string | undefined, options: Options) {
136
138
  if (failedInstall !== undefined) warnInstall(project, failedInstall, verbose);
137
139
 
138
140
  outro(`Done in ${seconds(started)}`);
139
- process.stdout.write(nextSteps(project, ports));
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 };
140
160
  }
141
161
 
142
162
  /**
@@ -156,17 +176,20 @@ function warnInstall(project: string, output: string, verbose: boolean) {
156
176
  );
157
177
  }
158
178
 
159
- function nextSteps(project: string, ports: Ports) {
160
- return (
161
- `\nNext steps:\n\n` +
162
- step(`cd ${project}`) +
163
- step("edit cortex.env", "fill in EMBEDDING_API_KEY") +
164
- step("docker compose up -d", `→ http://localhost:${ports.cortex}`) +
165
- // The port the generated index.ts listens on; only Cortex's ports move.
166
- step("bun run dev", "→ http://localhost:3331") +
167
- step("README.md", "register the server, then cortex apply") +
168
- `\n`
169
- );
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`;
170
193
  }
171
194
 
172
195
  async function install(cwd: string) {