@mercury-fw/cli 0.25.1 → 0.27.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/src/render.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * written in catalog order, whatever order they were chosen in.
9
9
  */
10
10
  import { DEFAULT_PERSONA_TONE } from "@mercury-fw/core";
11
- import { CATALOG, type CatalogEntry, type EnvVar } from "./catalog.ts";
11
+ import { CATALOG, type CatalogEntry, type CliCredentials, type EnvVar } from "./catalog.ts";
12
12
  import indexTs from "../template/src/index.ts.tpl" with { type: "text" };
13
13
  import replTs from "../template/src/repl.ts.tpl" with { type: "text" };
14
14
  import markdownDts from "../template/markdown.d.ts.tpl" with { type: "text" };
@@ -68,7 +68,7 @@ export function renderApp(input: RenderInput): Map<string, string> {
68
68
  [".dockerignore", dockerignore],
69
69
  [".env.example", renderEnv(channels, tools)],
70
70
  [".gitignore", gitignore],
71
- ["Dockerfile", dockerfile],
71
+ ["Dockerfile", renderDockerfile(tools)],
72
72
  ["README.md", renderReadme(input.name, channels, tools)],
73
73
  ["docker-compose.yml", renderCompose(input.name, tools.length > 0)],
74
74
  ["markdown.d.ts", markdownDts],
@@ -81,9 +81,60 @@ export function renderApp(input: RenderInput): Map<string, string> {
81
81
  ["src/index.ts", indexTs],
82
82
  ["src/repl.ts", replTs],
83
83
  ["tsconfig.json", tsconfigJson],
84
+ ...(withCredentials(tools).length > 0
85
+ ? [["docker-entrypoint.sh", renderEntrypoint(withCredentials(tools))] as [string, string]]
86
+ : []),
84
87
  ]);
85
88
  }
86
89
 
90
+ /** The chosen tool plugins whose CLI needs credentials, with them. */
91
+ function withCredentials(tools: CatalogEntry[]): Array<CatalogEntry & { credentials: CliCredentials }> {
92
+ return tools.filter((t): t is CatalogEntry & { credentials: CliCredentials } => t.credentials !== undefined);
93
+ }
94
+
95
+ /** The template's Dockerfile; with CLI credentials to materialize, the service
96
+ * starts through `docker-entrypoint.sh` instead of directly. */
97
+ function renderDockerfile(tools: CatalogEntry[]): string {
98
+ if (withCredentials(tools).length === 0) return dockerfile;
99
+ const cmd = 'CMD ["bun", "src/index.ts"]\n';
100
+ if (!dockerfile.endsWith(cmd)) throw new Error("Dockerfile.tpl no longer ends with the service's CMD");
101
+ return `${dockerfile.slice(0, -cmd.length)}# Materializes the tool plugins' CLI credentials on the first start, then
102
+ # starts the service.
103
+ COPY --chown=mercury:mercury --chmod=755 docker-entrypoint.sh ./
104
+ CMD ["./docker-entrypoint.sh"]
105
+ `;
106
+ }
107
+
108
+ /** \`docker-entrypoint.sh\`: each plugin's credentials variable materialized
109
+ * into its CLI's folder on the volume, only while that folder isn't there,
110
+ * then the service. */
111
+ function renderEntrypoint(tools: Array<CatalogEntry & { credentials: CliCredentials }>): string {
112
+ const lines = tools.map((t) => `materialize ${t.credentials.folder} ${t.credentials.variable}`).join("\n");
113
+ return `#!/usr/bin/env bash
114
+ # Starts the service, first materializing each tool plugin's CLI credentials
115
+ # onto the cli-credentials volume: its variable in the env file is the CLI's
116
+ # config folder as a base64 tar.gz (bunx mfw credentials set <plugin> writes
117
+ # it). Only when that CLI's folder isn't on the volume yet: what a CLI writes
118
+ # back while running, like a refreshed token, stays there across redeploys,
119
+ # and an older value in the env file never overwrites it. bunx mfw credentials
120
+ # reset <plugin> clears one folder so its variable is materialized again.
121
+ set -euo pipefail
122
+
123
+ materialize() {
124
+ local folder="$1"
125
+ local variable="$2"
126
+ local value="\${!variable:-}"
127
+ if [[ -n "$value" && ! -d "/home/mercury/.config/$folder" ]]; then
128
+ echo "$value" | base64 -d | tar xzf - -C /home/mercury/.config
129
+ fi
130
+ }
131
+
132
+ ${lines}
133
+
134
+ exec bun src/index.ts
135
+ `;
136
+ }
137
+
87
138
  /** Why `name` can't be an app name, or undefined when it can. Shared with the
88
139
  * wizard, which checks the name as it's typed. */
89
140
  export function appNameError(name: string): string | undefined {
@@ -213,6 +264,10 @@ function renderPackageJson(
213
264
  }
214
265
  dependencies[pkg] = `^${version}`;
215
266
  }
267
+ const cli = versions["@mercury-fw/cli"];
268
+ if (cli === undefined) {
269
+ throw new Error("No version known for @mercury-fw/cli");
270
+ }
216
271
  const manifest: Record<string, unknown> = {
217
272
  name,
218
273
  version: "0.1.0",
@@ -220,7 +275,9 @@ function renderPackageJson(
220
275
  private: true,
221
276
  scripts: { start: "bun src/index.ts", repl: "bun src/repl.ts", typecheck: "tsc --noEmit" },
222
277
  dependencies,
223
- devDependencies: { "@types/bun": "1.4.0", typescript: "6.0.3" },
278
+ // The CLI in the app itself, so `bunx mfw` runs the version that matches
279
+ // the framework the app depends on.
280
+ devDependencies: { "@mercury-fw/cli": `^${cli}`, "@types/bun": "^1.4.0", typescript: "^6.0.3" },
224
281
  };
225
282
  if (tools.length > 0) {
226
283
  manifest.trustedDependencies = tools.map((t) => t.package).sort();
@@ -245,8 +302,15 @@ function renderEnv(channels: CatalogEntry[], tools: CatalogEntry[]): string {
245
302
  );
246
303
  }
247
304
  for (const entry of [...channels, ...tools]) {
248
- if (entry.env.length > 0) {
249
- sections.push(`# --- ${entry.id}\n${block(entry.env)}`);
305
+ const vars = [...entry.env];
306
+ if (entry.credentials !== undefined) {
307
+ vars.push({
308
+ name: entry.credentials.variable,
309
+ comment: `${entry.credentials.folder}'s config folder, packed: bunx mfw credentials set ${entry.id} writes it; materialized on the credentials volume at the first start without that folder`,
310
+ });
311
+ }
312
+ if (vars.length > 0) {
313
+ sections.push(`# --- ${entry.id}\n${block(vars)}`);
250
314
  }
251
315
  }
252
316
  return `${sections.join("\n\n")}\n`;
@@ -258,7 +322,7 @@ function renderCompose(name: string, hasTools: boolean): string {
258
322
  const credentialsMount = hasTools
259
323
  ? [
260
324
  " # The tool plugins' CLI credentials, on a volume so what a CLI writes back",
261
- " # (refreshed tokens) survives a redeploy. It starts empty: see README.md.",
325
+ " # (refreshed tokens) survives a redeploy. docker-entrypoint.sh fills it: see README.md.",
262
326
  " - cli-credentials:/home/mercury/.config",
263
327
  ]
264
328
  : [];
@@ -317,17 +381,35 @@ A Mercury app, scaffolded by \`mfw create\`.
317
381
  \`\`\`bash
318
382
  bun install
319
383
  cp .env.example .env
320
- docker compose up --build
321
- docker compose run --rm mercury bun run repl
384
+ bunx mfw start
385
+ bunx mfw repl
322
386
  \`\`\`
323
387
 
324
- \`bun install\` here gives your editor and \`bun run typecheck\` the packages (tool plugins download their CLI binary as they install); the image installs its own copy when it builds.
325
- ${tools.length > 0 ? CREDENTIALS_SECTION : ""}`;
388
+ \`bun install\` here gives your editor, \`bun run typecheck\` and \`mfw\` the packages (tool plugins download their CLI binary as they install); the image installs its own copy when it builds. \`bunx mfw start\` builds the image and starts the app with Qdrant in the background, \`bunx mfw repl\` opens a terminal conversation with the assistant. \`bunx mfw --help\` lists the rest: stopping and restarting, logs, a shell in the container, the wiki and the memory, and resetting them.
389
+ ${renderCredentialsSection(withCredentials(tools))}`;
326
390
  }
327
391
 
328
- /** The README section on CLI credentials, for an app with tool plugins. */
329
- const CREDENTIALS_SECTION = `
392
+ /** The README section on CLI credentials, for an app with tool plugins;
393
+ * nothing without one. */
394
+ function renderCredentialsSection(tools: Array<CatalogEntry & { credentials: CliCredentials }>): string {
395
+ if (tools.length === 0) return "";
396
+ const first = tools[0] as CatalogEntry & { credentials: CliCredentials };
397
+ const list = tools.map((t) => `\`${t.id}\` (\`~/.config/${t.credentials.folder}\`)`).join(", ");
398
+ return `
330
399
  ## CLI credentials
331
400
 
332
- Each tool plugin runs its own CLI, and each CLI keeps its login under \`/home/mercury/.config\` in the container, on the \`cli-credentials\` volume. The volume starts empty: nothing in this app provisions it yet, so authenticate each CLI once inside the container (\`docker compose run --rm mercury <cli> --help\` lists its auth commands) or copy its config folder into the volume. What the CLIs write back afterwards, like refreshed tokens, stays on the volume across redeploys.
401
+ Each tool plugin runs its own CLI, and each CLI keeps its login in a folder of its own: ${list}. Log in with the CLI on your machine first (its own README says how), then hand that folder to the app:
402
+
403
+ \`\`\`bash
404
+ bunx mfw credentials set ${first.id}
405
+ \`\`\`
406
+
407
+ It packs the folder into its variable in \`.env\` (\`--from <folder>\` if it isn't where the CLI usually keeps it, \`--print\` to get the line to paste on another host instead). When the container starts, \`docker-entrypoint.sh\` unpacks it onto the \`cli-credentials\` volume, but only if that CLI's folder isn't there yet: what the CLI writes back afterwards, like a refreshed token, stays on the volume across redeploys, and the older value in \`.env\` never overwrites it.
408
+
409
+ That same rule means a corrected variable does nothing while the old folder is on the volume. Clear it, and the next start unpacks the variable again:
410
+
411
+ \`\`\`bash
412
+ bunx mfw credentials reset ${first.id}
413
+ \`\`\`
333
414
  `;
415
+ }
package/src/versions.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import pkg from "../package.json";
8
8
 
9
9
  /** The framework packages a new app can depend on: always at the CLI's version. */
10
- export const FRAMEWORK_PACKAGES = ["@mercury-fw/core", "@mercury-fw/formatter"];
10
+ export const FRAMEWORK_PACKAGES = ["@mercury-fw/cli", "@mercury-fw/core", "@mercury-fw/formatter"];
11
11
 
12
12
  /** The registry asked when `MFW_REGISTRY` doesn't name another. */
13
13
  export const DEFAULT_REGISTRY = "https://registry.npmjs.org";