@goodnesshq/opencode-notification 0.1.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.
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile, mkdir, copyFile, stat } from "node:fs/promises";
3
+ import { join, basename, dirname } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const REPO_CONFIG_PATH = ".opencode/oc-notify.json";
9
+ const GLOBAL_CONFIG_PATH = join(
10
+ homedir(),
11
+ ".config",
12
+ "opencode",
13
+ "oc-notify.json",
14
+ );
15
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
16
+ const SCRIPT_DIR = dirname(SCRIPT_PATH);
17
+ const SCHEMA_SOURCE_PATH = join(SCRIPT_DIR, "oc-notify.schema.json");
18
+
19
+ const DEFAULTS = {
20
+ enabled: true,
21
+ title: "",
22
+ tier: "standard",
23
+ detailLevel: "full",
24
+ responseComplete: {
25
+ enabled: true,
26
+ trigger: "message.updated",
27
+ },
28
+ channels: {
29
+ mac: {
30
+ enabled: true,
31
+ method: "auto",
32
+ },
33
+ ntfy: {
34
+ enabled: true,
35
+ server: "https://ntfy.sh",
36
+ topic: "oc-goodness-attn-1407537",
37
+ },
38
+ },
39
+ overrides: {
40
+ includeGroups: [],
41
+ excludeGroups: [],
42
+ },
43
+ };
44
+
45
+ const GROUPS_BASIC = [
46
+ "action_required",
47
+ "failures",
48
+ "change_summary",
49
+ "session_lifecycle",
50
+ "responses",
51
+ "vcs_worktree",
52
+ "todos",
53
+ ];
54
+
55
+ const GROUPS_ADVANCED = [
56
+ "files",
57
+ "pty",
58
+ "commands_tools",
59
+ "message_lifecycle",
60
+ "message_parts",
61
+ "system",
62
+ "lsp",
63
+ "tui",
64
+ ];
65
+
66
+ async function commandExists(command) {
67
+ try {
68
+ const { execSync } = await import("node:child_process");
69
+ execSync(`command -v ${command}`, { stdio: "ignore" });
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function yesNo(value, fallback) {
77
+ if (!value) return fallback;
78
+ const normalized = value.trim().toLowerCase();
79
+ if (normalized === "y" || normalized === "yes") return true;
80
+ if (normalized === "n" || normalized === "no") return false;
81
+ return fallback;
82
+ }
83
+
84
+ function parseList(input, allowed) {
85
+ if (!input) return [];
86
+ const items = input
87
+ .split(",")
88
+ .map((item) => item.trim())
89
+ .filter(Boolean);
90
+ if (!allowed) return items;
91
+ return items.filter((item) => allowed.includes(item));
92
+ }
93
+
94
+ async function readJson(path) {
95
+ try {
96
+ const content = await readFile(path, "utf8");
97
+ return JSON.parse(content);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ async function writeJson(path, data) {
104
+ const content = JSON.stringify(data, null, 2) + "\n";
105
+ await writeFile(path, content, "utf8");
106
+ }
107
+
108
+ async function fileExists(path) {
109
+ try {
110
+ await stat(path);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ async function ensureOpencodeDir(cwd) {
118
+ await mkdir(join(cwd, ".opencode"), { recursive: true });
119
+ }
120
+
121
+ async function copySchema(cwd) {
122
+ try {
123
+ await copyFile(
124
+ SCHEMA_SOURCE_PATH,
125
+ join(cwd, ".opencode", "oc-notify.schema.json"),
126
+ );
127
+ } catch {}
128
+ }
129
+
130
+
131
+ async function sendTestNotification(config, repoName) {
132
+ const title = config.title || `${repoName}@test`;
133
+ const subtitle = "OpenCode Notify";
134
+ const message = "Test notification";
135
+ const useTerminalNotifier =
136
+ config.channels.mac.enabled && (await commandExists("terminal-notifier"));
137
+
138
+ if (config.channels.mac.enabled) {
139
+ try {
140
+ const { execFileSync } = await import("node:child_process");
141
+ if (useTerminalNotifier && config.channels.mac.method !== "applescript") {
142
+ execFileSync("terminal-notifier", [
143
+ "-title",
144
+ title,
145
+ "-message",
146
+ message,
147
+ "-subtitle",
148
+ subtitle,
149
+ ]);
150
+ } else {
151
+ const script = [
152
+ "on run argv",
153
+ "set theMessage to item 1 of argv",
154
+ "set theTitle to item 2 of argv",
155
+ "set theSubtitle to item 3 of argv",
156
+ 'if theSubtitle is "" then',
157
+ " display notification theMessage with title theTitle",
158
+ "else",
159
+ " display notification theMessage with title theTitle subtitle theSubtitle",
160
+ "end if",
161
+ "end run",
162
+ ].join("\n");
163
+ execFileSync("osascript", ["-e", script, message, title, subtitle]);
164
+ }
165
+ } catch {
166
+ console.log("Test macOS notification failed.");
167
+ }
168
+ }
169
+
170
+ if (config.channels.ntfy.enabled) {
171
+ try {
172
+ const { execFileSync } = await import("node:child_process");
173
+ const server = config.channels.ntfy.server.replace(/\/$/, "");
174
+ const url = `${server}/${config.channels.ntfy.topic}`;
175
+ execFileSync("curl", [
176
+ "-sS",
177
+ "-H",
178
+ `Title: ${title}`,
179
+ "-H",
180
+ "Priority: 3",
181
+ "-H",
182
+ "Tags: opencode,info",
183
+ "-d",
184
+ message,
185
+ url,
186
+ ]);
187
+ } catch {
188
+ console.log("Test ntfy notification failed.");
189
+ }
190
+ }
191
+ }
192
+
193
+ async function main() {
194
+ const cwd = process.cwd();
195
+ const repoName = basename(cwd);
196
+ const args = process.argv.slice(2);
197
+
198
+ if (args.includes("--install")) {
199
+ await ensureOpencodeDir(cwd);
200
+ await copyFile(SCRIPT_PATH, join(cwd, ".opencode", "notify-init.mjs"));
201
+ await copySchema(cwd);
202
+ console.log("Installed .opencode/notify-init.mjs and schema in this repo.");
203
+ return;
204
+ }
205
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
206
+
207
+ console.log("OpenCode Notify — per-repo setup\n");
208
+
209
+ const enableInput = await rl.question(
210
+ "Enable notifications for this repo? (Y/n) [Y] ",
211
+ );
212
+ const enabled = yesNo(enableInput, true);
213
+ if (!enabled) {
214
+ await rl.close();
215
+ return;
216
+ }
217
+
218
+ const titleInput = await rl.question(
219
+ `Notification title (blank = "${repoName}@<branch>"): `,
220
+ );
221
+ const title = titleInput.trim();
222
+
223
+ console.log(
224
+ "\nSelect a tier:\n 1) Minimal\n 2) Standard\n 3) Verbose\n 4) Custom",
225
+ );
226
+ const tierInput = await rl.question("> [2] ");
227
+ const tier =
228
+ tierInput.trim() === "1"
229
+ ? "minimal"
230
+ : tierInput.trim() === "3"
231
+ ? "verbose"
232
+ : tierInput.trim() === "4"
233
+ ? "custom"
234
+ : "standard";
235
+
236
+ console.log(
237
+ "\nResponse-complete trigger:\n 1) session.idle (coarse, low noise)\n 2) message.updated (precise)\n 3) message.part.updated (very precise, more noise)",
238
+ );
239
+ const triggerInput = await rl.question("> [2] ");
240
+ const trigger =
241
+ triggerInput.trim() === "1"
242
+ ? "session.idle"
243
+ : triggerInput.trim() === "3"
244
+ ? "message.part.updated"
245
+ : "message.updated";
246
+
247
+ const macInput = await rl.question(
248
+ "\nEnable macOS Notification Center? (Y/n) [Y] ",
249
+ );
250
+ const macEnabled = yesNo(macInput, true);
251
+
252
+ const ntfyInput = await rl.question("Enable ntfy notifications? (Y/n) [Y] ");
253
+ const ntfyEnabled = yesNo(ntfyInput, true);
254
+
255
+ let globalConfig = await readJson(GLOBAL_CONFIG_PATH);
256
+ if (!globalConfig) globalConfig = {};
257
+ if (!globalConfig.ntfy) globalConfig.ntfy = {};
258
+
259
+ if (ntfyEnabled) {
260
+ const serverInput = await rl.question(
261
+ `ntfy server (blank = ${globalConfig.ntfy.server || DEFAULTS.channels.ntfy.server}): `,
262
+ );
263
+ const topicInput = await rl.question(
264
+ `ntfy topic (blank = ${globalConfig.ntfy.topic || DEFAULTS.channels.ntfy.topic}): `,
265
+ );
266
+ if (serverInput.trim()) globalConfig.ntfy.server = serverInput.trim();
267
+ if (topicInput.trim()) globalConfig.ntfy.topic = topicInput.trim();
268
+ }
269
+
270
+ const detailInput = await rl.question(
271
+ "Detail level (1=full, 2=title-only) [1] ",
272
+ );
273
+ const detailLevel = detailInput.trim() === "2" ? "title-only" : "full";
274
+
275
+ const adjustGroups = yesNo(
276
+ await rl.question("\nAdjust event groups? (y/N) [N] "),
277
+ false,
278
+ );
279
+ let includeGroups = [];
280
+ let excludeGroups = [];
281
+ if (adjustGroups) {
282
+ const advanced = yesNo(
283
+ await rl.question("Show advanced groups? (y/N) [N] "),
284
+ false,
285
+ );
286
+ const allowed = advanced
287
+ ? GROUPS_BASIC.concat(GROUPS_ADVANCED)
288
+ : GROUPS_BASIC;
289
+ console.log(`Available groups: ${allowed.join(", ")}`);
290
+ const includeInput = await rl.question(
291
+ "Include groups (comma-separated, blank to skip): ",
292
+ );
293
+ const excludeInput = await rl.question(
294
+ "Exclude groups (comma-separated, blank to skip): ",
295
+ );
296
+ includeGroups = parseList(includeInput, allowed);
297
+ excludeGroups = parseList(excludeInput, allowed);
298
+ }
299
+
300
+ const installTN = yesNo(
301
+ await rl.question("\nUse terminal-notifier if available? (Y/n) [Y] "),
302
+ true,
303
+ );
304
+
305
+ if (installTN) {
306
+ const hasTN = await commandExists("terminal-notifier");
307
+ const hasBrew = await commandExists("brew");
308
+ if (!hasTN && hasBrew) {
309
+ const installNow = yesNo(
310
+ await rl.question(
311
+ "terminal-notifier not found. Install with Homebrew now? (y/N) [N] ",
312
+ ),
313
+ false,
314
+ );
315
+ if (installNow) {
316
+ try {
317
+ const { execSync } = await import("node:child_process");
318
+ execSync("brew install terminal-notifier", { stdio: "inherit" });
319
+ } catch {
320
+ console.log("Homebrew install failed. Falling back to AppleScript.");
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ const config = {
327
+ version: 1,
328
+ enabled: true,
329
+ title,
330
+ tier,
331
+ detailLevel,
332
+ responseComplete: {
333
+ enabled: true,
334
+ trigger,
335
+ },
336
+ channels: {
337
+ mac: {
338
+ enabled: macEnabled,
339
+ method: installTN ? "auto" : "applescript",
340
+ },
341
+ ntfy: {
342
+ enabled: ntfyEnabled,
343
+ server: globalConfig.ntfy?.server || DEFAULTS.channels.ntfy.server,
344
+ topic: globalConfig.ntfy?.topic || DEFAULTS.channels.ntfy.topic,
345
+ },
346
+ },
347
+ overrides: {
348
+ includeGroups,
349
+ excludeGroups,
350
+ },
351
+ };
352
+
353
+ const writeInput = await rl.question(
354
+ "\nWrite config to .opencode/oc-notify.json? (Y/n) [Y] ",
355
+ );
356
+ const shouldWrite = yesNo(writeInput, true);
357
+ if (shouldWrite) {
358
+ await ensureOpencodeDir(cwd);
359
+ await writeJson(join(cwd, REPO_CONFIG_PATH), config);
360
+ await copySchema(cwd);
361
+ }
362
+
363
+ const testInput = await rl.question(
364
+ "Send a test notification now? (Y/n) [Y] ",
365
+ );
366
+ const sendTest = yesNo(testInput, true);
367
+ if (sendTest) {
368
+ await sendTestNotification(config, repoName);
369
+ }
370
+
371
+ await rl.close();
372
+ }
373
+
374
+ await main();
@@ -0,0 +1,101 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "OpenCode Notifications Repo Config",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "version": {
8
+ "type": "integer",
9
+ "minimum": 1
10
+ },
11
+ "enabled": {
12
+ "type": "boolean"
13
+ },
14
+ "title": {
15
+ "type": "string"
16
+ },
17
+ "tier": {
18
+ "type": "string",
19
+ "enum": ["minimal", "standard", "verbose", "custom"]
20
+ },
21
+ "detailLevel": {
22
+ "type": "string",
23
+ "enum": ["full", "title-only"]
24
+ },
25
+ "responseComplete": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "properties": {
29
+ "enabled": {
30
+ "type": "boolean"
31
+ },
32
+ "trigger": {
33
+ "type": "string",
34
+ "enum": ["session.idle", "message.updated", "message.part.updated"]
35
+ }
36
+ }
37
+ },
38
+ "channels": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "properties": {
42
+ "mac": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "enabled": {
47
+ "type": "boolean"
48
+ },
49
+ "method": {
50
+ "type": "string",
51
+ "enum": ["auto", "applescript", "terminal-notifier"]
52
+ }
53
+ }
54
+ },
55
+ "ntfy": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "enabled": {
60
+ "type": "boolean"
61
+ },
62
+ "server": {
63
+ "type": "string"
64
+ },
65
+ "topic": {
66
+ "type": "string"
67
+ }
68
+ }
69
+ }
70
+ }
71
+ },
72
+ "overrides": {
73
+ "type": "object",
74
+ "additionalProperties": false,
75
+ "properties": {
76
+ "includeGroups": {
77
+ "type": "array",
78
+ "items": {
79
+ "type": "string"
80
+ }
81
+ },
82
+ "excludeGroups": {
83
+ "type": "array",
84
+ "items": {
85
+ "type": "string"
86
+ }
87
+ }
88
+ }
89
+ },
90
+ "dedupe": {
91
+ "type": "object",
92
+ "additionalProperties": false,
93
+ "properties": {
94
+ "ttlSeconds": {
95
+ "type": "number",
96
+ "minimum": 0
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # OpenCode Notifications
2
+
3
+ This repo implements a per-repo notification plugin for OpenCode. It uses a repo-local config file (`.opencode/oc-notify.json`) and sends notifications to macOS Notification Center and ntfy.
4
+
5
+ ## Quick Start
6
+
7
+ 1) Install the CLI (global) or use `npx`:
8
+
9
+ ```bash
10
+ npm install -g @goodnesshq/opencode-notification
11
+ ```
12
+
13
+ 2) Install the repo installer (once per repo):
14
+
15
+ ```bash
16
+ ocn install
17
+ ```
18
+
19
+ 3) Add the plugin to your global OpenCode config (use an absolute path; `~` is not expanded).
20
+
21
+ `~/.config/opencode/opencode.json`:
22
+
23
+ ```json
24
+ {
25
+ "$schema": "https://opencode.ai/config.json",
26
+ "plugin": [
27
+ "<path-from-ocn-plugin-path>"
28
+ ]
29
+ }
30
+ ```
31
+
32
+ 4) Run the repo installer:
33
+
34
+ ```bash
35
+ node .opencode/notify-init.mjs
36
+ ```
37
+
38
+ This writes `.opencode/oc-notify.json` with your repo-specific settings.
39
+
40
+ ### Update guidance
41
+
42
+ - One-off: `npx @goodnesshq/opencode-notification@latest install`
43
+ - Global: `npm update -g @goodnesshq/opencode-notification`
44
+
45
+ ## Troubleshooting
46
+
47
+ **OpenCode fails to start after enabling the plugin**
48
+
49
+ - Ensure every entry in `plugin` points to an existing file.
50
+ - Remove stale plugin paths from other repos.
51
+ - Use absolute paths (no `~`).
52
+
53
+ ## Config Schema
54
+
55
+ The config is defined in `.opencode/oc-notify.schema.json` and supports:
56
+
57
+ - `enabled`: master switch
58
+ - `title`: override or template (supports `{repo}` and `{branch}`)
59
+ - `tier`: `minimal` | `standard` | `verbose` | `custom`
60
+ - `detailLevel`: `full` | `title-only`
61
+ - `responseComplete`: trigger for completion notifications
62
+ - `channels`: `mac` and `ntfy` settings
63
+ - `overrides`: include/exclude groups
64
+ - `dedupe`: in-memory TTL settings
65
+
66
+ ## Tier Defaults
67
+
68
+ - **minimal**: `action_required`, `failures`
69
+ - **standard**: minimal + `change_summary`, `session_lifecycle`
70
+ - **verbose**: standard + `files`, `todos`, `vcs_worktree`, `pty`, `responses`
71
+
72
+ ## Example Configs
73
+
74
+ ### Minimal
75
+ ```json
76
+ {
77
+ "enabled": true,
78
+ "tier": "minimal",
79
+ "responseComplete": {
80
+ "enabled": true,
81
+ "trigger": "session.idle"
82
+ },
83
+ "channels": {
84
+ "mac": { "enabled": true, "method": "auto" },
85
+ "ntfy": { "enabled": true, "server": "https://ntfy.sh", "topic": "oc-goodness-attn-1407537" }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### Standard
91
+ ```json
92
+ {
93
+ "enabled": true,
94
+ "tier": "standard",
95
+ "detailLevel": "full",
96
+ "responseComplete": {
97
+ "enabled": true,
98
+ "trigger": "message.updated"
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Verbose
104
+ ```json
105
+ {
106
+ "enabled": true,
107
+ "tier": "verbose",
108
+ "responseComplete": {
109
+ "enabled": true,
110
+ "trigger": "message.part.updated"
111
+ },
112
+ "overrides": {
113
+ "includeGroups": ["commands_tools"],
114
+ "excludeGroups": []
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### Custom
120
+ ```json
121
+ {
122
+ "enabled": true,
123
+ "tier": "custom",
124
+ "overrides": {
125
+ "includeGroups": ["action_required", "failures", "change_summary"],
126
+ "excludeGroups": ["session_lifecycle"]
127
+ }
128
+ }
129
+ ```
package/bin/ocn.mjs ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import { copyFile, mkdir, stat } from "node:fs/promises";
3
+ import { basename, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
7
+ const ASSET_NOTIFY_INIT = join(PACKAGE_ROOT, ".opencode", "notify-init.mjs");
8
+ const ASSET_SCHEMA = join(PACKAGE_ROOT, ".opencode", "oc-notify.schema.json");
9
+ const PLUGIN_PATH = join(PACKAGE_ROOT, "plugins", "opencode-notifications.mjs");
10
+
11
+ function usage() {
12
+ console.log("Usage: ocn <command>\n\nCommands:\n install Copy installer assets into the current repo\n plugin-path Print absolute path to the plugin entry file\n");
13
+ }
14
+
15
+ async function ensureDir(path) {
16
+ await mkdir(path, { recursive: true });
17
+ }
18
+
19
+ async function fileExists(path) {
20
+ try {
21
+ await stat(path);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ async function install() {
29
+ const cwd = process.cwd();
30
+ const repoName = basename(cwd);
31
+ const opencodeDir = join(cwd, ".opencode");
32
+ await ensureDir(opencodeDir);
33
+
34
+ if (!(await fileExists(ASSET_NOTIFY_INIT)) || !(await fileExists(ASSET_SCHEMA))) {
35
+ console.error("ocn: installer assets missing from package");
36
+ process.exit(1);
37
+ }
38
+
39
+ await copyFile(ASSET_NOTIFY_INIT, join(opencodeDir, "notify-init.mjs"));
40
+ await copyFile(ASSET_SCHEMA, join(opencodeDir, "oc-notify.schema.json"));
41
+
42
+ console.log(`Installed OpenCode Notifications installer in ${repoName}.`);
43
+ console.log("Next steps:");
44
+ console.log(" 1) Run: node .opencode/notify-init.mjs");
45
+ console.log(" 2) Add the plugin path to ~/.config/opencode/opencode.json");
46
+ console.log(" ocn plugin-path");
47
+ console.log("\nUpdate guidance:");
48
+ console.log(" - One-off: npx @goodnesshq/opencode-notification@latest install");
49
+ console.log(" - Global: npm update -g @goodnesshq/opencode-notification");
50
+ }
51
+
52
+ async function pluginPath() {
53
+ if (!(await fileExists(PLUGIN_PATH))) {
54
+ console.error("ocn: plugin entry file missing from package");
55
+ process.exit(1);
56
+ }
57
+ console.log(PLUGIN_PATH);
58
+ }
59
+
60
+ async function main() {
61
+ const [command] = process.argv.slice(2);
62
+ if (!command || command === "-h" || command === "--help") {
63
+ usage();
64
+ return;
65
+ }
66
+
67
+ if (command === "install") {
68
+ await install();
69
+ return;
70
+ }
71
+
72
+ if (command === "plugin-path") {
73
+ await pluginPath();
74
+ return;
75
+ }
76
+
77
+ usage();
78
+ process.exit(1);
79
+ }
80
+
81
+ await main();