@mercury-fw/cli 0.38.1 → 0.40.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 +43 -0
- package/README.md +31 -10
- package/dist/src/app/commands.d.ts +14 -10
- package/dist/src/app/credentials.d.ts +0 -5
- package/package.json +9 -9
- package/src/app/commands.ts +68 -32
- package/src/app/credentials.ts +4 -49
- package/src/catalog.ts +1 -0
- package/src/program.ts +19 -11
- package/src/render.ts +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @mercury-fw/cli
|
|
2
2
|
|
|
3
|
+
## 0.40.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0739f77: A plugin acts as the person Mercury is talking to, who logs in to the service through Mercury.
|
|
8
|
+
|
|
9
|
+
- Plugin contract version 4: a plugin declares `actsAs: "person" | "mercury"`, and one acting as the person contributes a `login`; its tools get the turn's `person` and `requireLogin` in their context.
|
|
10
|
+
- A person is offered only the plugins acting as the person: their prompt fragments, skills and tools. A plugin acting as Mercury, or declaring nothing, is offered on the terminal only until people can be allowed to make Mercury act as itself; the startup log names it.
|
|
11
|
+
- `createCliTool` adds `--user` with the person's id to every command, staged confirmations included, and when the CLI exits with code 3 (not logged in) returns their login instead. `createCliPersonLogin` builds a plugin's `login` on a CLI's two-step remote login. A failed `runCli` carries `exitCode`.
|
|
12
|
+
- Channel contract version 4: channels get `logins` (`accept` a callback URL, `complete` a login) and `detectLoginRequired`.
|
|
13
|
+
- The HTTP channel, with `HTTP_SURFACE_PUBLIC_URL` set, streams a `login` event with the link to open and takes the person back at `GET /login/callback`, public and protected by the single-use state.
|
|
14
|
+
- `mercury.cliCredentials` can declare `userSetup`, run by `mfw credentials setup <plugin> --user-app`: the app people log in through, set up without logging anyone in.
|
|
15
|
+
- plugin-jira and plugin-bitbucket act as the person, on CLIs 2.3.0: set up their people's app with `--user-app` and register `<HTTP_SURFACE_PUBLIC_URL>/login/callback` on it. Jira's skill now says `currentUser()` is the person.
|
|
16
|
+
- plugin-atlassian-admin acts as Mercury, on CLI 0.2.0, whose `init` asks for the key without echoing it: until permissions exist it's available on the terminal only.
|
|
17
|
+
- A third-party plugin needs `apiVersion: 4` to load, and `actsAs: "person"` with a `login` to be offered to people.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [0739f77]
|
|
22
|
+
- @mercury-fw/core@0.40.0
|
|
23
|
+
- @mercury-fw/utils@0.40.0
|
|
24
|
+
|
|
25
|
+
## 0.39.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 881835c: A CLI's login is set up inside the app's container, no longer carried in the env file.
|
|
30
|
+
|
|
31
|
+
- A plugin declares in `mercury.cliCredentials` the commands that set its CLI's login up (`setup`, required), check it (`check`) and log an identity out (`logout`), each a binary followed by its arguments.
|
|
32
|
+
- `mfw credentials setup <plugin>` runs the declared setup in a one-off container on your terminal, so the CLI writes its login straight onto the credentials volume; `mfw credentials check <plugin>` runs the declared check.
|
|
33
|
+
- `mfw credentials reset <plugin> [--user <key>]` runs the declared logout, of the identity Mercury runs as or of one person, instead of deleting the CLI's folder.
|
|
34
|
+
- `mfw credentials set` is removed, and the core no longer unpacks `*_CONFIG_TAR_B64` variables at startup: it reports a declared login that isn't set up yet, and still links a login kept outside `~/.config` to the volume.
|
|
35
|
+
- A command the model writes with `--user` is refused: which person a CLI acts as is never the model's to pick.
|
|
36
|
+
- `cliUserId` (`@mercury-fw/utils`) maps a user key to the id a CLI knows a person by.
|
|
37
|
+
- plugin-jira and plugin-bitbucket pin their CLIs at 2.1.0, which keep the service identity and each person apart and refuse the old login folder. After updating, remove the plugin's `*_CONFIG_TAR_B64` line from the env file and run `mfw credentials setup` once; on Jira, Mercury now runs as an Atlassian Service Account.
|
|
38
|
+
- plugin-atlassian-admin keeps its CLI at 0.1.2, whose `init` doesn't prompt for the organization key: until it does, write the key from `mfw shell` with `atlassian-admin init --api-key <KEY> --org-id <ORG_ID>` (the plugin's README says so).
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- Updated dependencies [881835c]
|
|
43
|
+
- @mercury-fw/core@0.39.0
|
|
44
|
+
- @mercury-fw/utils@0.39.0
|
|
45
|
+
|
|
3
46
|
## 0.38.1
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
- [`mfw memory list`](#mfw-memory-list)
|
|
18
18
|
- [`mfw memory read <collection> [--limit N]`](#mfw-memory-read-collection---limit-n)
|
|
19
19
|
- [`mfw reset <memory|wiki>`](#mfw-reset-memorywiki)
|
|
20
|
-
- [`mfw credentials
|
|
21
|
-
- [`mfw credentials
|
|
20
|
+
- [`mfw credentials setup <plugin>`](#mfw-credentials-setup-plugin)
|
|
21
|
+
- [`mfw credentials check <plugin>`](#mfw-credentials-check-plugin)
|
|
22
|
+
- [`mfw credentials reset <plugin> [--user <key>]`](#mfw-credentials-reset-plugin---user-key)
|
|
22
23
|
- [`mfw google-chat set-key <key-file> [--subscription <name>]`](#mfw-google-chat-set-key-key-file---subscription-name)
|
|
23
24
|
- [`mfw local-packages <folder>` / `mfw local-packages --off`](#mfw-local-packages-folder--mfw-local-packages---off)
|
|
24
25
|
- [`mfw e2e [tests...] [--repeat N]`](#mfw-e2e-tests---repeat-n)
|
|
@@ -185,24 +186,44 @@ mfw reset wiki
|
|
|
185
186
|
|
|
186
187
|
Useful for clearing out test data; the other layer isn't touched.
|
|
187
188
|
|
|
188
|
-
### `mfw credentials
|
|
189
|
+
### `mfw credentials setup <plugin>`
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
Sets up a plugin's CLI login, for a plugin whose CLI keeps it in a folder under the home and reads it from there at runtime. The plugin declares that folder in its `package.json`, with the commands that set the login up, check it and log an identity out, each a binary on the container's PATH followed by its arguments:
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
```json
|
|
194
|
+
"mercury": {
|
|
195
|
+
"cliCredentials": {
|
|
196
|
+
"folder": "jira-cli",
|
|
197
|
+
"setup": ["jira", "init"],
|
|
198
|
+
"check": ["jira", "doctor"],
|
|
199
|
+
"logout": ["jira", "auth", "logout"]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`folder` is under `~/.config`, the usual place; `{ "path": ".aws" }` declares one anywhere else under the home. `setup` is required, `check` and `logout` aren't. `<plugin>` names the declaration by the plugin's package or by its folder; a name the app doesn't have is an error listing the ones it has.
|
|
205
|
+
|
|
206
|
+
`setup` runs the declared setup in a one-off container of the app (`docker compose run --rm --no-deps mercury jira init`) on your terminal, so the CLI asks you for what it needs and writes the login straight onto the credentials volume, mounted on `~/.config`. A login declared elsewhere in the home lives on the volume under `~/.config/mercury-home`, and its usual place is linked there first (the app makes the same link at every start). What the CLI writes back while running, like a refreshed token or a person's login, stays on the volume across redeploys. This sets up the identity Mercury itself runs as; people log in on their own, through Mercury. Nothing goes in the env file, and nothing is carried between machines: each environment sets up its own. A CLI that authenticates any other way isn't covered by this, and neither is one that deletes its own folder and makes it again, since that replaces the link.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
mfw credentials setup @mercury-fw/plugin-jira
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `mfw credentials check <plugin>`
|
|
213
|
+
|
|
214
|
+
Runs the check the plugin declares (the CLI's `doctor`, for the first-party plugins) in a one-off container, and exits with its code.
|
|
193
215
|
|
|
194
216
|
```bash
|
|
195
|
-
mfw credentials
|
|
196
|
-
mfw credentials set @mercury-fw/plugin-bitbucket --from ~/work/bitbucket-login
|
|
197
|
-
mfw credentials set jira-cli --print
|
|
217
|
+
mfw credentials check jira-cli
|
|
198
218
|
```
|
|
199
219
|
|
|
200
|
-
### `mfw credentials reset <plugin
|
|
220
|
+
### `mfw credentials reset <plugin> [--user <key>]`
|
|
201
221
|
|
|
202
|
-
|
|
222
|
+
Runs the logout the plugin declares in a one-off container, after you type the name it's declared by (its folder). Without `--user` it logs out the identity Mercury runs as, which then needs `mfw credentials setup` again; with `--user` it logs out one person, by their user key (`<provider>:<id>`), who logs in again through Mercury the next time they need it. The CLI only forgets the login: the tokens aren't revoked at the service.
|
|
203
223
|
|
|
204
224
|
```bash
|
|
205
225
|
mfw credentials reset jira-cli
|
|
226
|
+
mfw credentials reset jira-cli --user oidc:312345678901234567
|
|
206
227
|
```
|
|
207
228
|
|
|
208
229
|
### `mfw google-chat set-key <key-file> [--subscription <name>]`
|
|
@@ -56,17 +56,21 @@ export declare function appCommands(app: App, deps: AppDeps): {
|
|
|
56
56
|
* brings its service back up on an empty volume. The volume's real name
|
|
57
57
|
* comes from the compose file, and a wrong answer deletes nothing. */
|
|
58
58
|
reset: (target: ResetTarget) => Promise<number>;
|
|
59
|
-
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
/** Runs the plugin's declared setup (with `userApp`, of the app people log
|
|
60
|
+
* in through) in a one-off container on the user's terminal (without `-T`, compose attaches a TTY whenever stdin is one),
|
|
61
|
+
* so the CLI asks what it needs and writes its login straight onto the
|
|
62
|
+
* credentials volume. */
|
|
63
|
+
credentialsSetup: (plugin: string, { userApp }?: {
|
|
64
|
+
userApp?: boolean;
|
|
65
|
+
}) => Promise<number>;
|
|
66
|
+
/** Runs the plugin's declared check in a one-off container. */
|
|
67
|
+
credentialsCheck: (plugin: string) => Promise<number>;
|
|
68
|
+
/** Runs the plugin's declared logout, of the service identity or of the
|
|
69
|
+
* person whose user key is `user`, once the user types the declared name.
|
|
70
|
+
* A wrong answer runs nothing. */
|
|
71
|
+
credentialsReset: (plugin: string, { user }: {
|
|
72
|
+
user?: string;
|
|
65
73
|
}) => Promise<number>;
|
|
66
|
-
/** Deletes the plugin's CLI folder from the credentials volume once the
|
|
67
|
-
* user types the folder's name, so its variable is unpacked again at the
|
|
68
|
-
* next start. A wrong answer deletes nothing. */
|
|
69
|
-
credentialsReset: (plugin: string) => Promise<number>;
|
|
70
74
|
/** Writes the Google Chat channel's service account key (the JSON file
|
|
71
75
|
* at `keyFile`) into the app's env file, and the Pub/Sub subscription when
|
|
72
76
|
* given. Everything is checked before anything is written; the key is
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/** `from` packed as `<folder>/…` into a base64 tar.gz on one line. The real
|
|
2
|
-
* folder behind `from` is archived through a symlink named `folder` and
|
|
3
|
-
* dereferenced (`-h`): whatever `from` is called, and even when it's itself a
|
|
4
|
-
* symlink (dotfiles), the archive holds the files under the CLI's folder name. */
|
|
5
|
-
export declare function packCredentials(from: string, folder: string): Promise<string>;
|
|
6
1
|
/** Sets `name=value` in the env file at `file`. Every line that assigns
|
|
7
2
|
* `name` (`export name=` and spaces around `=` included) gives way to one
|
|
8
3
|
* line, where the first was; with none, it's appended. Every other line stays
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,19 +41,19 @@
|
|
|
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.40.0",
|
|
45
|
+
"@mercury-fw/utils": "0.40.0",
|
|
46
46
|
"commander": "^15.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@mercury-fw/channel-google-chat": "0.
|
|
50
|
-
"@mercury-fw/channel-http": "0.
|
|
49
|
+
"@mercury-fw/channel-google-chat": "0.4.0",
|
|
50
|
+
"@mercury-fw/channel-http": "0.5.0",
|
|
51
51
|
"@mercury-fw/auth-oidc": "0.1.0",
|
|
52
52
|
"@mercury-fw/auth-static": "0.1.0",
|
|
53
|
-
"@mercury-fw/formatter": "0.
|
|
54
|
-
"@mercury-fw/plugin-atlassian-admin": "0.
|
|
55
|
-
"@mercury-fw/plugin-bitbucket": "0.
|
|
56
|
-
"@mercury-fw/plugin-jira": "0.
|
|
53
|
+
"@mercury-fw/formatter": "0.40.0",
|
|
54
|
+
"@mercury-fw/plugin-atlassian-admin": "0.3.0",
|
|
55
|
+
"@mercury-fw/plugin-bitbucket": "0.3.0",
|
|
56
|
+
"@mercury-fw/plugin-jira": "0.6.0",
|
|
57
57
|
"@mercury-fw/typescript-config": "*",
|
|
58
58
|
"@types/bun": "^1.4.2",
|
|
59
59
|
"typescript": "^6.0.3"
|
package/src/app/commands.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
import {
|
|
11
|
-
import { appCliCredentials, volumePath, type CliCredentials } from "@mercury-fw/utils";
|
|
12
|
-
import {
|
|
10
|
+
import { join, relative, resolve } from "node:path";
|
|
11
|
+
import { appCliCredentials, cliUserId, volumePath, type CliCredentials } from "@mercury-fw/utils";
|
|
12
|
+
import { readServiceAccountKey, setEnvVar } from "./credentials.ts";
|
|
13
13
|
import type { App } from "./find-app.ts";
|
|
14
14
|
import { copyPacks, LOCAL_PACKS_DIR, readPacks, withLocalOverrides, withoutLocalOverrides } from "./local-packages.ts";
|
|
15
15
|
import { findTests, loadTest } from "../e2e/load.ts";
|
|
@@ -128,38 +128,45 @@ export function appCommands(app: App, deps: AppDeps) {
|
|
|
128
128
|
}
|
|
129
129
|
return 0;
|
|
130
130
|
},
|
|
131
|
-
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
deps.print(`${variable} set in ${envFile}, from ${source}.`);
|
|
145
|
-
deps.print(
|
|
146
|
-
`The app unpacks it at its next start, if the volume has no ${name} folder yet; if it has one, run mfw credentials reset ${name} first.`,
|
|
147
|
-
);
|
|
148
|
-
return 0;
|
|
131
|
+
/** Runs the plugin's declared setup (with `userApp`, of the app people log
|
|
132
|
+
* in through) in a one-off container on the user's terminal (without `-T`, compose attaches a TTY whenever stdin is one),
|
|
133
|
+
* so the CLI asks what it needs and writes its login straight onto the
|
|
134
|
+
* credentials volume. */
|
|
135
|
+
credentialsSetup: async (plugin: string, { userApp = false }: { userApp?: boolean } = {}) => {
|
|
136
|
+
const declared = credentialsOf(app, plugin);
|
|
137
|
+
const setup = userApp ? declared.userSetup : declared.setup;
|
|
138
|
+
if (setup === undefined) throw new Error(`${declared.package} declares no setup for an app people log in through: it doesn't act as the person.`);
|
|
139
|
+
const code = await deps.run([...COMPOSE, "run", "--rm", "--no-deps", SERVICE, ...inContainer(declared, setup)], {
|
|
140
|
+
cwd: app.dir,
|
|
141
|
+
});
|
|
142
|
+
if (code === 0 && declared.check !== undefined) deps.print(`Next: mfw credentials check ${declared.package}`);
|
|
143
|
+
return code;
|
|
149
144
|
},
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
/** Runs the plugin's declared check in a one-off container. */
|
|
146
|
+
credentialsCheck: async (plugin: string) => {
|
|
147
|
+
const declared = credentialsOf(app, plugin);
|
|
148
|
+
if (declared.check === undefined) throw new Error(`${declared.package} declares no command to check its CLI's login.`);
|
|
149
|
+
return deps.run([...COMPOSE, "run", "--rm", "--no-deps", "-T", SERVICE, ...inContainer(declared, declared.check)], {
|
|
150
|
+
cwd: app.dir,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
/** Runs the plugin's declared logout, of the service identity or of the
|
|
154
|
+
* person whose user key is `user`, once the user types the declared name.
|
|
155
|
+
* A wrong answer runs nothing. */
|
|
156
|
+
credentialsReset: async (plugin: string, { user }: { user?: string }) => {
|
|
157
|
+
const declared = credentialsOf(app, plugin);
|
|
158
|
+
if (declared.logout === undefined) throw new Error(`${declared.package} declares no command to log its CLI out.`);
|
|
159
|
+
const who =
|
|
160
|
+
user === undefined
|
|
161
|
+
? `This logs the service identity of ${declared.package}'s CLI out: commands that run as it fail until mfw credentials setup ${declared.package}.`
|
|
162
|
+
: `This logs ${user} out of ${declared.package}'s CLI: they log in again the next time they need it.`;
|
|
163
|
+
const answer = await deps.ask(`${who} Type ${declared.name} to confirm: `);
|
|
164
|
+
if (answer.trim() !== declared.name) {
|
|
165
|
+
deps.print("Not confirmed: nobody logged out.");
|
|
160
166
|
return 1;
|
|
161
167
|
}
|
|
162
|
-
|
|
168
|
+
const argv = user === undefined ? declared.logout : [...declared.logout, "--user", cliUserId(user)];
|
|
169
|
+
return deps.run([...COMPOSE, "run", "--rm", "--no-deps", "-T", SERVICE, ...inContainer(declared, argv)], { cwd: app.dir });
|
|
163
170
|
},
|
|
164
171
|
/** Writes the Google Chat channel's service account key (the JSON file
|
|
165
172
|
* at `keyFile`) into the app's env file, and the Pub/Sub subscription when
|
|
@@ -276,6 +283,35 @@ export function appCommands(app: App, deps: AppDeps) {
|
|
|
276
283
|
}
|
|
277
284
|
}
|
|
278
285
|
|
|
286
|
+
/** Links `$2` (the home path) to `$1` (its place on the volume) the way the
|
|
287
|
+
* core does at startup, never replacing something else at `$2`, then runs the
|
|
288
|
+
* rest of the arguments. */
|
|
289
|
+
const LINK_THEN_RUN = [
|
|
290
|
+
'mkdir -p "$1" "$(dirname "$2")" || exit 1',
|
|
291
|
+
'if [ -L "$2" ] && [ "$(readlink "$2")" = "$1" ]; then :',
|
|
292
|
+
'elif [ -e "$2" ] || [ -L "$2" ]; then echo "$2 is already there and is not a link to the credentials volume" >&2; exit 1',
|
|
293
|
+
'else ln -s "$1" "$2" || exit 1; fi',
|
|
294
|
+
'shift 2',
|
|
295
|
+
'exec "$@"',
|
|
296
|
+
].join("\n");
|
|
297
|
+
|
|
298
|
+
/** `argv` as the app's container runs it for `declared`: as it is for a
|
|
299
|
+
* login under ~/.config, the volume's mount; for one kept elsewhere in the
|
|
300
|
+
* home, after linking that path to the volume, as the core does at startup,
|
|
301
|
+
* since a one-off container doesn't start the app. */
|
|
302
|
+
function inContainer(declared: CliCredentials, argv: string[]): string[] {
|
|
303
|
+
if (volumePath(declared.path) === declared.path) return argv;
|
|
304
|
+
return [
|
|
305
|
+
"sh",
|
|
306
|
+
"-c",
|
|
307
|
+
LINK_THEN_RUN,
|
|
308
|
+
"sh",
|
|
309
|
+
`/home/mercury/${volumePath(declared.path)}`,
|
|
310
|
+
`/home/mercury/${declared.path}`,
|
|
311
|
+
...argv,
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
279
315
|
/** The CLI credentials `plugin` names, by package or by folder, among those
|
|
280
316
|
* the app's dependencies declare; throws naming the ones it has otherwise,
|
|
281
317
|
* with the dependencies it couldn't read. */
|
package/src/app/credentials.ts
CHANGED
|
@@ -1,54 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* the core unpacks into `~/.config` on the volume at startup), and
|
|
5
|
-
* writing that variable into the app's env file. Also reading a service
|
|
6
|
-
* account key file, for `mfw google-chat set-key`.
|
|
2
|
+
* What `mfw google-chat set-key` needs: reading a service account key file,
|
|
3
|
+
* and writing a variable into the app's env file.
|
|
7
4
|
*/
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
existsSync,
|
|
11
|
-
mkdtempSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
realpathSync,
|
|
14
|
-
renameSync,
|
|
15
|
-
rmSync,
|
|
16
|
-
statSync,
|
|
17
|
-
symlinkSync,
|
|
18
|
-
writeFileSync,
|
|
19
|
-
} from "node:fs";
|
|
20
|
-
import { tmpdir } from "node:os";
|
|
21
|
-
import { dirname, join, resolve } from "node:path";
|
|
22
|
-
|
|
23
|
-
/** `from` packed as `<folder>/…` into a base64 tar.gz on one line. The real
|
|
24
|
-
* folder behind `from` is archived through a symlink named `folder` and
|
|
25
|
-
* dereferenced (`-h`): whatever `from` is called, and even when it's itself a
|
|
26
|
-
* symlink (dotfiles), the archive holds the files under the CLI's folder name. */
|
|
27
|
-
export async function packCredentials(from: string, folder: string): Promise<string> {
|
|
28
|
-
const source = resolve(from);
|
|
29
|
-
if (!existsSync(source) || !statSync(source).isDirectory()) {
|
|
30
|
-
throw new Error(`No folder at ${source}`);
|
|
31
|
-
}
|
|
32
|
-
const staging = mkdtempSync(join(tmpdir(), "mfw-credentials-"));
|
|
33
|
-
symlinkSync(realpathSync(source), join(staging, folder));
|
|
34
|
-
try {
|
|
35
|
-
// COPYFILE_DISABLE: macOS's tar would otherwise add ._ files for metadata.
|
|
36
|
-
const proc = Bun.spawn(["tar", "-czh", "--no-xattrs", "-f", "-", "-C", staging, folder], {
|
|
37
|
-
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
38
|
-
stdout: "pipe",
|
|
39
|
-
stderr: "pipe",
|
|
40
|
-
});
|
|
41
|
-
const [archive, stderr, code] = await Promise.all([
|
|
42
|
-
new Response(proc.stdout).arrayBuffer(),
|
|
43
|
-
new Response(proc.stderr).text(),
|
|
44
|
-
proc.exited,
|
|
45
|
-
]);
|
|
46
|
-
if (code !== 0) throw new Error(`tar failed packing ${source}: ${stderr.trim()}`);
|
|
47
|
-
return Buffer.from(archive).toString("base64");
|
|
48
|
-
} finally {
|
|
49
|
-
rmSync(staging, { recursive: true, force: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
5
|
+
import { chmodSync, existsSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
52
7
|
|
|
53
8
|
/** Sets `name=value` in the env file at `file`. Every line that assigns
|
|
54
9
|
* `name` (`export name=` and spaces around `=` included) gives way to one
|
package/src/catalog.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const CATALOG: CatalogEntry[] = [
|
|
|
58
58
|
env: [
|
|
59
59
|
{ name: "HTTP_SURFACE_PORT", comment: "Port of the HTTP surface", value: "4100" },
|
|
60
60
|
{ name: "HTTP_SURFACE_CORS_ORIGIN", comment: "Origin allowed to call the HTTP surface from a browser, if any" },
|
|
61
|
+
{ name: "HTTP_SURFACE_PUBLIC_URL", comment: "Address people's browsers reach the HTTP surface at, for logging in to the services plugins act on as them" },
|
|
61
62
|
],
|
|
62
63
|
},
|
|
63
64
|
{
|
package/src/program.ts
CHANGED
|
@@ -228,30 +228,38 @@ Examples:
|
|
|
228
228
|
.command("credentials")
|
|
229
229
|
.summary("the login of a plugin's CLI")
|
|
230
230
|
.description(
|
|
231
|
-
"For a tool plugin whose CLI keeps its login in a folder (under ~/.config, or elsewhere in the home), and declares it (mercury.cliCredentials in its package.json): the
|
|
231
|
+
"For a tool plugin whose CLI keeps its login in a folder (under ~/.config, or elsewhere in the home), and declares it with the commands that set it up (mercury.cliCredentials in its package.json): the commands run in a one-off container of the app, so the CLI writes its login straight onto the credentials volume, where it stays across redeploys. Every environment (development, production) sets up its own.",
|
|
232
232
|
)
|
|
233
233
|
.helpCommand(false)
|
|
234
234
|
.addHelpText("after", INSIDE_AN_APP);
|
|
235
235
|
credentials
|
|
236
|
-
.command("
|
|
237
|
-
.summary("
|
|
236
|
+
.command("setup")
|
|
237
|
+
.summary("sets up a CLI's login in the app's container")
|
|
238
238
|
.description(
|
|
239
|
-
"
|
|
239
|
+
"Runs the setup the plugin declares for its CLI's service identity (the identity Mercury acts as), in a one-off container on your terminal: the CLI asks for what it needs, and its login lands on the credentials volume. People log in on their own later, through Mercury: for a plugin acting as the person, --user-app sets up the app they log in through.",
|
|
240
240
|
)
|
|
241
241
|
.argument("<plugin>", "the plugin's package (@mercury-fw/plugin-jira) or its CLI's folder as declared (jira-cli)")
|
|
242
|
-
.option("--
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
inApp((app) => app.credentialsSet(plugin, { ...(opts.from === undefined ? {} : { from: opts.from }), print: opts.print ?? false }))(),
|
|
242
|
+
.option("--user-app", "set up instead the app people log in through, for a plugin acting as the person (logs nobody in)")
|
|
243
|
+
.action(async (plugin: string, opts: { userApp?: boolean }) =>
|
|
244
|
+
inApp((app) => app.credentialsSetup(plugin, opts.userApp ? { userApp: true } : {}))(),
|
|
246
245
|
);
|
|
246
|
+
credentials
|
|
247
|
+
.command("check")
|
|
248
|
+
.summary("checks a CLI's login")
|
|
249
|
+
.description("Runs the check the plugin declares for its CLI's login (its doctor, for the first-party plugins), in a one-off container.")
|
|
250
|
+
.argument("<plugin>", "the plugin's package or its CLI's folder")
|
|
251
|
+
.action(async (plugin: string) => inApp((app) => app.credentialsCheck(plugin))());
|
|
247
252
|
credentials
|
|
248
253
|
.command("reset")
|
|
249
|
-
.summary("
|
|
254
|
+
.summary("logs an identity out of a CLI")
|
|
250
255
|
.description(
|
|
251
|
-
"
|
|
256
|
+
"Runs the logout the plugin declares for its CLI, after you type the name it's declared by: of the service identity, which then needs mfw credentials setup again, or with --user of one person, who logs in again through Mercury the next time they need it. Local only: the tokens aren't revoked at the service.",
|
|
252
257
|
)
|
|
253
258
|
.argument("<plugin>", "the plugin's package or its CLI's folder")
|
|
254
|
-
.
|
|
259
|
+
.option("--user <key>", "the person to log out, by their user key (<provider>:<id>, e.g. oidc:312345678901234567)")
|
|
260
|
+
.action(async (plugin: string, opts: { user?: string }) =>
|
|
261
|
+
inApp((app) => app.credentialsReset(plugin, opts.user === undefined ? {} : { user: opts.user }))(),
|
|
262
|
+
);
|
|
255
263
|
|
|
256
264
|
const googleChat = program
|
|
257
265
|
.command("google-chat")
|
package/src/render.ts
CHANGED
|
@@ -371,18 +371,19 @@ The HTTP channel listens on \`http://<host>:4100\`, the port in \`HTTP_SURFACE_P
|
|
|
371
371
|
const CREDENTIALS_SECTION = `
|
|
372
372
|
## CLI credentials
|
|
373
373
|
|
|
374
|
-
Some tool plugins run a CLI. When that CLI keeps its login in a folder under your home and reads it at runtime to authenticate,
|
|
374
|
+
Some tool plugins run a CLI. When that CLI keeps its login in a folder under your home and reads it at runtime to authenticate, the plugin declares that folder and the commands that set it up (\`mercury.cliCredentials\` in its \`package.json\`: a folder under \`~/.config\`, or a path anywhere under the home), and the login is set up inside the app's container, on your terminal:
|
|
375
375
|
|
|
376
376
|
\`\`\`bash
|
|
377
|
-
mfw credentials
|
|
377
|
+
mfw credentials setup <plugin>
|
|
378
|
+
mfw credentials check <plugin>
|
|
378
379
|
\`\`\`
|
|
379
380
|
|
|
380
|
-
\`<plugin>\` is the plugin's package or its CLI's folder
|
|
381
|
+
\`<plugin>\` is the plugin's package or its CLI's folder; a name the app doesn't have lists the ones it has. \`setup\` runs the CLI's own setup in a one-off container, so it asks you for what it needs and writes the login straight onto the \`cli-credentials\` volume (mounted on \`~/.config\`: a folder declared elsewhere in the home lives under \`~/.config/mercury-home\`, and its usual place links there). What the CLI writes back afterwards, like a refreshed token, stays on the volume across redeploys. Nothing goes in \`.env\`, and nothing is carried from one machine to another: every environment sets up its own, development with its credentials, production with its own.
|
|
381
382
|
|
|
382
|
-
|
|
383
|
+
To log an identity out, the one Mercury runs as or, with \`--user\`, one person:
|
|
383
384
|
|
|
384
385
|
\`\`\`bash
|
|
385
|
-
mfw credentials reset <plugin>
|
|
386
|
+
mfw credentials reset <plugin> [--user <key>]
|
|
386
387
|
\`\`\`
|
|
387
388
|
|
|
388
389
|
A CLI that authenticates any other way isn't covered by this, and nothing guarantees it works in Mercury; neither does one that deletes its own folder and makes it again, since that replaces the link.
|