@mercury-fw/cli 0.28.4 → 0.29.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 +10 -0
- package/README.md +43 -31
- package/dist/src/main.d.ts +5 -2
- package/dist/src/program.d.ts +2 -0
- package/package.json +3 -3
- package/src/app/commands.ts +3 -3
- package/src/main.ts +100 -13
- package/src/program.ts +12 -0
- package/src/render.ts +18 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @mercury-fw/cli
|
|
2
2
|
|
|
3
|
+
## 0.29.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e498dad: `mfw` is meant to be installed globally (`bun add -g @mercury-fw/cli`) and used as a plain command. Inside an app, a global `mfw` at another version hands the command over to the app's own CLI, so the app's commands always match its framework. `mfw upgrade` installs the registry's latest globally, and `mfw create` run by a stale `mfw` says to. The CLI's messages, the generated app and the docs say `mfw` instead of `bunx mfw`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- @mercury-fw/core@0.29.0
|
|
12
|
+
|
|
3
13
|
## 0.28.4
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -20,17 +20,21 @@
|
|
|
20
20
|
- [`mfw credentials set <plugin> [--from <dir>] [--print]`](#mfw-credentials-set-plugin---from-dir---print)
|
|
21
21
|
- [`mfw credentials reset <plugin>`](#mfw-credentials-reset-plugin)
|
|
22
22
|
- [`mfw google-chat set-key <key-file> [--subscription <name>]`](#mfw-google-chat-set-key-key-file---subscription-name)
|
|
23
|
+
- [`mfw upgrade`](#mfw-upgrade)
|
|
23
24
|
- [Help](#help)
|
|
24
25
|
|
|
25
26
|
## Getting it
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Install it once, globally, and `mfw` works from anywhere (Bun puts it in `~/.bun/bin`, which its installer adds to the `PATH`):
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
|
-
bun
|
|
31
|
+
bun add -g @mercury-fw/cli
|
|
32
|
+
mfw create my-agent
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
`mfw upgrade` keeps it current. Every app also lists `@mercury-fw/cli` among its devDependencies, at the framework's version, and inside an app the global `mfw` hands its commands over to that one when the two differ (it says so in one line; `mfw --version` and `mfw --help` stay the global one's), so an app's commands always match the framework it runs, whatever version is installed globally. An app not installed yet (no `bun install`) runs on the global one.
|
|
36
|
+
|
|
37
|
+
Without a global install: `bun create mercury-agent my-agent` creates an app, and `bunx mfw <command>` runs the app's own CLI from inside it.
|
|
34
38
|
|
|
35
39
|
## Usage
|
|
36
40
|
|
|
@@ -54,8 +58,8 @@ It writes the `mercury.config.ts` for that selection, the persona, the service a
|
|
|
54
58
|
| `-y`, `--yes` | No questions: the flags, and the defaults for the rest. |
|
|
55
59
|
|
|
56
60
|
```bash
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
mfw create my-agent
|
|
62
|
+
mfw create my-agent --assistant-name Hermes --channels http --plugins jira --yes
|
|
59
63
|
```
|
|
60
64
|
|
|
61
65
|
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.
|
|
@@ -67,8 +71,8 @@ Before anything else it asks the registry for the latest `@mercury-fw/cli`: a ne
|
|
|
67
71
|
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.
|
|
68
72
|
|
|
69
73
|
```bash
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
mfw start
|
|
75
|
+
mfw start --no-cache
|
|
72
76
|
```
|
|
73
77
|
|
|
74
78
|
### `mfw stop`
|
|
@@ -76,7 +80,7 @@ bunx mfw start --no-cache
|
|
|
76
80
|
Stops the app and Qdrant and removes their containers (`docker compose down`). The volumes stay, so memory, the wiki and the CLI credentials are all there on the next start.
|
|
77
81
|
|
|
78
82
|
```bash
|
|
79
|
-
|
|
83
|
+
mfw stop
|
|
80
84
|
```
|
|
81
85
|
|
|
82
86
|
### `mfw restart [--no-cache]`
|
|
@@ -84,8 +88,8 @@ bunx mfw stop
|
|
|
84
88
|
Like `start`, but recreates the containers even when nothing changed (`--force-recreate`): a clean restart, for a process that got stuck. A changed `.env` doesn't need it, `start` already recreates what the change touches. `--no-cache` as in `start`.
|
|
85
89
|
|
|
86
90
|
```bash
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
mfw restart
|
|
92
|
+
mfw restart --no-cache
|
|
89
93
|
```
|
|
90
94
|
|
|
91
95
|
### `mfw logs [service]`
|
|
@@ -93,8 +97,8 @@ bunx mfw restart --no-cache
|
|
|
93
97
|
Follows the logs of every service, interleaved, or only of the one you name, `mercury` or `qdrant` (`docker compose logs -f`). One service at most. `Ctrl+C` stops following, the app keeps running.
|
|
94
98
|
|
|
95
99
|
```bash
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
mfw logs
|
|
101
|
+
mfw logs mercury
|
|
98
102
|
```
|
|
99
103
|
|
|
100
104
|
### `mfw repl`
|
|
@@ -102,7 +106,7 @@ bunx mfw logs mercury
|
|
|
102
106
|
Opens the dev REPL, a conversation with the assistant in the terminal, in a one-off container (`docker compose run --rm mercury bun run repl`). The REPL has no user identity by design, so what you try there never lands in anyone's memory; ending it (`Ctrl+D`) removes the one-off container and leaves a running app alone.
|
|
103
107
|
|
|
104
108
|
```bash
|
|
105
|
-
|
|
109
|
+
mfw repl
|
|
106
110
|
```
|
|
107
111
|
|
|
108
112
|
### `mfw shell`
|
|
@@ -110,7 +114,7 @@ bunx mfw repl
|
|
|
110
114
|
Opens a shell in the app's container: the running one if the app is up (`docker compose exec mercury bash`), a one-off one otherwise (`docker compose run --rm mercury bash`). The tool plugins' CLIs are on `PATH` there with their credentials, so it's where you check that a command works before blaming the model.
|
|
111
115
|
|
|
112
116
|
```bash
|
|
113
|
-
|
|
117
|
+
mfw shell
|
|
114
118
|
```
|
|
115
119
|
|
|
116
120
|
### `mfw vault <command>`
|
|
@@ -126,10 +130,10 @@ Maintains the wiki vault, which lives on a Docker volume and not in the app's fo
|
|
|
126
130
|
| `write-raw <path>` | Writes raw material for the nightly review to triage, the body read from stdin. |
|
|
127
131
|
|
|
128
132
|
```bash
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
cat note.md |
|
|
133
|
+
mfw vault list
|
|
134
|
+
mfw vault read curated/standards/jira-fields.md
|
|
135
|
+
mfw vault grep "story points"
|
|
136
|
+
cat note.md | mfw vault write-curated curated/standards/new-note.md --author luca
|
|
133
137
|
```
|
|
134
138
|
|
|
135
139
|
There's no command writing inferred notes on purpose: those are the agent's own, written only by its consolidation.
|
|
@@ -139,7 +143,7 @@ There's no command writing inferred notes on purpose: those are the agent's own,
|
|
|
139
143
|
Lists the collections of the memory on Qdrant with how many points each holds, in a one-off container that reaches Qdrant the way the app does. Read-only.
|
|
140
144
|
|
|
141
145
|
```bash
|
|
142
|
-
|
|
146
|
+
mfw memory list
|
|
143
147
|
```
|
|
144
148
|
|
|
145
149
|
```
|
|
@@ -154,19 +158,19 @@ verbatim_archive 0 points
|
|
|
154
158
|
Prints a collection's points, each as its id followed by one line per payload field. Newest first where the collection has a timestamp index (episodic memory and the verbatim archive do); in Qdrant's own order otherwise, and it says so. `--limit` defaults to 20. Read-only.
|
|
155
159
|
|
|
156
160
|
```bash
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
mfw memory read episodic_memory
|
|
162
|
+
mfw memory read semantic_facts --limit 5
|
|
159
163
|
```
|
|
160
164
|
|
|
161
165
|
### `mfw reset <memory|wiki>`
|
|
162
166
|
|
|
163
167
|
Deletes for good what the assistant remembers: `memory` is every collection on Qdrant, `wiki` the whole vault. It reads the volume's real name from the compose file, tells you which one is about to go, and asks you to type the app's name (the `name` in `package.json`); anything else, an empty answer, a `y` or closing the input included, deletes nothing and exits 1. Once confirmed it stops the service using the volume, removes its container and the volume, and starts the service again on an empty one (`docker compose stop`, `rm -f`, `docker volume rm`, `up -d`). After `memory`, a running app is restarted too (`docker compose restart mercury`), since it sets up its collections only when it starts.
|
|
164
168
|
|
|
165
|
-
If a step fails once the service is stopped (the volume still in use by a one-off container, say), it stops there and says the service is down: `
|
|
169
|
+
If a step fails once the service is stopped (the volume still in use by a one-off container, say), it stops there and says the service is down: `mfw start` brings it back.
|
|
166
170
|
|
|
167
171
|
```bash
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
mfw reset memory
|
|
173
|
+
mfw reset wiki
|
|
170
174
|
```
|
|
171
175
|
|
|
172
176
|
Useful for clearing out test data; the other layer isn't touched.
|
|
@@ -178,9 +182,9 @@ Hands a tool plugin's CLI its login. Every such CLI keeps it in a config folder
|
|
|
178
182
|
When the container starts, the app's `docker-entrypoint.sh` unpacks the variable onto the credentials volume, but only if that CLI's folder isn't there yet: what the CLI writes back while running, like a refreshed token, stays on the volume across redeploys, and an older value in `.env` never overwrites it. `<plugin>` has to be a tool plugin the app depends on.
|
|
179
183
|
|
|
180
184
|
```bash
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
mfw credentials set jira
|
|
186
|
+
mfw credentials set bitbucket --from ~/work/bitbucket-login
|
|
187
|
+
mfw credentials set jira --print
|
|
184
188
|
```
|
|
185
189
|
|
|
186
190
|
### `mfw credentials reset <plugin>`
|
|
@@ -188,18 +192,26 @@ bunx mfw credentials set jira --print
|
|
|
188
192
|
Deletes the plugin's CLI folder from the credentials volume, so the variable in `.env` is unpacked again at the next start: what to run after correcting a variable whose folder is already on the volume, since the entrypoint never touches an existing folder. It asks you to type the plugin's name first, because a token the CLI refreshed on the volume goes too (and with a CLI that rotates its refresh token, the one in `.env` may no longer work). Once confirmed it stops the app, removes the folder in a one-off container of the app's own image, and starts the app again (`docker compose stop mercury`, `run --rm --no-deps -T mercury rm -rf …`, `up -d mercury`).
|
|
189
193
|
|
|
190
194
|
```bash
|
|
191
|
-
|
|
195
|
+
mfw credentials reset jira
|
|
192
196
|
```
|
|
193
197
|
|
|
194
198
|
### `mfw google-chat set-key <key-file> [--subscription <name>]`
|
|
195
199
|
|
|
196
200
|
Writes the Google Chat channel's credentials into the app's `.env`, from the service account's JSON key (the file `gcloud iam service-accounts keys create` writes): `GOOGLE_CHAT_APP_CLIENT_EMAIL`, and `GOOGLE_CHAT_APP_PRIVATE_KEY` on one line with literal `\n`, the way the channel reads it. With `--subscription` (`projects/<project>/subscriptions/<name>`) it sets `GOOGLE_CHAT_PUBSUB_SUBSCRIPTION` too; without it, that line stays as it is, which is what you want when only the key changes. Older values and the empty lines `mfw create` leaves are replaced, the other lines stay alone, and the key is never printed.
|
|
197
201
|
|
|
198
|
-
It checks everything before writing: the app has to depend on `@mercury-fw/channel-google-chat`, the file has to be a service account key and the subscription has to have that shape, otherwise it exits 1 saying why and `.env` stays as it was. Delete the key file afterwards; `
|
|
202
|
+
It checks everything before writing: the app has to depend on `@mercury-fw/channel-google-chat`, the file has to be a service account key and the subscription has to have that shape, otherwise it exits 1 saying why and `.env` stays as it was. Delete the key file afterwards; `mfw start` applies the change to a running app. The whole setup of the Chat app is in the [channel's README](https://github.com/lucabro81/mercury-fw/tree/main/packages/channels/channel-google-chat#setting-up-the-chat-app).
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
mfw google-chat set-key key.json --subscription projects/my-project/subscriptions/mercury-chat-sub
|
|
206
|
+
mfw google-chat set-key new-key.json
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `mfw upgrade`
|
|
210
|
+
|
|
211
|
+
Installs the registry's latest `mfw` globally (`bun add -g @mercury-fw/cli@<latest>`) when it's newer than the one running, and says it's already the latest otherwise; a registry that doesn't answer exits 1. It's about the global `mfw` only: an app's framework, its own CLI included, moves with `bun update` in the app. `mfw create` run by a stale global `mfw` creates the app with the latest one anyway, and says to run this.
|
|
199
212
|
|
|
200
213
|
```bash
|
|
201
|
-
|
|
202
|
-
bunx mfw google-chat set-key new-key.json
|
|
214
|
+
mfw upgrade
|
|
203
215
|
```
|
|
204
216
|
|
|
205
217
|
## Help
|
package/dist/src/main.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { type AppDeps } from "./app/commands.ts";
|
|
2
2
|
/** Runs `argv` with stdio inherited and `env` on top of this process's
|
|
3
3
|
* environment; returns its exit code. */
|
|
4
|
-
export type Relaunch = (argv: string[], env: Record<string, string
|
|
4
|
+
export type Relaunch = (argv: string[], env: Record<string, string>, opts?: {
|
|
5
|
+
quiet?: boolean;
|
|
6
|
+
}) => Promise<number>;
|
|
5
7
|
/** Runs `mfw` with `argv` (the arguments after the command name) and returns
|
|
6
8
|
* the exit code, printing to stdout/stderr. `bin.ts` and `create-mercury-agent`
|
|
7
9
|
* both call it; the app commands look for the app from `cwd` and run docker
|
|
8
10
|
* through `deps`; `relaunch` is how `create` hands over to a newer CLI. */
|
|
9
|
-
export declare function main(argv: string[], { cwd, deps, relaunch }?: {
|
|
11
|
+
export declare function main(argv: string[], { cwd, deps, relaunch, globalInstall, }?: {
|
|
10
12
|
cwd?: string;
|
|
11
13
|
deps?: AppDeps;
|
|
12
14
|
relaunch?: Relaunch;
|
|
15
|
+
globalInstall?: boolean;
|
|
13
16
|
}): Promise<number>;
|
package/dist/src/program.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type AppCommands = ReturnType<typeof appCommands>;
|
|
|
13
13
|
export type ProgramHandlers = {
|
|
14
14
|
/** `mfw create`; returns the exit code. */
|
|
15
15
|
create: (args: CreateArgs) => Promise<number>;
|
|
16
|
+
/** `mfw upgrade`; returns the exit code. */
|
|
17
|
+
upgrade: () => Promise<number>;
|
|
16
18
|
/** The app the app commands act on; throws outside one. */
|
|
17
19
|
app: () => AppCommands;
|
|
18
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
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.
|
|
39
|
+
"@mercury-fw/core": "0.29.0",
|
|
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.
|
|
45
|
+
"@mercury-fw/formatter": "0.29.0",
|
|
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/app/commands.ts
CHANGED
|
@@ -128,7 +128,7 @@ export function appCommands(app: App, deps: AppDeps) {
|
|
|
128
128
|
setEnvVar(envFile, variable, value);
|
|
129
129
|
deps.print(`${variable} set in ${envFile}, from ${source}.`);
|
|
130
130
|
deps.print(
|
|
131
|
-
`The app unpacks it at its next start, if the volume has no ${folder} folder yet; if it has one, run
|
|
131
|
+
`The app unpacks it at its next start, if the volume has no ${folder} folder yet; if it has one, run mfw credentials reset ${plugin} first.`,
|
|
132
132
|
);
|
|
133
133
|
return 0;
|
|
134
134
|
},
|
|
@@ -171,7 +171,7 @@ export function appCommands(app: App, deps: AppDeps) {
|
|
|
171
171
|
? "GOOGLE_CHAT_APP_CLIENT_EMAIL and GOOGLE_CHAT_APP_PRIVATE_KEY"
|
|
172
172
|
: "GOOGLE_CHAT_APP_CLIENT_EMAIL, GOOGLE_CHAT_APP_PRIVATE_KEY and GOOGLE_CHAT_PUBSUB_SUBSCRIPTION";
|
|
173
173
|
deps.print(`${written} set in ${envFile}, from ${source}.`);
|
|
174
|
-
deps.print(`Delete ${source} now, the env file holds the key.
|
|
174
|
+
deps.print(`Delete ${source} now, the env file holds the key. mfw start applies it to a running app.`);
|
|
175
175
|
return 0;
|
|
176
176
|
},
|
|
177
177
|
};
|
|
@@ -184,7 +184,7 @@ export function appCommands(app: App, deps: AppDeps) {
|
|
|
184
184
|
for (const [i, argv] of all.entries()) {
|
|
185
185
|
const code = await deps.run(argv, { cwd: app.dir });
|
|
186
186
|
if (code === 0) continue;
|
|
187
|
-
if (i > 0) deps.print(`The ${service} service was stopped and not restarted:
|
|
187
|
+
if (i > 0) deps.print(`The ${service} service was stopped and not restarted: mfw start brings it back.`);
|
|
188
188
|
return code;
|
|
189
189
|
}
|
|
190
190
|
return 0;
|
package/src/main.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* commands operate an existing app from inside its folder (see
|
|
6
6
|
* `app/commands.ts`). The command line itself is declared in `program.ts`.
|
|
7
7
|
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
9
|
import { basename, dirname, join, resolve } from "node:path";
|
|
9
10
|
import type { CreateArgs } from "./args.ts";
|
|
10
11
|
import { CATALOG } from "./catalog.ts";
|
|
@@ -13,7 +14,7 @@ import { runProgram } from "./program.ts";
|
|
|
13
14
|
import { renderApp, selectionError } from "./render.ts";
|
|
14
15
|
import { appVersions, cliVersion, newerCli, registryFrom } from "./versions.ts";
|
|
15
16
|
import { appCommands, terminalDeps, type AppDeps } from "./app/commands.ts";
|
|
16
|
-
import { findApp } from "./app/find-app.ts";
|
|
17
|
+
import { findApp, type App } from "./app/find-app.ts";
|
|
17
18
|
import { askAnswers, DEFAULT_ASSISTANT_NAME, DEFAULT_ROLE, type Answers } from "./wizard.ts";
|
|
18
19
|
import { targetError, writeApp } from "./write.ts";
|
|
19
20
|
|
|
@@ -30,14 +31,30 @@ function answersFromFlags(args: CreateArgs, defaultName: string): Answers {
|
|
|
30
31
|
|
|
31
32
|
/** Runs `argv` with stdio inherited and `env` on top of this process's
|
|
32
33
|
* environment; returns its exit code. */
|
|
33
|
-
export type Relaunch = (argv: string[], env: Record<string, string
|
|
34
|
+
export type Relaunch = (argv: string[], env: Record<string, string>, opts?: { quiet?: boolean }) => Promise<number>;
|
|
34
35
|
|
|
35
|
-
/** The real relaunch: a child process on the user's terminal
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
/** The real relaunch: a child process on the user's terminal (its output
|
|
37
|
+
* dropped with `quiet`). While it runs, Ctrl+C is the child's to handle: this
|
|
38
|
+
* process ignores SIGINT, so it doesn't exit ahead of the child and lose its
|
|
39
|
+
* exit code. */
|
|
40
|
+
const spawnRelaunch: Relaunch = async (argv, env, opts) => {
|
|
41
|
+
const output = opts?.quiet ? "ignore" : "inherit";
|
|
42
|
+
const proc = Bun.spawn(argv, { stdin: "inherit", stdout: output, stderr: output, env: { ...process.env, ...env } });
|
|
43
|
+
const ignore = () => {};
|
|
44
|
+
process.on("SIGINT", ignore);
|
|
45
|
+
try {
|
|
46
|
+
return await proc.exited;
|
|
47
|
+
} finally {
|
|
48
|
+
process.off("SIGINT", ignore);
|
|
49
|
+
}
|
|
39
50
|
};
|
|
40
51
|
|
|
52
|
+
/** Whether this CLI runs from Bun's global install (`bun add -g`), the one
|
|
53
|
+
* `mfw upgrade` updates, rather than from `bunx`'s cache or an app. */
|
|
54
|
+
function isGlobalInstall(): boolean {
|
|
55
|
+
return /[\\/]install[\\/]global[\\/]node_modules[\\/]/.test(import.meta.dir);
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
/** When the registry has a newer `@mercury-fw/cli` than this one (a stale copy
|
|
42
59
|
* out of Bun's bunx cache), installs it (`bunx … --version`), then runs
|
|
43
60
|
* `create` again through it with the same arguments as typed and returns its
|
|
@@ -45,7 +62,7 @@ const spawnRelaunch: Relaunch = async (argv, env) => {
|
|
|
45
62
|
* carry on here: no newer CLI, a check skipped inside a relaunch
|
|
46
63
|
* (`MFW_SELF_UPDATED`), a registry that can't answer, or a newer CLI that
|
|
47
64
|
* can't be installed; the last two with a warning. */
|
|
48
|
-
async function relaunchIfStale(rawArgs: string[], relaunch: Relaunch): Promise<number | undefined> {
|
|
65
|
+
async function relaunchIfStale(rawArgs: string[], relaunch: Relaunch, globalInstall: boolean): Promise<number | undefined> {
|
|
49
66
|
if (process.env.MFW_SELF_UPDATED) return undefined;
|
|
50
67
|
const registry = registryFrom(process.env.MFW_REGISTRY).replace(/\/+$/, "");
|
|
51
68
|
let newer: string | undefined;
|
|
@@ -60,7 +77,7 @@ async function relaunchIfStale(rawArgs: string[], relaunch: Relaunch): Promise<n
|
|
|
60
77
|
const env = { MFW_SELF_UPDATED: newer, NPM_CONFIG_REGISTRY: registry };
|
|
61
78
|
let installed: number;
|
|
62
79
|
try {
|
|
63
|
-
installed = await relaunch(["bunx", cli, "--version"], env);
|
|
80
|
+
installed = await relaunch(["bunx", cli, "--version"], env, { quiet: true });
|
|
64
81
|
} catch {
|
|
65
82
|
installed = -1;
|
|
66
83
|
}
|
|
@@ -69,13 +86,14 @@ async function relaunchIfStale(rawArgs: string[], relaunch: Relaunch): Promise<n
|
|
|
69
86
|
return undefined;
|
|
70
87
|
}
|
|
71
88
|
console.error(`mfw ${cliVersion()} is behind the registry's ${newer}: running ${newer} instead.`);
|
|
89
|
+
if (globalInstall) console.error("update your mfw: mfw upgrade");
|
|
72
90
|
return relaunch(["bunx", cli, "create", ...rawArgs], env);
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
/** `mfw create`: returns the exit code. `rawArgs` are the arguments after
|
|
76
94
|
* `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);
|
|
95
|
+
async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, globalInstall: boolean): Promise<number> {
|
|
96
|
+
const relaunched = await relaunchIfStale(rawArgs, relaunch, globalInstall);
|
|
79
97
|
if (relaunched !== undefined) return relaunched;
|
|
80
98
|
// The folder is created in kebab case, only its own name: the parent path is
|
|
81
99
|
// taken as typed. Its name is also the app name's default.
|
|
@@ -106,21 +124,90 @@ Next:
|
|
|
106
124
|
cd ${dir}
|
|
107
125
|
bun install
|
|
108
126
|
cp .env.example .env # then fill it in
|
|
109
|
-
|
|
127
|
+
mfw start`);
|
|
110
128
|
return 0;
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
/** `mfw upgrade`: installs the registry's latest `@mercury-fw/cli` globally
|
|
132
|
+
* when it's newer than this one; returns the install's exit code, 0 when
|
|
133
|
+
* there's nothing newer, 1 when the registry can't answer. */
|
|
134
|
+
async function upgrade(relaunch: Relaunch): Promise<number> {
|
|
135
|
+
const registry = registryFrom(process.env.MFW_REGISTRY).replace(/\/+$/, "");
|
|
136
|
+
let newer: string | undefined;
|
|
137
|
+
try {
|
|
138
|
+
newer = await newerCli({ registry });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(`couldn't check for a newer mfw: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
if (newer === undefined) {
|
|
144
|
+
console.log(`mfw ${cliVersion()} is the latest.`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
const code = await relaunch(["bun", "add", "-g", `@mercury-fw/cli@${newer}`], { NPM_CONFIG_REGISTRY: registry });
|
|
148
|
+
if (code === 0) console.log(`mfw upgraded from ${cliVersion()} to ${newer}.`);
|
|
149
|
+
return code;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The commands that belong to the CLI itself, wherever it runs: never
|
|
153
|
+
* handed over to an app's CLI. */
|
|
154
|
+
const OWN_COMMANDS = new Set(["create", "upgrade"]);
|
|
155
|
+
|
|
156
|
+
/** Inside an app whose own CLI (its devDependency) is installed at another
|
|
157
|
+
* version than this one, as a global `mfw` can be, runs `argv` through that
|
|
158
|
+
* CLI and returns its exit code: the app's commands then always match the
|
|
159
|
+
* framework the app runs. `undefined` means run here: not an app command, not
|
|
160
|
+
* inside an app, the app not installed yet, the same version, or already
|
|
161
|
+
* handed over (`MFW_DEFERRED`). */
|
|
162
|
+
async function handOverToAppCli(argv: string[], cwd: string, relaunch: Relaunch): Promise<number | undefined> {
|
|
163
|
+
const command = argv[0];
|
|
164
|
+
if (command === undefined || command.startsWith("-") || OWN_COMMANDS.has(command)) return undefined;
|
|
165
|
+
if (process.env.MFW_DEFERRED) return undefined;
|
|
166
|
+
let app: App;
|
|
167
|
+
try {
|
|
168
|
+
app = findApp(cwd);
|
|
169
|
+
} catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const local = join(app.dir, "node_modules", "@mercury-fw", "cli");
|
|
173
|
+
const manifest = join(local, "package.json");
|
|
174
|
+
if (!existsSync(manifest)) return undefined;
|
|
175
|
+
let version: unknown;
|
|
176
|
+
try {
|
|
177
|
+
version = (JSON.parse(readFileSync(manifest, "utf-8")) as { version?: unknown } | null)?.version;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(`couldn't read the app's mfw (${manifest}): ${err instanceof Error ? err.message : String(err)}; running mfw ${cliVersion()}`);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
if (typeof version !== "string" || version === cliVersion()) return undefined;
|
|
183
|
+
console.error(`mfw ${cliVersion()}: running the app's ${version}`);
|
|
184
|
+
try {
|
|
185
|
+
return await relaunch(["bun", join(local, "src", "bin.ts"), ...argv], { MFW_DEFERRED: "1" });
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error(`couldn't run the app's mfw: ${err instanceof Error ? err.message : String(err)}; running mfw ${cliVersion()}`);
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
113
192
|
/** Runs `mfw` with `argv` (the arguments after the command name) and returns
|
|
114
193
|
* the exit code, printing to stdout/stderr. `bin.ts` and `create-mercury-agent`
|
|
115
194
|
* both call it; the app commands look for the app from `cwd` and run docker
|
|
116
195
|
* through `deps`; `relaunch` is how `create` hands over to a newer CLI. */
|
|
117
196
|
export async function main(
|
|
118
197
|
argv: string[],
|
|
119
|
-
{
|
|
198
|
+
{
|
|
199
|
+
cwd = process.cwd(),
|
|
200
|
+
deps,
|
|
201
|
+
relaunch = spawnRelaunch,
|
|
202
|
+
globalInstall = isGlobalInstall(),
|
|
203
|
+
}: { cwd?: string; deps?: AppDeps; relaunch?: Relaunch; globalInstall?: boolean } = {},
|
|
120
204
|
): Promise<number> {
|
|
205
|
+
const handedOver = await handOverToAppCli(argv, cwd, relaunch);
|
|
206
|
+
if (handedOver !== undefined) return handedOver;
|
|
121
207
|
const rawCreateArgs = argv.slice(argv.indexOf("create") + 1);
|
|
122
208
|
return runProgram(argv, {
|
|
123
|
-
create: (args) => create(args, rawCreateArgs, relaunch),
|
|
209
|
+
create: (args) => create(args, rawCreateArgs, relaunch, globalInstall),
|
|
210
|
+
upgrade: () => upgrade(relaunch),
|
|
124
211
|
app: () => appCommands(findApp(cwd), deps ?? terminalDeps()),
|
|
125
212
|
});
|
|
126
213
|
}
|
package/src/program.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type AppCommands = ReturnType<typeof appCommands>;
|
|
|
18
18
|
export type ProgramHandlers = {
|
|
19
19
|
/** `mfw create`; returns the exit code. */
|
|
20
20
|
create: (args: CreateArgs) => Promise<number>;
|
|
21
|
+
/** `mfw upgrade`; returns the exit code. */
|
|
22
|
+
upgrade: () => Promise<number>;
|
|
21
23
|
/** The app the app commands act on; throws outside one. */
|
|
22
24
|
app: () => AppCommands;
|
|
23
25
|
};
|
|
@@ -74,6 +76,16 @@ Examples:
|
|
|
74
76
|
result.code = await handlers.create(toCreateArgs(folder, opts));
|
|
75
77
|
});
|
|
76
78
|
|
|
79
|
+
program
|
|
80
|
+
.command("upgrade")
|
|
81
|
+
.summary("updates the global mfw")
|
|
82
|
+
.description(
|
|
83
|
+
"Installs the registry's latest mfw globally (bun add -g @mercury-fw/cli@<latest>), when it's newer than this one. An app's own framework, its CLI included, is upgraded with bun update in the app.",
|
|
84
|
+
)
|
|
85
|
+
.action(async () => {
|
|
86
|
+
result.code = await handlers.upgrade();
|
|
87
|
+
});
|
|
88
|
+
|
|
77
89
|
// commander reads --no-cache as "cache: false"; the commands take noCache.
|
|
78
90
|
const cacheOff = (opts: { cache: boolean }) => ({ noCache: !opts.cache });
|
|
79
91
|
|
package/src/render.ts
CHANGED
|
@@ -113,10 +113,10 @@ function renderEntrypoint(tools: Array<CatalogEntry & { credentials: CliCredenti
|
|
|
113
113
|
return `#!/usr/bin/env bash
|
|
114
114
|
# Starts the service, first materializing each tool plugin's CLI credentials
|
|
115
115
|
# onto the cli-credentials volume: its variable in the env file is the CLI's
|
|
116
|
-
# config folder as a base64 tar.gz (
|
|
116
|
+
# config folder as a base64 tar.gz (mfw credentials set <plugin> writes
|
|
117
117
|
# it). Only when that CLI's folder isn't on the volume yet: what a CLI writes
|
|
118
118
|
# back while running, like a refreshed token, stays there across redeploys,
|
|
119
|
-
# and an older value in the env file never overwrites it.
|
|
119
|
+
# and an older value in the env file never overwrites it. mfw credentials
|
|
120
120
|
# reset <plugin> clears one folder so its variable is materialized again.
|
|
121
121
|
set -euo pipefail
|
|
122
122
|
|
|
@@ -275,8 +275,8 @@ function renderPackageJson(
|
|
|
275
275
|
private: true,
|
|
276
276
|
scripts: { start: "bun src/index.ts", repl: "bun src/repl.ts", typecheck: "tsc --noEmit" },
|
|
277
277
|
dependencies,
|
|
278
|
-
// The CLI in the app itself,
|
|
279
|
-
//
|
|
278
|
+
// The CLI in the app itself, at the framework's version: a global `mfw`
|
|
279
|
+
// hands the app's commands over to it.
|
|
280
280
|
devDependencies: { "@mercury-fw/cli": `^${cli}`, "@types/bun": "^1.4.2", typescript: "^6.0.3" },
|
|
281
281
|
};
|
|
282
282
|
const trusted = [...new Set([...tools.map((t) => t.package), ...[...channels, ...tools].flatMap((e) => e.trusts ?? [])])];
|
|
@@ -307,7 +307,7 @@ function renderEnv(channels: CatalogEntry[], tools: CatalogEntry[]): string {
|
|
|
307
307
|
if (entry.credentials !== undefined) {
|
|
308
308
|
vars.push({
|
|
309
309
|
name: entry.credentials.variable,
|
|
310
|
-
comment: `${entry.credentials.folder}'s config folder, packed:
|
|
310
|
+
comment: `${entry.credentials.folder}'s config folder, packed: mfw credentials set ${entry.id} writes it; materialized on the credentials volume at the first start without that folder`,
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
313
|
if (vars.length > 0) {
|
|
@@ -388,14 +388,22 @@ A Mercury app, scaffolded by \`mfw create\`.
|
|
|
388
388
|
|
|
389
389
|
## Running it
|
|
390
390
|
|
|
391
|
+
\`mfw\` is Mercury's command-line tool. Install it once, globally (\`mfw upgrade\` keeps it current), or put \`bunx\` in front of every command instead (\`bunx mfw start\`):
|
|
392
|
+
|
|
393
|
+
\`\`\`bash
|
|
394
|
+
bun add -g @mercury-fw/cli
|
|
395
|
+
\`\`\`
|
|
396
|
+
|
|
397
|
+
Then, in the app:
|
|
398
|
+
|
|
391
399
|
\`\`\`bash
|
|
392
400
|
bun install
|
|
393
401
|
cp .env.example .env
|
|
394
|
-
|
|
395
|
-
|
|
402
|
+
mfw start
|
|
403
|
+
mfw repl
|
|
396
404
|
\`\`\`
|
|
397
405
|
|
|
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. \`
|
|
406
|
+
\`bun install\` here gives your editor, \`bun run typecheck\` and the app's own \`mfw\` (the one a global \`mfw\` runs inside the app) the packages (tool plugins download their CLI binary as they install); the image installs its own copy when it builds. \`mfw start\` builds the image and starts the app with Qdrant in the background, \`mfw repl\` opens a terminal conversation with the assistant. \`mfw --help\` lists the rest: stopping and restarting, logs, a shell in the container, the wiki and the memory, and resetting them.
|
|
399
407
|
${renderHttpSection(channels)}${renderCredentialsSection(withCredentials(tools))}`;
|
|
400
408
|
}
|
|
401
409
|
|
|
@@ -422,7 +430,7 @@ function renderCredentialsSection(tools: Array<CatalogEntry & { credentials: Cli
|
|
|
422
430
|
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:
|
|
423
431
|
|
|
424
432
|
\`\`\`bash
|
|
425
|
-
|
|
433
|
+
mfw credentials set ${first.id}
|
|
426
434
|
\`\`\`
|
|
427
435
|
|
|
428
436
|
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.
|
|
@@ -430,7 +438,7 @@ It packs the folder into its variable in \`.env\` (\`--from <folder>\` if it isn
|
|
|
430
438
|
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:
|
|
431
439
|
|
|
432
440
|
\`\`\`bash
|
|
433
|
-
|
|
441
|
+
mfw credentials reset ${first.id}
|
|
434
442
|
\`\`\`
|
|
435
443
|
`;
|
|
436
444
|
}
|