@mercury-fw/cli 0.28.3 → 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 CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 0.28.3
4
11
 
5
12
  ### 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.
@@ -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>;
@@ -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",
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.3",
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.3",
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
- /** `mfw create`: returns the exit code. */
32
- async function create(args: CreateArgs): Promise<number> {
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/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
+ }