@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/CHANGELOG.md +32 -0
- package/README.md +162 -7
- package/dist/src/app/commands.d.ts +76 -0
- package/dist/src/app/credentials.d.ts +12 -0
- package/dist/src/app/find-app.d.ts +6 -0
- package/dist/src/args.d.ts +18 -3
- package/dist/src/catalog.d.ts +9 -0
- package/dist/src/main.d.ts +7 -2
- package/dist/src/program.d.ts +22 -0
- package/package.json +4 -3
- package/src/app/commands.ts +214 -0
- package/src/app/credentials.ts +82 -0
- package/src/app/find-app.ts +25 -0
- package/src/args.ts +23 -33
- package/src/catalog.ts +10 -0
- package/src/main.ts +21 -62
- package/src/program.ts +259 -0
- package/src/render.ts +95 -13
- package/src/versions.ts +1 -1
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",
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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.
|
|
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
|
-
|
|
321
|
-
|
|
384
|
+
bunx mfw start
|
|
385
|
+
bunx mfw repl
|
|
322
386
|
\`\`\`
|
|
323
387
|
|
|
324
|
-
\`bun install\` here gives your editor
|
|
325
|
-
${tools
|
|
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
|
-
|
|
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
|
|
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";
|