@mercury-fw/cli 0.42.0 → 0.44.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 +34 -0
- package/README.md +12 -0
- package/dist/src/app/commands.d.ts +3 -0
- package/dist/src/args.d.ts +3 -0
- package/dist/src/catalog.d.ts +7 -6
- package/dist/src/render.d.ts +6 -4
- package/dist/src/wizard.d.ts +2 -0
- package/package.json +10 -9
- package/src/app/commands.ts +3 -0
- package/src/args.ts +4 -0
- package/src/catalog.ts +29 -6
- package/src/main.ts +14 -5
- package/src/program.ts +33 -0
- package/src/render.ts +44 -19
- package/src/wizard.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @mercury-fw/cli
|
|
2
2
|
|
|
3
|
+
## 0.44.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2c63bbf: One person across accounts: linking them by a code.
|
|
8
|
+
|
|
9
|
+
- `POST /link` on the HTTP surface hands the caller a one-time code (`xxxx-xxxx-xxxx`, ten minutes, single use). They send it from the other account, on any channel, twice. The first time Mercury says whom the account would be linked to, the second time it links it.
|
|
10
|
+
- From then on the account is the person it's linked to: one private area, one memory, the same roles. Its earlier area stays where it was, and undoing the link gives it back.
|
|
11
|
+
- A link never touches the terminal, never chains, and is refused for an account the directory already knows as someone else.
|
|
12
|
+
- The links live on the vault's volume, out of the vault's git and out of reach of the model and the HTTP reads.
|
|
13
|
+
- `mfw identity links | link | unlink` lets the operator manage them, for instances where nobody can be shown a code.
|
|
14
|
+
- Channels get the code through the confirmation hook they already call (`ctx.confirm`), and the HTTP surface gets `ctx.linking`.
|
|
15
|
+
- Fixed: `ctx.confirm` refused someone the core doesn't admit before checking that their text was a token at all. On Google Chat, every message from an unknown sender came back as the refusal.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [2c63bbf]
|
|
20
|
+
- @mercury-fw/core@0.44.0
|
|
21
|
+
- @mercury-fw/utils@0.44.0
|
|
22
|
+
|
|
23
|
+
## 0.43.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- 07eb858: ZITADEL as the user directory.
|
|
28
|
+
|
|
29
|
+
- `@mercury-fw/plugin-zitadel` exports `zitadelDirectory`. Someone calling with a ZITADEL token is the ZITADEL user the token names, with the role keys of their active role assignments on the project `ZITADEL_PROJECT_ID`. A user ZITADEL doesn't find, or one that isn't active, is unknown. It reads through the zitadel CLI's service user, from code.
|
|
30
|
+
- `mfw create --directory <id>` (`static` or `zitadel`) gives the new app a user directory, and the wizard asks for one.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- @mercury-fw/core@0.43.0
|
|
35
|
+
- @mercury-fw/utils@0.43.0
|
|
36
|
+
|
|
3
37
|
## 0.42.0
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- [`mfw vault <command>`](#mfw-vault-command)
|
|
17
17
|
- [`mfw memory list`](#mfw-memory-list)
|
|
18
18
|
- [`mfw memory read <collection> [--limit N]`](#mfw-memory-read-collection---limit-n)
|
|
19
|
+
- [`mfw identity links`, `link`, `unlink`](#mfw-identity-links-link-unlink)
|
|
19
20
|
- [`mfw reset <memory|wiki>`](#mfw-reset-memorywiki)
|
|
20
21
|
- [`mfw credentials setup <plugin>`](#mfw-credentials-setup-plugin)
|
|
21
22
|
- [`mfw credentials check <plugin>`](#mfw-credentials-check-plugin)
|
|
@@ -62,6 +63,7 @@ Then it runs `bun install` in the app and creates a git repository on `main` wit
|
|
|
62
63
|
| `--channels <ids>` | Comma-separated: `google-chat`, `http`. |
|
|
63
64
|
| `--plugins <ids>` | Comma-separated: `jira`, `bitbucket`, `atlassian-admin`, `zitadel`. |
|
|
64
65
|
| `--auth <id>` | The HTTP channel's auth provider, `oidc` or `static`: required with `http`, refused without it, since the channel doesn't start without one. The wizard asks for it once `http` is chosen. |
|
|
66
|
+
| `--directory <id>` | The user directory, `static` or `zitadel`: who the people are and their roles. Without one, the person is whoever the channel says, with no roles. |
|
|
65
67
|
| `--local-packages <folder>` | Installs the packages packed in `<folder>` (`bun pm pack`) instead of the registry's, as [`mfw local-packages`](#mfw-local-packages-folder--mfw-local-packages---off) does on an existing app: their versions come from the tarballs, so a package that isn't published yet works too, and no newer `mfw` is looked up on the registry. For trying unreleased packages in a new app; the test bed makes its apps this way. |
|
|
66
68
|
| `--git-remote <url>` | The repository's `origin`, taken as typed. |
|
|
67
69
|
| `--no-install` | Don't run `bun install`. |
|
|
@@ -173,6 +175,16 @@ mfw memory read episodic_memory
|
|
|
173
175
|
mfw memory read semantic_facts --limit 5
|
|
174
176
|
```
|
|
175
177
|
|
|
178
|
+
### `mfw identity links`, `link`, `unlink`
|
|
179
|
+
|
|
180
|
+
The links that make several accounts one person: one private area, one memory, the same roles. People link their own accounts with a code (the HTTP surface's `POST /link` gives one, they send it from the other account, twice); these are for the operator, in a one-off container, where nobody can be shown a code. Identities are written `<provider>:<id>`, as the channel or the auth provider vouches for them (`google-chat:users/123`, `oidc:3123`, `static:alice`). The running service applies a change within five minutes. Undoing a link gives the account back its own area, untouched.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
mfw identity links
|
|
184
|
+
mfw identity link google-chat:users/123 oidc:3123
|
|
185
|
+
mfw identity unlink google-chat:users/123
|
|
186
|
+
```
|
|
187
|
+
|
|
176
188
|
### `mfw reset <memory|wiki>`
|
|
177
189
|
|
|
178
190
|
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.
|
|
@@ -21,6 +21,7 @@ export type AppDeps = {
|
|
|
21
21
|
* bin whose file isn't there yet. The core moves in lockstep with this CLI. */
|
|
22
22
|
export declare const VAULT_CLI = "node_modules/@mercury-fw/core/src/wiki/vault-cli.ts";
|
|
23
23
|
export declare const MEMORY_CLI = "node_modules/@mercury-fw/core/src/memory/memory-cli.ts";
|
|
24
|
+
export declare const IDENTITY_CLI = "node_modules/@mercury-fw/core/src/identity/links-cli.ts";
|
|
24
25
|
/** What `mfw reset` can wipe: the compose service using the volume, the
|
|
25
26
|
* volume's key in the compose file, and how to say what's lost. */
|
|
26
27
|
export declare const RESET_TARGETS: {
|
|
@@ -52,6 +53,8 @@ export declare function appCommands(app: App, deps: AppDeps): {
|
|
|
52
53
|
vault: (args: string[]) => Promise<number>;
|
|
53
54
|
/** `args` is the memory CLI's own command line (`list`, `read <collection>`, …). */
|
|
54
55
|
memory: (args: string[]) => Promise<number>;
|
|
56
|
+
/** `args` is the account links CLI's own command line (`links`, `link <identity> <owner>`, `unlink <identity>`). */
|
|
57
|
+
identity: (args: string[]) => Promise<number>;
|
|
55
58
|
/** Deletes `target`'s volume once the user types the app's name, then
|
|
56
59
|
* brings its service back up on an empty volume. The volume's real name
|
|
57
60
|
* comes from the compose file, and a wrong answer deletes nothing. */
|
package/dist/src/args.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type CreateArgs = {
|
|
|
16
16
|
plugins?: string[];
|
|
17
17
|
/** The auth provider, which goes with the HTTP channel. */
|
|
18
18
|
auth?: string;
|
|
19
|
+
/** The user directory. */
|
|
20
|
+
directory?: string;
|
|
19
21
|
/** The repository's origin, taken as typed. */
|
|
20
22
|
gitRemote?: string;
|
|
21
23
|
/** A folder of packed tarballs the app installs instead of the registry's packages. */
|
|
@@ -34,6 +36,7 @@ export type CreateOptions = {
|
|
|
34
36
|
channels?: string;
|
|
35
37
|
plugins?: string;
|
|
36
38
|
auth?: string;
|
|
39
|
+
directory?: string;
|
|
37
40
|
gitRemote?: string;
|
|
38
41
|
localPackages?: string;
|
|
39
42
|
yes?: boolean;
|
package/dist/src/catalog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The channels, auth providers
|
|
2
|
+
* The channels, auth providers, tool plugins and user directories `mfw create` can put in a new app. Written
|
|
3
3
|
* by hand for now: once the packages are published, each plugin will describe
|
|
4
4
|
* itself (kind, export, env vars) and this list goes away. `catalog.test.ts`
|
|
5
5
|
* checks every entry against the real package until then.
|
|
@@ -11,13 +11,14 @@ export type EnvVar = {
|
|
|
11
11
|
comment: string;
|
|
12
12
|
value?: string;
|
|
13
13
|
};
|
|
14
|
-
/** A channel, auth provider
|
|
15
|
-
* plugin's own `name` (what the logs and `dependsOn` call it
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
/** A channel, auth provider, tool plugin or user directory the app can include.
|
|
15
|
+
* `id` is the plugin's own `name` (what the logs and `dependsOn` call it; for a
|
|
16
|
+
* directory, the choice's name), `exportName` the value the app's config
|
|
17
|
+
* imports from `package`. An auth provider goes with the HTTP channel, which
|
|
18
|
+
* doesn't start without one; a directory goes with any channel. */
|
|
18
19
|
export type CatalogEntry = {
|
|
19
20
|
id: string;
|
|
20
|
-
kind: "channel" | "auth" | "tool";
|
|
21
|
+
kind: "channel" | "auth" | "tool" | "directory";
|
|
21
22
|
package: string;
|
|
22
23
|
exportName: string;
|
|
23
24
|
env: EnvVar[];
|
package/dist/src/render.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type RenderInput = {
|
|
|
8
8
|
plugins: string[];
|
|
9
9
|
/** The HTTP channel's auth provider: required with it, refused without it. */
|
|
10
10
|
auth?: string;
|
|
11
|
+
/** The user directory: who the people are and their roles. */
|
|
12
|
+
directory?: string;
|
|
11
13
|
versions: Record<string, string>;
|
|
12
14
|
};
|
|
13
15
|
/** Builds every file of the new app. Throws on an invalid app name, an empty
|
|
@@ -17,10 +19,10 @@ export declare function renderApp(input: RenderInput): Map<string, string>;
|
|
|
17
19
|
/** Why `name` can't be an app name, or undefined when it can. Shared with the
|
|
18
20
|
* wizard, which checks the name as it's typed. */
|
|
19
21
|
export declare function appNameError(name: string): string | undefined;
|
|
20
|
-
/** Names the first channel, plugin
|
|
21
|
-
* have, with the valid ones, or undefined when all are known. Shared
|
|
22
|
-
* command, which checks the flags before asking anything. */
|
|
23
|
-
export declare function selectionError(channels: string[], plugins: string[], auth?: string): string | undefined;
|
|
22
|
+
/** Names the first channel, plugin, auth provider or directory id the catalog
|
|
23
|
+
* doesn't have, with the valid ones, or undefined when all are known. Shared
|
|
24
|
+
* with the command, which checks the flags before asking anything. */
|
|
25
|
+
export declare function selectionError(channels: string[], plugins: string[], auth?: string, directory?: string): string | undefined;
|
|
24
26
|
/** What's wrong with pairing the HTTP channel and the auth provider: it
|
|
25
27
|
* doesn't start without one, and one without it has nothing to authenticate.
|
|
26
28
|
* Undefined when they go together. Shared with the command, which checks it
|
package/dist/src/wizard.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type Answers = {
|
|
|
9
9
|
plugins: string[];
|
|
10
10
|
/** The HTTP channel's auth provider, there exactly when that channel is. */
|
|
11
11
|
auth?: string;
|
|
12
|
+
/** The user directory, when there's one. */
|
|
13
|
+
directory?: string;
|
|
12
14
|
gitRemote?: string;
|
|
13
15
|
};
|
|
14
16
|
export declare const DEFAULT_ASSISTANT_NAME = "Mercury";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,20 +41,21 @@
|
|
|
41
41
|
"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.",
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@clack/prompts": "^1.8.1",
|
|
44
|
-
"@mercury-fw/core": "0.
|
|
45
|
-
"@mercury-fw/utils": "0.
|
|
44
|
+
"@mercury-fw/core": "0.44.0",
|
|
45
|
+
"@mercury-fw/utils": "0.44.0",
|
|
46
46
|
"commander": "^15.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@mercury-fw/channel-google-chat": "0.5.0",
|
|
50
|
-
"@mercury-fw/channel-http": "0.6.0",
|
|
51
49
|
"@mercury-fw/auth-oidc": "0.1.0",
|
|
52
50
|
"@mercury-fw/auth-static": "0.2.0",
|
|
53
|
-
"@mercury-fw/
|
|
51
|
+
"@mercury-fw/channel-google-chat": "0.5.0",
|
|
52
|
+
"@mercury-fw/channel-http": "0.7.0",
|
|
53
|
+
"@mercury-fw/directory-static": "0.1.0",
|
|
54
|
+
"@mercury-fw/formatter": "0.44.0",
|
|
54
55
|
"@mercury-fw/plugin-atlassian-admin": "0.3.1",
|
|
55
|
-
"@mercury-fw/plugin-bitbucket": "0.3.
|
|
56
|
-
"@mercury-fw/plugin-jira": "0.6.
|
|
57
|
-
"@mercury-fw/plugin-zitadel": "0.
|
|
56
|
+
"@mercury-fw/plugin-bitbucket": "0.3.1",
|
|
57
|
+
"@mercury-fw/plugin-jira": "0.6.1",
|
|
58
|
+
"@mercury-fw/plugin-zitadel": "0.2.0",
|
|
58
59
|
"@mercury-fw/typescript-config": "*",
|
|
59
60
|
"@types/bun": "^1.4.2",
|
|
60
61
|
"typescript": "^6.0.3"
|
package/src/app/commands.ts
CHANGED
|
@@ -44,6 +44,7 @@ const SERVICE = "mercury";
|
|
|
44
44
|
* bin whose file isn't there yet. The core moves in lockstep with this CLI. */
|
|
45
45
|
export const VAULT_CLI = "node_modules/@mercury-fw/core/src/wiki/vault-cli.ts";
|
|
46
46
|
export const MEMORY_CLI = "node_modules/@mercury-fw/core/src/memory/memory-cli.ts";
|
|
47
|
+
export const IDENTITY_CLI = "node_modules/@mercury-fw/core/src/identity/links-cli.ts";
|
|
47
48
|
|
|
48
49
|
/** What `mfw reset` can wipe: the compose service using the volume, the
|
|
49
50
|
* volume's key in the compose file, and how to say what's lost. */
|
|
@@ -100,6 +101,8 @@ export function appCommands(app: App, deps: AppDeps) {
|
|
|
100
101
|
vault: (args: string[]) => oneOff(["bun", VAULT_CLI, ...args]),
|
|
101
102
|
/** `args` is the memory CLI's own command line (`list`, `read <collection>`, …). */
|
|
102
103
|
memory: (args: string[]) => oneOff(["bun", MEMORY_CLI, ...args]),
|
|
104
|
+
/** `args` is the account links CLI's own command line (`links`, `link <identity> <owner>`, `unlink <identity>`). */
|
|
105
|
+
identity: (args: string[]) => oneOff(["bun", IDENTITY_CLI, ...args]),
|
|
103
106
|
/** Deletes `target`'s volume once the user types the app's name, then
|
|
104
107
|
* brings its service back up on an empty volume. The volume's real name
|
|
105
108
|
* comes from the compose file, and a wrong answer deletes nothing. */
|
package/src/args.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type CreateArgs = {
|
|
|
17
17
|
plugins?: string[];
|
|
18
18
|
/** The auth provider, which goes with the HTTP channel. */
|
|
19
19
|
auth?: string;
|
|
20
|
+
/** The user directory. */
|
|
21
|
+
directory?: string;
|
|
20
22
|
/** The repository's origin, taken as typed. */
|
|
21
23
|
gitRemote?: string;
|
|
22
24
|
/** A folder of packed tarballs the app installs instead of the registry's packages. */
|
|
@@ -36,6 +38,7 @@ export type CreateOptions = {
|
|
|
36
38
|
channels?: string;
|
|
37
39
|
plugins?: string;
|
|
38
40
|
auth?: string;
|
|
41
|
+
directory?: string;
|
|
39
42
|
gitRemote?: string;
|
|
40
43
|
localPackages?: string;
|
|
41
44
|
yes?: boolean;
|
|
@@ -59,6 +62,7 @@ export function toCreateArgs(folder: string, opts: CreateOptions): CreateArgs {
|
|
|
59
62
|
if (opts.channels !== undefined) args.channels = list(opts.channels);
|
|
60
63
|
if (opts.plugins !== undefined) args.plugins = list(opts.plugins);
|
|
61
64
|
if (opts.auth !== undefined && opts.auth.trim() !== "") args.auth = opts.auth.trim();
|
|
65
|
+
if (opts.directory !== undefined && opts.directory.trim() !== "") args.directory = opts.directory.trim();
|
|
62
66
|
if (opts.localPackages !== undefined) args.localPackages = opts.localPackages;
|
|
63
67
|
// Blank is none, as in the wizard.
|
|
64
68
|
if (opts.gitRemote !== undefined && opts.gitRemote.trim() !== "") args.gitRemote = opts.gitRemote.trim();
|
package/src/catalog.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The channels, auth providers
|
|
2
|
+
* The channels, auth providers, tool plugins and user directories `mfw create` can put in a new app. Written
|
|
3
3
|
* by hand for now: once the packages are published, each plugin will describe
|
|
4
4
|
* itself (kind, export, env vars) and this list goes away. `catalog.test.ts`
|
|
5
5
|
* checks every entry against the real package until then.
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* example: `value` is the example value, empty when the deployer must fill it. */
|
|
10
10
|
export type EnvVar = { name: string; comment: string; value?: string };
|
|
11
11
|
|
|
12
|
-
/** A channel, auth provider
|
|
13
|
-
* plugin's own `name` (what the logs and `dependsOn` call it
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
/** A channel, auth provider, tool plugin or user directory the app can include.
|
|
13
|
+
* `id` is the plugin's own `name` (what the logs and `dependsOn` call it; for a
|
|
14
|
+
* directory, the choice's name), `exportName` the value the app's config
|
|
15
|
+
* imports from `package`. An auth provider goes with the HTTP channel, which
|
|
16
|
+
* doesn't start without one; a directory goes with any channel. */
|
|
16
17
|
export type CatalogEntry = {
|
|
17
18
|
id: string;
|
|
18
|
-
kind: "channel" | "auth" | "tool";
|
|
19
|
+
kind: "channel" | "auth" | "tool" | "directory";
|
|
19
20
|
package: string;
|
|
20
21
|
exportName: string;
|
|
21
22
|
env: EnvVar[];
|
|
@@ -122,6 +123,28 @@ export const CATALOG: CatalogEntry[] = [
|
|
|
122
123
|
exportName: "zitadelPlugin",
|
|
123
124
|
env: [],
|
|
124
125
|
},
|
|
126
|
+
{
|
|
127
|
+
id: "static",
|
|
128
|
+
kind: "directory",
|
|
129
|
+
package: "@mercury-fw/directory-static",
|
|
130
|
+
exportName: "staticDirectory",
|
|
131
|
+
env: [
|
|
132
|
+
{
|
|
133
|
+
name: "DIRECTORY_STATIC_PEOPLE",
|
|
134
|
+
comment:
|
|
135
|
+
'Required: the people, as JSON: [{"id": "alice", "roles": ["mercury.act-as-self"], "identities": ["static:alice"]}]',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "zitadel",
|
|
141
|
+
kind: "directory",
|
|
142
|
+
package: "@mercury-fw/plugin-zitadel",
|
|
143
|
+
exportName: "zitadelDirectory",
|
|
144
|
+
env: [{ name: "ZITADEL_PROJECT_ID", comment: "Required: the ZITADEL project whose roles count" }],
|
|
145
|
+
// It calls the zitadel CLI, which the package downloads as it installs.
|
|
146
|
+
trusts: ["@mercury-fw/plugin-zitadel"],
|
|
147
|
+
},
|
|
125
148
|
];
|
|
126
149
|
|
|
127
150
|
/** The catalog entry of `kind` with `id`, or undefined when there is none. */
|
package/src/main.ts
CHANGED
|
@@ -29,6 +29,7 @@ function answersFromFlags(args: CreateArgs, defaultName: string): Answers {
|
|
|
29
29
|
channels: args.channels ?? [],
|
|
30
30
|
plugins: args.plugins ?? [],
|
|
31
31
|
...(args.auth !== undefined ? { auth: args.auth } : {}),
|
|
32
|
+
...(args.directory !== undefined ? { directory: args.directory } : {}),
|
|
32
33
|
...(args.gitRemote !== undefined ? { gitRemote: args.gitRemote } : {}),
|
|
33
34
|
};
|
|
34
35
|
}
|
|
@@ -127,7 +128,7 @@ async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, g
|
|
|
127
128
|
const pairedByFlags = args.yes || (args.channels !== undefined && args.auth !== undefined);
|
|
128
129
|
const early =
|
|
129
130
|
targetError(dir) ??
|
|
130
|
-
selectionError(args.channels ?? [], args.plugins ?? [], args.auth) ??
|
|
131
|
+
selectionError(args.channels ?? [], args.plugins ?? [], args.auth, args.directory) ??
|
|
131
132
|
(pairedByFlags ? pairingError(args.channels ?? [], args.auth) : undefined) ??
|
|
132
133
|
remoteError(args);
|
|
133
134
|
if (early !== undefined) {
|
|
@@ -138,9 +139,17 @@ async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, g
|
|
|
138
139
|
if (answers === undefined) {
|
|
139
140
|
return 1;
|
|
140
141
|
}
|
|
141
|
-
const chosen =
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const chosen = [
|
|
143
|
+
...new Set(
|
|
144
|
+
CATALOG.filter((e) =>
|
|
145
|
+
e.kind === "auth"
|
|
146
|
+
? e.id === answers.auth
|
|
147
|
+
: e.kind === "directory"
|
|
148
|
+
? e.id === answers.directory
|
|
149
|
+
: (e.kind === "channel" ? answers.channels : answers.plugins).includes(e.id),
|
|
150
|
+
).map((e) => e.package),
|
|
151
|
+
),
|
|
152
|
+
];
|
|
144
153
|
const versions = await appVersions(chosen, {
|
|
145
154
|
registry: registryFrom(process.env.MFW_REGISTRY),
|
|
146
155
|
...(packs !== undefined ? { local: Object.fromEntries(packs.map((p) => [p.name, p.version])) } : {}),
|
|
@@ -162,7 +171,7 @@ async function create(args: CreateArgs, rawArgs: string[], relaunch: Relaunch, g
|
|
|
162
171
|
...(answers.gitRemote !== undefined ? { remote: answers.gitRemote } : {}),
|
|
163
172
|
commitMessage: [
|
|
164
173
|
`Scaffold with mfw create ${cliVersion()}`,
|
|
165
|
-
`Channels: ${none(answers.channels)}\nPlugins: ${none(answers.plugins)}${answers.auth !== undefined ? `\nAuth: ${answers.auth}` : ""}`,
|
|
174
|
+
`Channels: ${none(answers.channels)}\nPlugins: ${none(answers.plugins)}${answers.auth !== undefined ? `\nAuth: ${answers.auth}` : ""}${answers.directory !== undefined ? `\nDirectory: ${answers.directory}` : ""}`,
|
|
166
175
|
],
|
|
167
176
|
},
|
|
168
177
|
run,
|
package/src/program.ts
CHANGED
|
@@ -67,6 +67,10 @@ function buildProgram(handlers: ProgramHandlers, result: { code: number }): Comm
|
|
|
67
67
|
"--auth <id>",
|
|
68
68
|
`the HTTP channel's auth provider, required with it: ${CATALOG.filter((e) => e.kind === "auth").map((e) => e.id).join(", ")}`,
|
|
69
69
|
)
|
|
70
|
+
.option(
|
|
71
|
+
"--directory <id>",
|
|
72
|
+
`the user directory, who the people are and their roles: ${CATALOG.filter((e) => e.kind === "directory").map((e) => e.id).join(", ")} (default: none)`,
|
|
73
|
+
)
|
|
70
74
|
.option("--git-remote <url>", "the repository's origin, taken as typed (default: none)")
|
|
71
75
|
.option(
|
|
72
76
|
"--local-packages <folder>",
|
|
@@ -214,6 +218,35 @@ Examples:
|
|
|
214
218
|
inApp((app) => app.memory(["read", collection, ...(opts.limit === undefined ? [] : ["--limit", opts.limit])]))(),
|
|
215
219
|
);
|
|
216
220
|
|
|
221
|
+
const identity = program
|
|
222
|
+
.command("identity")
|
|
223
|
+
.summary("the accounts linked to a person")
|
|
224
|
+
.description(
|
|
225
|
+
"Lists, makes and undoes the links that make several accounts one person (one private area, the same roles), in a one-off container. People link their own accounts with a code; this is for the operator, where nobody can be shown one.",
|
|
226
|
+
)
|
|
227
|
+
.helpCommand(false)
|
|
228
|
+
.addHelpText("after", INSIDE_AN_APP);
|
|
229
|
+
identity
|
|
230
|
+
.command("links")
|
|
231
|
+
.summary("the linked accounts")
|
|
232
|
+
.description("Lists every linked account and the account it belongs to.")
|
|
233
|
+
.action(async () => inApp((app) => app.identity(["links"]))());
|
|
234
|
+
identity
|
|
235
|
+
.command("link")
|
|
236
|
+
.summary("links an account to another")
|
|
237
|
+
.description(
|
|
238
|
+
"Makes <identity> the same person as <owner>, both as <provider>:<id> (e.g. google-chat:users/123, oidc:3123). The running service applies it within five minutes.",
|
|
239
|
+
)
|
|
240
|
+
.argument("<identity>", "the account to link")
|
|
241
|
+
.argument("<owner>", "the account it belongs to")
|
|
242
|
+
.action(async (account: string, owner: string) => inApp((app) => app.identity(["link", account, owner]))());
|
|
243
|
+
identity
|
|
244
|
+
.command("unlink")
|
|
245
|
+
.summary("undoes an account's link")
|
|
246
|
+
.description("Makes <identity> a person of its own again; what it had before being linked is still there.")
|
|
247
|
+
.argument("<identity>", "as mfw identity links prints it")
|
|
248
|
+
.action(async (account: string) => inApp((app) => app.identity(["unlink", account]))());
|
|
249
|
+
|
|
217
250
|
program
|
|
218
251
|
.command("reset")
|
|
219
252
|
.summary("deletes memory or the wiki, after confirmation")
|
package/src/render.ts
CHANGED
|
@@ -28,6 +28,8 @@ export type RenderInput = {
|
|
|
28
28
|
plugins: string[];
|
|
29
29
|
/** The HTTP channel's auth provider: required with it, refused without it. */
|
|
30
30
|
auth?: string;
|
|
31
|
+
/** The user directory: who the people are and their roles. */
|
|
32
|
+
directory?: string;
|
|
31
33
|
versions: Record<string, string>;
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -66,18 +68,19 @@ export function renderApp(input: RenderInput): Map<string, string> {
|
|
|
66
68
|
const channels = selected("channel", input.channels);
|
|
67
69
|
const tools = selected("tool", input.plugins);
|
|
68
70
|
const auth = selected("auth", input.auth === undefined ? [] : [input.auth])[0];
|
|
71
|
+
const directory = selected("directory", input.directory === undefined ? [] : [input.directory])[0];
|
|
69
72
|
const assistantName = input.assistantName.trim();
|
|
70
73
|
|
|
71
74
|
return new Map([
|
|
72
75
|
[".dockerignore", dockerignore],
|
|
73
|
-
[".env.example", renderEnv(channels, auth, tools)],
|
|
76
|
+
[".env.example", renderEnv(channels, auth, tools, directory)],
|
|
74
77
|
[".gitignore", gitignore],
|
|
75
78
|
["Dockerfile", dockerfile],
|
|
76
|
-
["README.md", renderReadme(input.name, channels, auth, tools)],
|
|
79
|
+
["README.md", renderReadme(input.name, channels, auth, tools, directory)],
|
|
77
80
|
["docker-compose.yml", renderCompose(input.name, channels.some((c) => c.id === "http"))],
|
|
78
81
|
["markdown.d.ts", markdownDts],
|
|
79
|
-
["mercury.config.ts", renderConfig(channels, auth, tools)],
|
|
80
|
-
["package.json", renderPackageJson(input.name, channels, auth, tools, input.versions)],
|
|
82
|
+
["mercury.config.ts", renderConfig(channels, auth, tools, directory)],
|
|
83
|
+
["package.json", renderPackageJson(input.name, channels, auth, tools, input.versions, directory)],
|
|
81
84
|
["persona/identity.md", `You are ${assistantName}, ${input.role.trim().replace(/\.+$/, "")}.\n`],
|
|
82
85
|
// A replacer function, not a string: in a replacement string "$&" and the
|
|
83
86
|
// like are patterns, and the name must land verbatim.
|
|
@@ -112,7 +115,8 @@ function validate(input: RenderInput): void {
|
|
|
112
115
|
if (/[\r\n]/.test(input.assistantName) || /[\r\n]/.test(input.role)) {
|
|
113
116
|
throw new Error("The assistant name and role must each be one line");
|
|
114
117
|
}
|
|
115
|
-
const selection =
|
|
118
|
+
const selection =
|
|
119
|
+
selectionError(input.channels, input.plugins, input.auth, input.directory) ?? pairingError(input.channels, input.auth);
|
|
116
120
|
if (selection !== undefined) {
|
|
117
121
|
throw new Error(selection);
|
|
118
122
|
}
|
|
@@ -121,14 +125,15 @@ function validate(input: RenderInput): void {
|
|
|
121
125
|
/** The ids of the catalog entries of `kind`. */
|
|
122
126
|
const idsOf = (kind: CatalogEntry["kind"]): string[] => CATALOG.filter((e) => e.kind === kind).map((e) => e.id);
|
|
123
127
|
|
|
124
|
-
/** Names the first channel, plugin
|
|
125
|
-
* have, with the valid ones, or undefined when all are known. Shared
|
|
126
|
-
* command, which checks the flags before asking anything. */
|
|
127
|
-
export function selectionError(channels: string[], plugins: string[], auth?: string): string | undefined {
|
|
128
|
+
/** Names the first channel, plugin, auth provider or directory id the catalog
|
|
129
|
+
* doesn't have, with the valid ones, or undefined when all are known. Shared
|
|
130
|
+
* with the command, which checks the flags before asking anything. */
|
|
131
|
+
export function selectionError(channels: string[], plugins: string[], auth?: string, directory?: string): string | undefined {
|
|
128
132
|
for (const [kind, ids, label] of [
|
|
129
133
|
["channel", channels, "channel"],
|
|
130
134
|
["tool", plugins, "plugin"],
|
|
131
135
|
["auth", auth === undefined ? [] : [auth], "auth provider"],
|
|
136
|
+
["directory", directory === undefined ? [] : [directory], "directory"],
|
|
132
137
|
] as const) {
|
|
133
138
|
const valid = idsOf(kind);
|
|
134
139
|
const unknown = ids.find((id) => !valid.includes(id));
|
|
@@ -158,7 +163,12 @@ function selected(kind: CatalogEntry["kind"], ids: string[]): CatalogEntry[] {
|
|
|
158
163
|
/** `mercury.config.ts`: imports, the formatter helpers of the plugins that have
|
|
159
164
|
* any, then the config with each plugin (wrapped in the formatter when it has
|
|
160
165
|
* starting rules) and channel. */
|
|
161
|
-
function renderConfig(
|
|
166
|
+
function renderConfig(
|
|
167
|
+
channels: CatalogEntry[],
|
|
168
|
+
auth: CatalogEntry | undefined,
|
|
169
|
+
tools: CatalogEntry[],
|
|
170
|
+
directory: CatalogEntry | undefined,
|
|
171
|
+
): string {
|
|
162
172
|
const withRules = tools.filter((t) => t.formatter);
|
|
163
173
|
const imports = ['import { defineMercuryConfig } from "@mercury-fw/core";'];
|
|
164
174
|
if (withRules.length > 0) {
|
|
@@ -168,7 +178,7 @@ function renderConfig(channels: CatalogEntry[], auth: CatalogEntry | undefined,
|
|
|
168
178
|
const names = t.formatter ? `${t.exportName}, type ${t.formatter.displaysType}` : t.exportName;
|
|
169
179
|
imports.push(`import { ${names} } from "${t.package}";`);
|
|
170
180
|
}
|
|
171
|
-
for (const c of [...channels, ...(auth ? [auth] : [])]) {
|
|
181
|
+
for (const c of [...channels, ...(auth ? [auth] : []), ...(directory ? [directory] : [])]) {
|
|
172
182
|
imports.push(`import { ${c.exportName} } from "${c.package}";`);
|
|
173
183
|
}
|
|
174
184
|
imports.push('import identity from "./persona/identity.md" with { type: "text" };');
|
|
@@ -191,6 +201,7 @@ function renderConfig(channels: CatalogEntry[], auth: CatalogEntry | undefined,
|
|
|
191
201
|
plugins,
|
|
192
202
|
channelList,
|
|
193
203
|
...(auth ? [` auth: ${auth.exportName},`] : []),
|
|
204
|
+
...(directory ? [` directory: ${directory.exportName},`] : []),
|
|
194
205
|
"});",
|
|
195
206
|
"",
|
|
196
207
|
].join("\n");
|
|
@@ -213,15 +224,17 @@ function renderPluginEntry(t: CatalogEntry): string {
|
|
|
213
224
|
|
|
214
225
|
/** `package.json`: the core, the formatter when a plugin is wrapped in it, the
|
|
215
226
|
* chosen packages, and every tool plugin trusted to run its postinstall (it
|
|
216
|
-
* downloads the plugin's CLI). */
|
|
227
|
+
* downloads the plugin's CLI), plus what a chosen entry says it needs trusted. */
|
|
217
228
|
function renderPackageJson(
|
|
218
229
|
name: string,
|
|
219
230
|
channels: CatalogEntry[],
|
|
220
231
|
auth: CatalogEntry | undefined,
|
|
221
232
|
tools: CatalogEntry[],
|
|
222
233
|
versions: Record<string, string>,
|
|
234
|
+
directory: CatalogEntry | undefined,
|
|
223
235
|
): string {
|
|
224
|
-
const
|
|
236
|
+
const chosen = [...channels, ...(auth ? [auth] : []), ...tools, ...(directory ? [directory] : [])];
|
|
237
|
+
const packages = [...new Set(["@mercury-fw/core", ...chosen.map((e) => e.package)])];
|
|
225
238
|
if (tools.some((t) => t.formatter)) {
|
|
226
239
|
packages.push("@mercury-fw/formatter");
|
|
227
240
|
}
|
|
@@ -248,7 +261,7 @@ function renderPackageJson(
|
|
|
248
261
|
// hands the app's commands over to it.
|
|
249
262
|
devDependencies: { "@mercury-fw/cli": `^${cli}`, "@types/bun": "^1.4.2", typescript: "^6.0.3" },
|
|
250
263
|
};
|
|
251
|
-
const trusted = [...new Set([...tools.map((t) => t.package), ...
|
|
264
|
+
const trusted = [...new Set([...tools.map((t) => t.package), ...chosen.flatMap((e) => e.trusts ?? [])])];
|
|
252
265
|
if (trusted.length > 0) {
|
|
253
266
|
manifest.trustedDependencies = trusted.sort();
|
|
254
267
|
}
|
|
@@ -257,12 +270,18 @@ function renderPackageJson(
|
|
|
257
270
|
|
|
258
271
|
/** The env example: the core's variables, then a section per chosen entry
|
|
259
272
|
* that reads any variable. */
|
|
260
|
-
function renderEnv(
|
|
273
|
+
function renderEnv(
|
|
274
|
+
channels: CatalogEntry[],
|
|
275
|
+
auth: CatalogEntry | undefined,
|
|
276
|
+
tools: CatalogEntry[],
|
|
277
|
+
directory: CatalogEntry | undefined,
|
|
278
|
+
): string {
|
|
261
279
|
const block = (vars: EnvVar[]) => vars.map((v) => `# ${v.comment}\n${v.name}=${v.value ?? ""}`).join("\n");
|
|
262
280
|
const sections = [block(CORE_ENV)];
|
|
263
|
-
for (const entry of [...channels, ...(auth ? [auth] : []), ...tools]) {
|
|
281
|
+
for (const entry of [...channels, ...(auth ? [auth] : []), ...tools, ...(directory ? [directory] : [])]) {
|
|
264
282
|
if (entry.env.length > 0) {
|
|
265
|
-
|
|
283
|
+
// A directory can share its id with a tool plugin of the same package.
|
|
284
|
+
sections.push(`# --- ${entry.id}${entry.kind === "directory" ? " directory" : ""}\n${block(entry.env)}`);
|
|
266
285
|
}
|
|
267
286
|
}
|
|
268
287
|
return `${sections.join("\n\n")}\n`;
|
|
@@ -317,7 +336,13 @@ function renderCompose(name: string, hasHttp: boolean): string {
|
|
|
317
336
|
}
|
|
318
337
|
|
|
319
338
|
/** The app's README: what it was scaffolded with and how to run it. */
|
|
320
|
-
function renderReadme(
|
|
339
|
+
function renderReadme(
|
|
340
|
+
name: string,
|
|
341
|
+
channels: CatalogEntry[],
|
|
342
|
+
auth: CatalogEntry | undefined,
|
|
343
|
+
tools: CatalogEntry[],
|
|
344
|
+
directory: CatalogEntry | undefined,
|
|
345
|
+
): string {
|
|
321
346
|
const list = (entries: CatalogEntry[]) => (entries.length > 0 ? entries.map((e) => e.id).join(", ") : "none");
|
|
322
347
|
return `# ${name}
|
|
323
348
|
|
|
@@ -325,7 +350,7 @@ A Mercury app, scaffolded by \`mfw create\`.
|
|
|
325
350
|
|
|
326
351
|
- Channels: ${list(channels)}
|
|
327
352
|
${auth ? `- Auth: ${auth.id}\n` : ""}- Tool plugins: ${list(tools)}
|
|
328
|
-
|
|
353
|
+
${directory ? `- Directory: ${directory.id}\n` : ""}
|
|
329
354
|
## Layout
|
|
330
355
|
|
|
331
356
|
- \`mercury.config.ts\`: what the app is made of (tool plugins, channels, how their lists read) and the assistant's persona.
|
package/src/wizard.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type Answers = {
|
|
|
19
19
|
plugins: string[];
|
|
20
20
|
/** The HTTP channel's auth provider, there exactly when that channel is. */
|
|
21
21
|
auth?: string;
|
|
22
|
+
/** The user directory, when there's one. */
|
|
23
|
+
directory?: string;
|
|
22
24
|
gitRemote?: string;
|
|
23
25
|
};
|
|
24
26
|
|
|
@@ -87,6 +89,17 @@ export async function askAnswers(args: CreateArgs, dir: string): Promise<Answers
|
|
|
87
89
|
}
|
|
88
90
|
const plugins = await pick("tool", "Tool plugins (space to select)", args.plugins ?? []);
|
|
89
91
|
if (plugins === undefined) return cancelled();
|
|
92
|
+
const NO_DIRECTORY = "";
|
|
93
|
+
const pickedDirectory = await p.select({
|
|
94
|
+
message: "User directory (who the people are and their roles; none: whoever the channel says, with no roles)",
|
|
95
|
+
options: [
|
|
96
|
+
{ value: NO_DIRECTORY, label: "none" },
|
|
97
|
+
...CATALOG.filter((e) => e.kind === "directory").map((e) => ({ value: e.id, label: e.id, hint: e.package })),
|
|
98
|
+
],
|
|
99
|
+
initialValue: args.directory ?? NO_DIRECTORY,
|
|
100
|
+
});
|
|
101
|
+
if (p.isCancel(pickedDirectory)) return cancelled();
|
|
102
|
+
const directory = pickedDirectory === NO_DIRECTORY ? undefined : pickedDirectory;
|
|
90
103
|
// Asked only when there will be a repository and the flag didn't say.
|
|
91
104
|
let gitRemote = args.gitRemote;
|
|
92
105
|
if (args.git && gitRemote === undefined) {
|
|
@@ -105,6 +118,7 @@ export async function askAnswers(args: CreateArgs, dir: string): Promise<Answers
|
|
|
105
118
|
`Channels: ${channels.length > 0 ? channels.join(", ") : "none"}`,
|
|
106
119
|
...(auth !== undefined ? [`Auth: ${auth}`] : []),
|
|
107
120
|
`Tool plugins: ${plugins.length > 0 ? plugins.join(", ") : "none"}`,
|
|
121
|
+
`Directory: ${directory ?? "none"}`,
|
|
108
122
|
...(args.git ? [`Origin: ${gitRemote ?? "none"}`] : []),
|
|
109
123
|
].join("\n"),
|
|
110
124
|
"Summary",
|
|
@@ -118,6 +132,7 @@ export async function askAnswers(args: CreateArgs, dir: string): Promise<Answers
|
|
|
118
132
|
channels,
|
|
119
133
|
plugins,
|
|
120
134
|
...(auth !== undefined ? { auth } : {}),
|
|
135
|
+
...(directory !== undefined ? { directory } : {}),
|
|
121
136
|
...(gitRemote !== undefined ? { gitRemote } : {}),
|
|
122
137
|
};
|
|
123
138
|
}
|