@mercury-fw/cli 0.28.2 → 0.28.4
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 +14 -0
- package/README.md +2 -0
- package/dist/src/main.d.ts +6 -2
- package/dist/src/versions.d.ts +11 -0
- package/package.json +3 -3
- package/src/main.ts +54 -6
- package/src/render.ts +24 -4
- package/src/versions.ts +42 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @mercury-fw/cli
|
|
2
2
|
|
|
3
|
+
## 0.28.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 65e78fe: `mfw create` (and `bun create mercury-agent`) checks the registry for a newer `@mercury-fw/cli` first, and when it's behind, as a copy left in Bun's bunx cache can be, it runs the same command through the newer version instead of writing an outdated app.
|
|
8
|
+
- @mercury-fw/core@0.28.4
|
|
9
|
+
|
|
10
|
+
## 0.28.3
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 5175e10: An app created with the HTTP channel publishes the surface's port on the host (`HTTP_SURFACE_PORT`, 4100 when unset), so it's reachable from outside the container; its README says where it listens and that it has no authentication.
|
|
15
|
+
- @mercury-fw/core@0.28.3
|
|
16
|
+
|
|
3
17
|
## 0.28.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ bunx @mercury-fw/cli create my-agent --assistant-name Hermes --channels http --p
|
|
|
60
60
|
|
|
61
61
|
The framework packages get the CLI's own version (they're released together); each chosen plugin or channel gets its latest version on the registry, `https://registry.npmjs.org` unless `MFW_REGISTRY` names another.
|
|
62
62
|
|
|
63
|
+
Before anything else it asks the registry for the latest `@mercury-fw/cli`: a newer one than itself means it's a stale copy (Bun keeps the `create-mercury-agent` that `bun create` ran last in its cache, a release behind), so it says so and runs the same command through the newer version (`bunx @mercury-fw/cli@<newer> create …`), which writes the app instead. A registry that doesn't answer within a few seconds, or a newer version that can't be installed yet, is only a warning, and the CLI carries on with itself. Only CLIs from 0.28.4 on do this: a copy older than that, still in Bun's cache, needs one last `bun pm cache rm`, run from any folder with a `package.json` (Bun refuses it elsewhere).
|
|
64
|
+
|
|
63
65
|
### `mfw start [--no-cache]`
|
|
64
66
|
|
|
65
67
|
Builds the app's image and starts the app and Qdrant in the background (`docker compose up -d --build`). Docker's cache means only what changed gets rebuilt, and a running container is recreated only if its image or configuration changed, so it's also the command to run after changing a dependency, the Dockerfile or `.env`. `--no-cache` rebuilds everything from scratch (`docker compose build --no-cache`, then `up -d`), which is what refetches a tool plugin's CLI binary when a new release is out: a normal build keeps the cached one.
|
package/dist/src/main.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { type AppDeps } from "./app/commands.ts";
|
|
2
|
+
/** Runs `argv` with stdio inherited and `env` on top of this process's
|
|
3
|
+
* environment; returns its exit code. */
|
|
4
|
+
export type Relaunch = (argv: string[], env: Record<string, string>) => Promise<number>;
|
|
2
5
|
/** Runs `mfw` with `argv` (the arguments after the command name) and returns
|
|
3
6
|
* the exit code, printing to stdout/stderr. `bin.ts` and `create-mercury-agent`
|
|
4
7
|
* both call it; the app commands look for the app from `cwd` and run docker
|
|
5
|
-
* through `deps
|
|
6
|
-
export declare function main(argv: string[], { cwd, deps }?: {
|
|
8
|
+
* through `deps`; `relaunch` is how `create` hands over to a newer CLI. */
|
|
9
|
+
export declare function main(argv: string[], { cwd, deps, relaunch }?: {
|
|
7
10
|
cwd?: string;
|
|
8
11
|
deps?: AppDeps;
|
|
12
|
+
relaunch?: Relaunch;
|
|
9
13
|
}): Promise<number>;
|
package/dist/src/versions.d.ts
CHANGED
|
@@ -14,3 +14,14 @@ export declare function appVersions(packages: string[], opts: {
|
|
|
14
14
|
registry: string;
|
|
15
15
|
fetchFn?: typeof fetch;
|
|
16
16
|
}): Promise<Record<string, string>>;
|
|
17
|
+
/** The registry's `latest` `@mercury-fw/cli` when it's newer than `current`
|
|
18
|
+
* (this CLI's version by default), nothing otherwise: a CLI run from Bun's
|
|
19
|
+
* bunx cache can be behind the release it was meant to be. Rejects like
|
|
20
|
+
* `appVersions` when the registry can't answer, within `timeoutMs` (5s by
|
|
21
|
+
* default: this check runs on every create, also one needing no network). */
|
|
22
|
+
export declare function newerCli(opts: {
|
|
23
|
+
registry: string;
|
|
24
|
+
fetchFn?: typeof fetch;
|
|
25
|
+
current?: string;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}): Promise<string | undefined>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/cli",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"comment:shape": "The Mercury CLI (`mfw`). `mfw create <dir>` writes a new Mercury app from the template. The framework packages move in lockstep with it, so a new app gets them at the CLI's own version; plugins and channels at the registry's latest. The plugin and channel packages are devDependencies only for the catalog test. `main(argv)` is exported for create-mercury-agent.",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@clack/prompts": "^1.8.1",
|
|
39
|
-
"@mercury-fw/core": "0.28.
|
|
39
|
+
"@mercury-fw/core": "0.28.4",
|
|
40
40
|
"commander": "^15.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@mercury-fw/channel-google-chat": "0.1.2",
|
|
44
44
|
"@mercury-fw/channel-http": "0.1.0",
|
|
45
|
-
"@mercury-fw/formatter": "0.28.
|
|
45
|
+
"@mercury-fw/formatter": "0.28.4",
|
|
46
46
|
"@mercury-fw/plugin-atlassian-admin": "0.1.0",
|
|
47
47
|
"@mercury-fw/plugin-bitbucket": "0.1.0",
|
|
48
48
|
"@mercury-fw/plugin-jira": "0.1.1",
|
package/src/main.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { CATALOG } from "./catalog.ts";
|
|
|
11
11
|
import { kebabCase } from "./naming.ts";
|
|
12
12
|
import { runProgram } from "./program.ts";
|
|
13
13
|
import { renderApp, selectionError } from "./render.ts";
|
|
14
|
-
import { appVersions, registryFrom } from "./versions.ts";
|
|
14
|
+
import { appVersions, cliVersion, newerCli, registryFrom } from "./versions.ts";
|
|
15
15
|
import { appCommands, terminalDeps, type AppDeps } from "./app/commands.ts";
|
|
16
16
|
import { findApp } from "./app/find-app.ts";
|
|
17
17
|
import { askAnswers, DEFAULT_ASSISTANT_NAME, DEFAULT_ROLE, type Answers } from "./wizard.ts";
|
|
@@ -28,8 +28,55 @@ function answersFromFlags(args: CreateArgs, defaultName: string): Answers {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** `
|
|
32
|
-
|
|
31
|
+
/** Runs `argv` with stdio inherited and `env` on top of this process's
|
|
32
|
+
* environment; returns its exit code. */
|
|
33
|
+
export type Relaunch = (argv: string[], env: Record<string, string>) => Promise<number>;
|
|
34
|
+
|
|
35
|
+
/** The real relaunch: a child process on the user's terminal. */
|
|
36
|
+
const spawnRelaunch: Relaunch = async (argv, env) => {
|
|
37
|
+
const proc = Bun.spawn(argv, { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: { ...process.env, ...env } });
|
|
38
|
+
return await proc.exited;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** When the registry has a newer `@mercury-fw/cli` than this one (a stale copy
|
|
42
|
+
* out of Bun's bunx cache), installs it (`bunx … --version`), then runs
|
|
43
|
+
* `create` again through it with the same arguments as typed and returns its
|
|
44
|
+
* exit code, whatever it is (a cancelled wizard included). `undefined` means
|
|
45
|
+
* carry on here: no newer CLI, a check skipped inside a relaunch
|
|
46
|
+
* (`MFW_SELF_UPDATED`), a registry that can't answer, or a newer CLI that
|
|
47
|
+
* can't be installed; the last two with a warning. */
|
|
48
|
+
async function relaunchIfStale(rawArgs: string[], relaunch: Relaunch): Promise<number | undefined> {
|
|
49
|
+
if (process.env.MFW_SELF_UPDATED) return undefined;
|
|
50
|
+
const registry = registryFrom(process.env.MFW_REGISTRY).replace(/\/+$/, "");
|
|
51
|
+
let newer: string | undefined;
|
|
52
|
+
try {
|
|
53
|
+
newer = await newerCli({ registry });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`couldn't check for a newer mfw: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (newer === undefined) return undefined;
|
|
59
|
+
const cli = `@mercury-fw/cli@${newer}`;
|
|
60
|
+
const env = { MFW_SELF_UPDATED: newer, NPM_CONFIG_REGISTRY: registry };
|
|
61
|
+
let installed: number;
|
|
62
|
+
try {
|
|
63
|
+
installed = await relaunch(["bunx", cli, "--version"], env);
|
|
64
|
+
} catch {
|
|
65
|
+
installed = -1;
|
|
66
|
+
}
|
|
67
|
+
if (installed !== 0) {
|
|
68
|
+
console.error(`mfw ${cliVersion()} is behind the registry's ${newer}, which couldn't be installed: creating with ${cliVersion()}.`);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
console.error(`mfw ${cliVersion()} is behind the registry's ${newer}: running ${newer} instead.`);
|
|
72
|
+
return relaunch(["bunx", cli, "create", ...rawArgs], env);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `mfw create`: returns the exit code. `rawArgs` are the arguments after
|
|
76
|
+
* `create` as typed, for a relaunch. */
|
|
77
|
+
async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch): Promise<number> {
|
|
78
|
+
const relaunched = await relaunchIfStale(rawArgs, relaunch);
|
|
79
|
+
if (relaunched !== undefined) return relaunched;
|
|
33
80
|
// The folder is created in kebab case, only its own name: the parent path is
|
|
34
81
|
// taken as typed. Its name is also the app name's default.
|
|
35
82
|
const typed = resolve(args.dir);
|
|
@@ -66,13 +113,14 @@ Next:
|
|
|
66
113
|
/** Runs `mfw` with `argv` (the arguments after the command name) and returns
|
|
67
114
|
* the exit code, printing to stdout/stderr. `bin.ts` and `create-mercury-agent`
|
|
68
115
|
* both call it; the app commands look for the app from `cwd` and run docker
|
|
69
|
-
* through `deps
|
|
116
|
+
* through `deps`; `relaunch` is how `create` hands over to a newer CLI. */
|
|
70
117
|
export async function main(
|
|
71
118
|
argv: string[],
|
|
72
|
-
{ cwd = process.cwd(), deps }: { cwd?: string; deps?: AppDeps } = {},
|
|
119
|
+
{ cwd = process.cwd(), deps, relaunch = spawnRelaunch }: { cwd?: string; deps?: AppDeps; relaunch?: Relaunch } = {},
|
|
73
120
|
): Promise<number> {
|
|
121
|
+
const rawCreateArgs = argv.slice(argv.indexOf("create") + 1);
|
|
74
122
|
return runProgram(argv, {
|
|
75
|
-
create,
|
|
123
|
+
create: (args) => create(args, rawCreateArgs, relaunch),
|
|
76
124
|
app: () => appCommands(findApp(cwd), deps ?? terminalDeps()),
|
|
77
125
|
});
|
|
78
126
|
}
|
package/src/render.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function renderApp(input: RenderInput): Map<string, string> {
|
|
|
70
70
|
[".gitignore", gitignore],
|
|
71
71
|
["Dockerfile", renderDockerfile(tools)],
|
|
72
72
|
["README.md", renderReadme(input.name, channels, tools)],
|
|
73
|
-
["docker-compose.yml", renderCompose(input.name, tools.length > 0)],
|
|
73
|
+
["docker-compose.yml", renderCompose(input.name, tools.length > 0, channels.some((c) => c.id === "http"))],
|
|
74
74
|
["markdown.d.ts", markdownDts],
|
|
75
75
|
["mercury.config.ts", renderConfig(channels, tools)],
|
|
76
76
|
["package.json", renderPackageJson(input.name, channels, tools, input.versions)],
|
|
@@ -318,8 +318,9 @@ function renderEnv(channels: CatalogEntry[], tools: CatalogEntry[]): string {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/** `docker-compose.yml`: the app and Qdrant, with named volumes prefixed by the
|
|
321
|
-
* app's name; the CLI credentials volume only when a tool plugin was chosen
|
|
322
|
-
|
|
321
|
+
* app's name; the CLI credentials volume only when a tool plugin was chosen,
|
|
322
|
+
* the HTTP surface's port published on the host only with the HTTP channel. */
|
|
323
|
+
function renderCompose(name: string, hasTools: boolean, hasHttp: boolean): string {
|
|
323
324
|
const credentialsMount = hasTools
|
|
324
325
|
? [
|
|
325
326
|
" # The tool plugins' CLI credentials, on a volume so what a CLI writes back",
|
|
@@ -327,6 +328,13 @@ function renderCompose(name: string, hasTools: boolean): string {
|
|
|
327
328
|
" - cli-credentials:/home/mercury/.config",
|
|
328
329
|
]
|
|
329
330
|
: [];
|
|
331
|
+
const httpPort = hasHttp
|
|
332
|
+
? [
|
|
333
|
+
" # The HTTP surface, on the host: no authentication, keep it off the public network.",
|
|
334
|
+
" ports:",
|
|
335
|
+
' - "${HTTP_SURFACE_PORT:-4100}:${HTTP_SURFACE_PORT:-4100}"',
|
|
336
|
+
]
|
|
337
|
+
: [];
|
|
330
338
|
const credentialsVolume = hasTools ? [" cli-credentials:", ` name: ${name}_cli-credentials`] : [];
|
|
331
339
|
return [
|
|
332
340
|
"services:",
|
|
@@ -340,6 +348,7 @@ function renderCompose(name: string, hasTools: boolean): string {
|
|
|
340
348
|
" volumes:",
|
|
341
349
|
" - wiki-vault:/app/wiki-vault",
|
|
342
350
|
...credentialsMount,
|
|
351
|
+
...httpPort,
|
|
343
352
|
" extra_hosts:",
|
|
344
353
|
' - "host.docker.internal:host-gateway"',
|
|
345
354
|
" depends_on:",
|
|
@@ -387,7 +396,18 @@ bunx mfw repl
|
|
|
387
396
|
\`\`\`
|
|
388
397
|
|
|
389
398
|
\`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.
|
|
390
|
-
${renderCredentialsSection(withCredentials(tools))}`;
|
|
399
|
+
${renderHttpSection(channels)}${renderCredentialsSection(withCredentials(tools))}`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The README section on the HTTP surface, for an app with the HTTP channel;
|
|
403
|
+
* nothing without it. */
|
|
404
|
+
function renderHttpSection(channels: CatalogEntry[]): string {
|
|
405
|
+
if (!channels.some((c) => c.id === "http")) return "";
|
|
406
|
+
return `
|
|
407
|
+
## HTTP surface
|
|
408
|
+
|
|
409
|
+
The HTTP channel listens on \`http://<host>:4100\`, the port in \`HTTP_SURFACE_PORT\` (the compose file publishes whatever port it holds on the host). It has no authentication: whoever reaches the port can talk to the assistant, so keep it on a network you trust.
|
|
410
|
+
`;
|
|
391
411
|
}
|
|
392
412
|
|
|
393
413
|
/** The README section on CLI credentials, for an app with tool plugins;
|
package/src/versions.ts
CHANGED
|
@@ -23,6 +23,32 @@ export function cliVersion(): string {
|
|
|
23
23
|
return pkg.version;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** The registry's `latest` version of `name`. Rejects naming the package the
|
|
27
|
+
* registry doesn't have, or saying the registry can't be reached (also when
|
|
28
|
+
* it hasn't answered within `timeoutMs`, if given). */
|
|
29
|
+
async function latestVersion(
|
|
30
|
+
name: string,
|
|
31
|
+
opts: { registry: string; fetchFn?: typeof fetch; timeoutMs?: number },
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const registry = opts.registry.replace(/\/+$/, "");
|
|
34
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
35
|
+
let res: Response;
|
|
36
|
+
try {
|
|
37
|
+
const signal = opts.timeoutMs === undefined ? undefined : AbortSignal.timeout(opts.timeoutMs);
|
|
38
|
+
res = await fetchFn(`${registry}/${name.replace("/", "%2F")}/latest`, { signal });
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`Can't reach ${registry} to look up ${name}`);
|
|
41
|
+
}
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(`${name} is not on ${registry}`);
|
|
44
|
+
}
|
|
45
|
+
const body = (await res.json().catch(() => undefined)) as { version?: unknown } | undefined;
|
|
46
|
+
if (typeof body?.version !== "string") {
|
|
47
|
+
throw new Error(`${registry} gave no version for ${name}`);
|
|
48
|
+
}
|
|
49
|
+
return body.version;
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
/** Maps the framework packages to the CLI's version and each of `packages`
|
|
27
53
|
* (plugins and channels) to the registry's `latest`. Rejects naming the
|
|
28
54
|
* package the registry doesn't have, or saying the registry can't be reached. */
|
|
@@ -30,29 +56,25 @@ export async function appVersions(
|
|
|
30
56
|
packages: string[],
|
|
31
57
|
opts: { registry: string; fetchFn?: typeof fetch },
|
|
32
58
|
): Promise<Record<string, string>> {
|
|
33
|
-
const registry = opts.registry.replace(/\/+$/, "");
|
|
34
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
35
59
|
const versions: Record<string, string> = Object.fromEntries(FRAMEWORK_PACKAGES.map((p) => [p, cliVersion()]));
|
|
36
|
-
const latest = await Promise.all(
|
|
37
|
-
packages.map(async (name) => {
|
|
38
|
-
let res: Response;
|
|
39
|
-
try {
|
|
40
|
-
res = await fetchFn(`${registry}/${name.replace("/", "%2F")}/latest`);
|
|
41
|
-
} catch {
|
|
42
|
-
throw new Error(`Can't reach ${registry} to look up ${name}`);
|
|
43
|
-
}
|
|
44
|
-
if (!res.ok) {
|
|
45
|
-
throw new Error(`${name} is not on ${registry}`);
|
|
46
|
-
}
|
|
47
|
-
const body = (await res.json().catch(() => undefined)) as { version?: unknown } | undefined;
|
|
48
|
-
if (typeof body?.version !== "string") {
|
|
49
|
-
throw new Error(`${registry} gave no version for ${name}`);
|
|
50
|
-
}
|
|
51
|
-
return [name, body.version] as const;
|
|
52
|
-
}),
|
|
53
|
-
);
|
|
60
|
+
const latest = await Promise.all(packages.map(async (name) => [name, await latestVersion(name, opts)] as const));
|
|
54
61
|
for (const [name, version] of latest) {
|
|
55
62
|
versions[name] = version;
|
|
56
63
|
}
|
|
57
64
|
return versions;
|
|
58
65
|
}
|
|
66
|
+
|
|
67
|
+
/** The registry's `latest` `@mercury-fw/cli` when it's newer than `current`
|
|
68
|
+
* (this CLI's version by default), nothing otherwise: a CLI run from Bun's
|
|
69
|
+
* bunx cache can be behind the release it was meant to be. Rejects like
|
|
70
|
+
* `appVersions` when the registry can't answer, within `timeoutMs` (5s by
|
|
71
|
+
* default: this check runs on every create, also one needing no network). */
|
|
72
|
+
export async function newerCli(opts: {
|
|
73
|
+
registry: string;
|
|
74
|
+
fetchFn?: typeof fetch;
|
|
75
|
+
current?: string;
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
}): Promise<string | undefined> {
|
|
78
|
+
const latest = await latestVersion("@mercury-fw/cli", { ...opts, timeoutMs: opts.timeoutMs ?? 5000 });
|
|
79
|
+
return Bun.semver.order(latest, opts.current ?? cliVersion()) === 1 ? latest : undefined;
|
|
80
|
+
}
|