@mercury-fw/cli 0.27.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +12 -0
- package/dist/src/app/commands.d.ts +7 -0
- package/dist/src/app/credentials.d.ts +9 -0
- package/package.json +4 -4
- package/src/app/commands.ts +32 -1
- package/src/app/credentials.ts +22 -1
- package/src/program.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @mercury-fw/cli
|
|
2
2
|
|
|
3
|
+
## 0.28.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 51bd439: `mfw google-chat set-key <key-file> [--subscription <name>]` writes the Google Chat channel's service account key (and its subscription) into the app's env file, the key on one line the way the channel reads it and never printed. The channel's setup uses it in place of the hand-written `sed`/`printf` step.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- @mercury-fw/core@0.28.0
|
|
12
|
+
|
|
3
13
|
## 0.27.1
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
- [`mfw reset <memory|wiki>`](#mfw-reset-memorywiki)
|
|
20
20
|
- [`mfw credentials set <plugin> [--from <dir>] [--print]`](#mfw-credentials-set-plugin---from-dir---print)
|
|
21
21
|
- [`mfw credentials reset <plugin>`](#mfw-credentials-reset-plugin)
|
|
22
|
+
- [`mfw google-chat set-key <key-file> [--subscription <name>]`](#mfw-google-chat-set-key-key-file---subscription-name)
|
|
22
23
|
- [Help](#help)
|
|
23
24
|
|
|
24
25
|
## Getting it
|
|
@@ -188,6 +189,17 @@ Deletes the plugin's CLI folder from the credentials volume, so the variable in
|
|
|
188
189
|
bunx mfw credentials reset jira
|
|
189
190
|
```
|
|
190
191
|
|
|
192
|
+
### `mfw google-chat set-key <key-file> [--subscription <name>]`
|
|
193
|
+
|
|
194
|
+
Writes the Google Chat channel's credentials into the app's `.env`, from the service account's JSON key (the file `gcloud iam service-accounts keys create` writes): `GOOGLE_CHAT_APP_CLIENT_EMAIL`, and `GOOGLE_CHAT_APP_PRIVATE_KEY` on one line with literal `\n`, the way the channel reads it. With `--subscription` (`projects/<project>/subscriptions/<name>`) it sets `GOOGLE_CHAT_PUBSUB_SUBSCRIPTION` too; without it, that line stays as it is, which is what you want when only the key changes. Older values and the empty lines `mfw create` leaves are replaced, the other lines stay alone, and the key is never printed.
|
|
195
|
+
|
|
196
|
+
It checks everything before writing: the app has to depend on `@mercury-fw/channel-google-chat`, the file has to be a service account key and the subscription has to have that shape, otherwise it exits 1 saying why and `.env` stays as it was. Delete the key file afterwards; `bunx mfw start` applies the change to a running app. The whole setup of the Chat app is in the [channel's README](https://github.com/lucabro81/mercury-fw/tree/main/packages/channels/channel-google-chat#setting-up-the-chat-app).
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
bunx mfw google-chat set-key key.json --subscription projects/my-project/subscriptions/mercury-chat-sub
|
|
200
|
+
bunx mfw google-chat set-key new-key.json
|
|
201
|
+
```
|
|
202
|
+
|
|
191
203
|
## Help
|
|
192
204
|
|
|
193
205
|
`mfw --help` lists every command, and `--help` after any of them describes it, down to the subcommands (`mfw vault write-curated --help`). A mistyped command gets a suggestion (`mfw strat` → "Did you mean start?"). Every argument is checked before anything runs: a wrong one exits 1 saying why, with no container started.
|
|
@@ -67,6 +67,13 @@ export declare function appCommands(app: App, deps: AppDeps): {
|
|
|
67
67
|
* user types the plugin's name, so its variable is unpacked again at the
|
|
68
68
|
* next start. A wrong answer deletes nothing. */
|
|
69
69
|
credentialsReset: (plugin: string) => Promise<number>;
|
|
70
|
+
/** Writes the Google Chat channel's service account key (the JSON file
|
|
71
|
+
* at `keyFile`) into the app's env file, and the Pub/Sub subscription when
|
|
72
|
+
* given. Everything is checked before anything is written; the key is
|
|
73
|
+
* never printed. */
|
|
74
|
+
googleChatSetKey: (keyFile: string, { subscription }: {
|
|
75
|
+
subscription?: string;
|
|
76
|
+
}) => Promise<number>;
|
|
70
77
|
};
|
|
71
78
|
/** The real deps: docker on the user's terminal, questions on `input`
|
|
72
79
|
* (stdin by default). */
|
|
@@ -10,3 +10,12 @@ export declare function packCredentials(from: string, folder: string): Promise<s
|
|
|
10
10
|
* keeping its permissions, so a failed write never leaves it half written; a
|
|
11
11
|
* missing file is created readable by its owner only. */
|
|
12
12
|
export declare function setEnvVar(file: string, name: string, value: string): void;
|
|
13
|
+
/** The two values of a Google Cloud service account key file (the JSON
|
|
14
|
+
* `gcloud iam service-accounts keys create` writes) that an app needs: its
|
|
15
|
+
* email, and its private key on one line with literal `\n`, the form an env
|
|
16
|
+
* file holds and the Google Chat channel unescapes. Throws naming `file` when
|
|
17
|
+
* it's missing or isn't such a key. */
|
|
18
|
+
export declare function readServiceAccountKey(file: string): {
|
|
19
|
+
clientEmail: string;
|
|
20
|
+
privateKey: string;
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"comment:shape": "The Mercury CLI (`mfw`). `mfw create <dir>` writes a new Mercury app from the template. The framework packages move in lockstep with it, so a new app gets them at the CLI's own version; plugins and channels at the registry's latest. The plugin and channel packages are devDependencies only for the catalog test. `main(argv)` is exported for create-mercury-agent.",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@clack/prompts": "^1.8.1",
|
|
39
|
-
"@mercury-fw/core": "0.
|
|
39
|
+
"@mercury-fw/core": "0.28.0",
|
|
40
40
|
"commander": "^15.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@mercury-fw/channel-google-chat": "0.1.
|
|
43
|
+
"@mercury-fw/channel-google-chat": "0.1.1",
|
|
44
44
|
"@mercury-fw/channel-http": "0.1.0",
|
|
45
|
-
"@mercury-fw/formatter": "0.
|
|
45
|
+
"@mercury-fw/formatter": "0.28.0",
|
|
46
46
|
"@mercury-fw/plugin-atlassian-admin": "0.1.0",
|
|
47
47
|
"@mercury-fw/plugin-bitbucket": "0.1.0",
|
|
48
48
|
"@mercury-fw/plugin-jira": "0.1.0",
|
package/src/app/commands.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync } from "node:fs";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join, resolve } from "node:path";
|
|
11
11
|
import { CATALOG, type CliCredentials } from "../catalog.ts";
|
|
12
|
-
import { packCredentials, setEnvVar } from "./credentials.ts";
|
|
12
|
+
import { packCredentials, readServiceAccountKey, setEnvVar } from "./credentials.ts";
|
|
13
13
|
import type { App } from "./find-app.ts";
|
|
14
14
|
|
|
15
15
|
export type AppDeps = {
|
|
@@ -27,6 +27,9 @@ export type AppDeps = {
|
|
|
27
27
|
|
|
28
28
|
const COMPOSE = ["docker", "compose"];
|
|
29
29
|
|
|
30
|
+
/** The Google Chat channel's package, which `google-chat` commands require. */
|
|
31
|
+
const GOOGLE_CHAT_PACKAGE = "@mercury-fw/channel-google-chat";
|
|
32
|
+
|
|
30
33
|
/** The app's service, the one the image builds. */
|
|
31
34
|
const SERVICE = "mercury";
|
|
32
35
|
|
|
@@ -143,6 +146,34 @@ export function appCommands(app: App, deps: AppDeps) {
|
|
|
143
146
|
}
|
|
144
147
|
return stopped(SERVICE, [[...COMPOSE, "run", "--rm", "--no-deps", "-T", SERVICE, "rm", "-rf", `/home/mercury/.config/${folder}`]]);
|
|
145
148
|
},
|
|
149
|
+
/** Writes the Google Chat channel's service account key (the JSON file
|
|
150
|
+
* at `keyFile`) into the app's env file, and the Pub/Sub subscription when
|
|
151
|
+
* given. Everything is checked before anything is written; the key is
|
|
152
|
+
* never printed. */
|
|
153
|
+
googleChatSetKey: async (keyFile: string, { subscription }: { subscription?: string }) => {
|
|
154
|
+
const manifest = JSON.parse(readFileSync(join(app.dir, "package.json"), "utf-8")) as {
|
|
155
|
+
dependencies?: Record<string, string>;
|
|
156
|
+
};
|
|
157
|
+
if (manifest.dependencies?.[GOOGLE_CHAT_PACKAGE] === undefined) {
|
|
158
|
+
throw new Error(`${app.name} doesn't have the Google Chat channel (${GOOGLE_CHAT_PACKAGE}) among its dependencies.`);
|
|
159
|
+
}
|
|
160
|
+
if (subscription !== undefined && !/^projects\/[^/]+\/subscriptions\/[^/]+$/.test(subscription)) {
|
|
161
|
+
throw new Error(`--subscription takes projects/<project>/subscriptions/<name> (got "${subscription}").`);
|
|
162
|
+
}
|
|
163
|
+
const source = resolve(keyFile);
|
|
164
|
+
const { clientEmail, privateKey } = readServiceAccountKey(source);
|
|
165
|
+
const envFile = join(app.dir, ".env");
|
|
166
|
+
setEnvVar(envFile, "GOOGLE_CHAT_APP_CLIENT_EMAIL", clientEmail);
|
|
167
|
+
setEnvVar(envFile, "GOOGLE_CHAT_APP_PRIVATE_KEY", privateKey);
|
|
168
|
+
if (subscription !== undefined) setEnvVar(envFile, "GOOGLE_CHAT_PUBSUB_SUBSCRIPTION", subscription);
|
|
169
|
+
const written =
|
|
170
|
+
subscription === undefined
|
|
171
|
+
? "GOOGLE_CHAT_APP_CLIENT_EMAIL and GOOGLE_CHAT_APP_PRIVATE_KEY"
|
|
172
|
+
: "GOOGLE_CHAT_APP_CLIENT_EMAIL, GOOGLE_CHAT_APP_PRIVATE_KEY and GOOGLE_CHAT_PUBSUB_SUBSCRIPTION";
|
|
173
|
+
deps.print(`${written} set in ${envFile}, from ${source}.`);
|
|
174
|
+
deps.print(`Delete ${source} now, the env file holds the key. bunx mfw start applies it to a running app.`);
|
|
175
|
+
return 0;
|
|
176
|
+
},
|
|
146
177
|
};
|
|
147
178
|
|
|
148
179
|
/** Stops `service`, runs `steps`, starts it again; stops at the first
|
package/src/app/credentials.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The two halves of `mfw credentials set`: packing a CLI's config folder into
|
|
3
3
|
* its credentials variable (a base64 tar.gz with the folder at its root, what
|
|
4
4
|
* the app's `docker-entrypoint.sh` unpacks into `~/.config` on the volume), and
|
|
5
|
-
* writing that variable into the app's env file.
|
|
5
|
+
* writing that variable into the app's env file. Also reading a service
|
|
6
|
+
* account key file, for `mfw google-chat set-key`.
|
|
6
7
|
*/
|
|
7
8
|
import {
|
|
8
9
|
chmodSync,
|
|
@@ -80,3 +81,23 @@ export function setEnvVar(file: string, name: string, value: string): void {
|
|
|
80
81
|
chmodSync(temporary, mode);
|
|
81
82
|
renameSync(temporary, file);
|
|
82
83
|
}
|
|
84
|
+
|
|
85
|
+
/** The two values of a Google Cloud service account key file (the JSON
|
|
86
|
+
* `gcloud iam service-accounts keys create` writes) that an app needs: its
|
|
87
|
+
* email, and its private key on one line with literal `\n`, the form an env
|
|
88
|
+
* file holds and the Google Chat channel unescapes. Throws naming `file` when
|
|
89
|
+
* it's missing or isn't such a key. */
|
|
90
|
+
export function readServiceAccountKey(file: string): { clientEmail: string; privateKey: string } {
|
|
91
|
+
const invalid = () => new Error(`${file} isn't a service account key: it needs "type": "service_account", "client_email" and "private_key".`);
|
|
92
|
+
let key: { type?: unknown; client_email?: unknown; private_key?: unknown };
|
|
93
|
+
try {
|
|
94
|
+
key = JSON.parse(readFileSync(file, "utf-8"));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") throw new Error(`${file} doesn't exist.`);
|
|
97
|
+
throw invalid();
|
|
98
|
+
}
|
|
99
|
+
if (key?.type !== "service_account" || typeof key.client_email !== "string" || typeof key.private_key !== "string") {
|
|
100
|
+
throw invalid();
|
|
101
|
+
}
|
|
102
|
+
return { clientEmail: key.client_email, privateKey: key.private_key.replace(/\n/g, "\\n") };
|
|
103
|
+
}
|
package/src/program.ts
CHANGED
|
@@ -228,6 +228,30 @@ Examples:
|
|
|
228
228
|
.argument("<plugin>", "a tool plugin of the app")
|
|
229
229
|
.action(async (plugin: string) => inApp((app) => app.credentialsReset(plugin))());
|
|
230
230
|
|
|
231
|
+
const googleChat = program
|
|
232
|
+
.command("google-chat")
|
|
233
|
+
.summary("the Google Chat channel's setup")
|
|
234
|
+
.description("Sets up the Google Chat channel of an app that has it.")
|
|
235
|
+
.helpCommand(false)
|
|
236
|
+
.addHelpText("after", INSIDE_AN_APP);
|
|
237
|
+
googleChat
|
|
238
|
+
.command("set-key")
|
|
239
|
+
.summary("writes the Chat app's key into the env file")
|
|
240
|
+
.description(
|
|
241
|
+
"Reads the service account key file gcloud iam service-accounts keys create writes, and sets GOOGLE_CHAT_APP_CLIENT_EMAIL and GOOGLE_CHAT_APP_PRIVATE_KEY in the app's env file (the key on one line), plus GOOGLE_CHAT_PUBSUB_SUBSCRIPTION with --subscription. Older values are replaced, the key is never printed. Delete the key file afterwards.",
|
|
242
|
+
)
|
|
243
|
+
.argument("<key-file>", "the service account's JSON key")
|
|
244
|
+
.option("--subscription <name>", "projects/<project>/subscriptions/<name>, the subscription the Chat app's events arrive on")
|
|
245
|
+
.addHelpText(
|
|
246
|
+
"after",
|
|
247
|
+
"\nExample:\n mfw google-chat set-key key.json --subscription projects/my-project/subscriptions/mercury-chat-sub",
|
|
248
|
+
)
|
|
249
|
+
.action(async (keyFile: string, opts: { subscription?: string }) =>
|
|
250
|
+
inApp((app) =>
|
|
251
|
+
app.googleChatSetKey(keyFile, opts.subscription === undefined ? {} : { subscription: opts.subscription }),
|
|
252
|
+
)(),
|
|
253
|
+
);
|
|
254
|
+
|
|
231
255
|
return program;
|
|
232
256
|
}
|
|
233
257
|
|