@mercury-fw/cli 0.27.0 → 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 CHANGED
@@ -1,5 +1,22 @@
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
+
13
+ ## 0.27.1
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [2c0e020]
18
+ - @mercury-fw/core@0.27.1
19
+
3
20
  ## 0.27.0
4
21
 
5
22
  ### Minor 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.27.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.27.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.0",
43
+ "@mercury-fw/channel-google-chat": "0.1.1",
44
44
  "@mercury-fw/channel-http": "0.1.0",
45
- "@mercury-fw/formatter": "0.27.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",
@@ -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
@@ -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