@mercury-fw/cli 0.42.0 → 0.43.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 +14 -0
- package/README.md +1 -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/args.ts +4 -0
- package/src/catalog.ts +29 -6
- package/src/main.ts +14 -5
- package/src/program.ts +4 -0
- package/src/render.ts +44 -19
- package/src/wizard.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @mercury-fw/cli
|
|
2
2
|
|
|
3
|
+
## 0.43.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 07eb858: ZITADEL as the user directory.
|
|
8
|
+
|
|
9
|
+
- `@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.
|
|
10
|
+
- `mfw create --directory <id>` (`static` or `zitadel`) gives the new app a user directory, and the wizard asks for one.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- @mercury-fw/core@0.43.0
|
|
15
|
+
- @mercury-fw/utils@0.43.0
|
|
16
|
+
|
|
3
17
|
## 0.42.0
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ Then it runs `bun install` in the app and creates a git repository on `main` wit
|
|
|
62
62
|
| `--channels <ids>` | Comma-separated: `google-chat`, `http`. |
|
|
63
63
|
| `--plugins <ids>` | Comma-separated: `jira`, `bitbucket`, `atlassian-admin`, `zitadel`. |
|
|
64
64
|
| `--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. |
|
|
65
|
+
| `--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
66
|
| `--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
67
|
| `--git-remote <url>` | The repository's `origin`, taken as typed. |
|
|
67
68
|
| `--no-install` | Don't run `bun install`. |
|
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.43.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.43.0",
|
|
45
|
+
"@mercury-fw/utils": "0.43.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.6.0",
|
|
53
|
+
"@mercury-fw/directory-static": "0.1.0",
|
|
54
|
+
"@mercury-fw/formatter": "0.43.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/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>",
|
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
|
}
|