@cotal-ai/connector-hermes 0.1.0 → 0.1.1
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/bin/install.mjs +146 -35
- package/package.json +1 -1
package/bin/install.mjs
CHANGED
|
@@ -2,44 +2,143 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `npx @cotal-ai/connector-hermes install`
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* Copies this package's `plugin/cotal` (
|
|
7
|
-
* `<HERMES_HOME>/plugins/cotal
|
|
5
|
+
* Guided install of the Cotal plugin into an existing Hermes so its gateway can join a Cotal mesh.
|
|
6
|
+
* Copies this package's `plugin/cotal` (incl. the bundled `_sidecar/standalone.cjs`) into
|
|
7
|
+
* `<HERMES_HOME>/plugins/cotal`, enables it, optionally writes your mesh config to
|
|
8
|
+
* `<HERMES_HOME>/.env`, and checks the mesh is reachable.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Interactive when run in a TTY; non-interactive with `--yes` (or no TTY), taking config from
|
|
11
|
+
* `--link/--space/--name/--server` flags or the matching `COTAL_*` env vars.
|
|
12
|
+
*
|
|
13
|
+
* House rule: no fallbacks. If Hermes isn't installed we fail loudly and write NOTHING.
|
|
12
14
|
*/
|
|
13
15
|
import { execFileSync } from "node:child_process";
|
|
14
|
-
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
16
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { createConnection } from "node:net";
|
|
15
18
|
import { homedir } from "node:os";
|
|
16
19
|
import { join, sep } from "node:path";
|
|
20
|
+
import { createInterface } from "node:readline/promises";
|
|
17
21
|
import { fileURLToPath } from "node:url";
|
|
18
22
|
|
|
19
23
|
const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
20
24
|
const PLUGIN_SRC = join(PKG_ROOT, "plugin", "cotal");
|
|
21
25
|
const SIDECAR = join(PLUGIN_SRC, "_sidecar", "standalone.cjs");
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
const DEFAULTS = { space: "demo", name: "my-hermes", server: "nats://127.0.0.1:4222" };
|
|
28
|
+
|
|
29
|
+
const die = (msg) => {
|
|
24
30
|
process.stderr.write(`✗ ${msg}\n`);
|
|
25
31
|
process.exit(1);
|
|
32
|
+
};
|
|
33
|
+
const info = (msg) => process.stdout.write(`${msg}\n`);
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
info("usage: npx @cotal-ai/connector-hermes install [--yes] [--link <url> | --space <s> --name <n> --server <url>]");
|
|
26
37
|
}
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
|
|
39
|
+
// ---- arg parsing ------------------------------------------------------------
|
|
40
|
+
function parseFlags(argv) {
|
|
41
|
+
const f = {};
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a === "--yes" || a === "-y") f.yes = true;
|
|
45
|
+
else if (a.startsWith("--")) {
|
|
46
|
+
const [k, inline] = a.slice(2).split("=");
|
|
47
|
+
f[k] = inline ?? (argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return f;
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
// ---- mesh config gathering --------------------------------------------------
|
|
54
|
+
async function promptConfig() {
|
|
55
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
try {
|
|
57
|
+
info("\nCotal mesh — paste a join link (cotal://token@host/space), or press Enter to set it manually:");
|
|
58
|
+
const link = (await rl.question(" link: ")).trim();
|
|
59
|
+
if (link) {
|
|
60
|
+
const name = (await rl.question(` name [${DEFAULTS.name}]: `)).trim() || DEFAULTS.name;
|
|
61
|
+
return { COTAL_LINK: link, COTAL_NAME: name };
|
|
62
|
+
}
|
|
63
|
+
const space = (await rl.question(` space [${DEFAULTS.space}]: `)).trim() || DEFAULTS.space;
|
|
64
|
+
const name = (await rl.question(` name [${DEFAULTS.name}]: `)).trim() || DEFAULTS.name;
|
|
65
|
+
const server = (await rl.question(` server [${DEFAULTS.server}]: `)).trim() || DEFAULTS.server;
|
|
66
|
+
return { COTAL_SPACE: space, COTAL_NAME: name, COTAL_SERVERS: server };
|
|
67
|
+
} finally {
|
|
68
|
+
rl.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function configFromFlags(flags) {
|
|
73
|
+
const link = flags.link || process.env.COTAL_LINK;
|
|
74
|
+
if (link) return { COTAL_LINK: link, COTAL_NAME: flags.name || process.env.COTAL_NAME || DEFAULTS.name };
|
|
75
|
+
const space = flags.space || process.env.COTAL_SPACE;
|
|
76
|
+
const server = flags.server || process.env.COTAL_SERVERS;
|
|
77
|
+
if (!space && !server) return {}; // nothing supplied → skip .env, print manual next steps
|
|
78
|
+
return {
|
|
79
|
+
COTAL_SPACE: space || DEFAULTS.space,
|
|
80
|
+
COTAL_NAME: flags.name || process.env.COTAL_NAME || DEFAULTS.name,
|
|
81
|
+
COTAL_SERVERS: server || DEFAULTS.server,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- ~/.hermes/.env editing (idempotent) ------------------------------------
|
|
86
|
+
function writeEnv(file, set) {
|
|
87
|
+
// Link mode and manual mode are mutually exclusive; comment out the other mode's keys so a
|
|
88
|
+
// re-run never leaves a stale COTAL_SPACE overriding a new COTAL_LINK (individual vars win in
|
|
89
|
+
// configFromEnv).
|
|
90
|
+
const drop = set.COTAL_LINK ? ["COTAL_SPACE", "COTAL_SERVERS"] : ["COTAL_LINK"];
|
|
91
|
+
const lines = existsSync(file) ? readFileSync(file, "utf8").split("\n") : [];
|
|
92
|
+
const keyOf = (l) => l.replace(/^#\s*/, "").split("=")[0].trim();
|
|
93
|
+
for (const [k, v] of Object.entries(set)) {
|
|
94
|
+
const line = `${k}=${v}`;
|
|
95
|
+
const i = lines.findIndex((l) => keyOf(l) === k);
|
|
96
|
+
if (i >= 0) lines[i] = line;
|
|
97
|
+
else lines.push(line);
|
|
98
|
+
}
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
if (drop.includes(keyOf(lines[i])) && !lines[i].trimStart().startsWith("#")) lines[i] = `# ${lines[i]}`;
|
|
101
|
+
}
|
|
102
|
+
writeFileSync(file, lines.join("\n").replace(/\n*$/, "\n"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- mesh reachability ------------------------------------------------------
|
|
106
|
+
function hostPort(server) {
|
|
107
|
+
try {
|
|
108
|
+
const u = new URL(server.replace(/^(cotals?|nats):\/\//, "http://"));
|
|
109
|
+
return { host: u.hostname, port: Number(u.port || 4222) };
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function reachable(server) {
|
|
115
|
+
const hp = hostPort(server);
|
|
116
|
+
if (!hp) return Promise.resolve(false);
|
|
117
|
+
return new Promise((res) => {
|
|
118
|
+
const s = createConnection({ host: hp.host, port: hp.port });
|
|
119
|
+
const done = (ok) => {
|
|
120
|
+
try {
|
|
121
|
+
s.destroy();
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
res(ok);
|
|
126
|
+
};
|
|
127
|
+
s.setTimeout(2000);
|
|
128
|
+
s.once("connect", () => done(true));
|
|
129
|
+
s.once("timeout", () => done(false));
|
|
130
|
+
s.once("error", () => done(false));
|
|
131
|
+
});
|
|
34
132
|
}
|
|
35
133
|
|
|
36
|
-
|
|
37
|
-
|
|
134
|
+
// ---- main -------------------------------------------------------------------
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
if (args[0] !== "install") {
|
|
38
137
|
usage();
|
|
39
|
-
process.exit(
|
|
138
|
+
process.exit(args[0] ? 1 : 0);
|
|
40
139
|
}
|
|
140
|
+
const flags = parseFlags(args.slice(1));
|
|
41
141
|
|
|
42
|
-
// 1. Require a working Hermes — refuse (writing nothing) if absent.
|
|
43
142
|
function hermesVersion() {
|
|
44
143
|
try {
|
|
45
144
|
return execFileSync("hermes", ["--version"], { encoding: "utf8" }).trim();
|
|
@@ -47,6 +146,7 @@ function hermesVersion() {
|
|
|
47
146
|
return undefined;
|
|
48
147
|
}
|
|
49
148
|
}
|
|
149
|
+
|
|
50
150
|
const version = hermesVersion();
|
|
51
151
|
if (!version) {
|
|
52
152
|
die(
|
|
@@ -55,45 +155,56 @@ if (!version) {
|
|
|
55
155
|
" then re-run: npx @cotal-ai/connector-hermes install",
|
|
56
156
|
);
|
|
57
157
|
}
|
|
58
|
-
|
|
59
|
-
// 2. The bundled sidecar must be present (it ships in the npm package; a raw source checkout
|
|
60
|
-
// needs `pnpm --filter @cotal-ai/connector-hermes build` first). No silent half-install.
|
|
61
158
|
if (!existsSync(SIDECAR)) {
|
|
62
159
|
die(
|
|
63
160
|
`bundled sidecar missing at ${SIDECAR}\n` +
|
|
64
161
|
" (running from source? build it first: pnpm --filter @cotal-ai/connector-hermes build)",
|
|
65
162
|
);
|
|
66
163
|
}
|
|
164
|
+
info(`✓ Hermes detected — ${version.split("·")[0].trim()}`);
|
|
67
165
|
|
|
68
|
-
// 3. Resolve the Hermes home honestly: HERMES_HOME wins, else ~/.hermes.
|
|
69
166
|
const hermesHome = process.env.HERMES_HOME?.trim() || join(homedir(), ".hermes");
|
|
167
|
+
const interactive = Boolean(process.stdin.isTTY) && !flags.yes;
|
|
168
|
+
const meshEnv = interactive ? await promptConfig() : configFromFlags(flags);
|
|
169
|
+
|
|
170
|
+
// Write mesh config to <home>/.env (if any was gathered).
|
|
171
|
+
if (Object.keys(meshEnv).length) {
|
|
172
|
+
const envFile = join(hermesHome, ".env");
|
|
173
|
+
mkdirSync(hermesHome, { recursive: true });
|
|
174
|
+
writeEnv(envFile, meshEnv);
|
|
175
|
+
info(`✓ Wrote ${Object.keys(meshEnv).join(", ")} to ${envFile}`);
|
|
176
|
+
|
|
177
|
+
// Best-effort reachability check.
|
|
178
|
+
const server = meshEnv.COTAL_SERVERS || meshEnv.COTAL_LINK;
|
|
179
|
+
if (server) {
|
|
180
|
+
const ok = await reachable(server);
|
|
181
|
+
info(ok ? `✓ Mesh reachable` : `✗ Mesh not reachable at ${hostPort(server)?.host}:${hostPort(server)?.port} — start it (e.g. \`nats-server -js\`) before \`hermes gateway run\``);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Copy the plugin + enable it.
|
|
70
186
|
const pluginsDir = join(hermesHome, "plugins");
|
|
71
187
|
const dest = join(pluginsDir, "cotal");
|
|
72
|
-
|
|
73
|
-
info(`Hermes: ${version}`);
|
|
74
|
-
info(`Installing the Cotal plugin into ${dest} ...`);
|
|
75
188
|
mkdirSync(pluginsDir, { recursive: true });
|
|
76
|
-
rmSync(dest, { recursive: true, force: true });
|
|
189
|
+
rmSync(dest, { recursive: true, force: true });
|
|
77
190
|
cpSync(PLUGIN_SRC, dest, {
|
|
78
191
|
recursive: true,
|
|
79
192
|
filter: (src) => !src.includes(`${sep}__pycache__`) && !src.endsWith(".pyc"),
|
|
80
193
|
});
|
|
81
|
-
|
|
82
|
-
// 4. Enable it via Hermes' own command (it's discovered now that it's in plugins/).
|
|
83
194
|
try {
|
|
84
195
|
execFileSync("hermes", ["plugins", "enable", "cotal"], { stdio: "inherit" });
|
|
85
196
|
} catch {
|
|
86
197
|
die(
|
|
87
198
|
"copied the plugin but `hermes plugins enable cotal` failed.\n" +
|
|
88
|
-
" Enable it manually: `hermes plugins enable cotal
|
|
199
|
+
" Enable it manually: `hermes plugins enable cotal`.",
|
|
89
200
|
);
|
|
90
201
|
}
|
|
91
202
|
|
|
92
203
|
info("\n✓ Cotal plugin installed + enabled.\n");
|
|
93
|
-
|
|
94
|
-
info("
|
|
95
|
-
info("
|
|
96
|
-
|
|
97
|
-
info("
|
|
98
|
-
info("");
|
|
99
|
-
|
|
204
|
+
if (Object.keys(meshEnv).length) {
|
|
205
|
+
info("Run it on the mesh:");
|
|
206
|
+
info(" hermes gateway run");
|
|
207
|
+
} else {
|
|
208
|
+
info("Next: set your mesh in ~/.hermes/.env (COTAL_LINK, or COTAL_SPACE/COTAL_NAME/COTAL_SERVERS),");
|
|
209
|
+
info(" then: hermes gateway run");
|
|
210
|
+
}
|