@bulolo/hermes-link 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.
- package/dist/chunk-C24HF73Y.js +1843 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +525 -0
- package/dist/http/app.d.ts +111 -0
- package/dist/http/app.js +6 -0
- package/package.json +69 -0
- package/scripts/check-node-version.mjs +70 -0
- package/scripts/postinstall.mjs +45 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DAEMON_LOG_FILE,
|
|
4
|
+
LINK_COMMAND,
|
|
5
|
+
LINK_VERSION,
|
|
6
|
+
bootstrapWithRelay,
|
|
7
|
+
createRotatingTextLogWriter,
|
|
8
|
+
currentCliScriptPath,
|
|
9
|
+
detectRuntimeEnvironment,
|
|
10
|
+
disableAutostart,
|
|
11
|
+
enableAutostart,
|
|
12
|
+
ensureIdentity,
|
|
13
|
+
generateAppConnectToken,
|
|
14
|
+
getAutostartStatus,
|
|
15
|
+
loadConfig,
|
|
16
|
+
loadIdentity,
|
|
17
|
+
normalizeLanHost,
|
|
18
|
+
readRecentGatewayLogEntries,
|
|
19
|
+
readRecentLogEntries,
|
|
20
|
+
resolveRuntimePaths,
|
|
21
|
+
saveAssignedLinkId,
|
|
22
|
+
saveConfig,
|
|
23
|
+
startLinkService
|
|
24
|
+
} from "../chunk-C24HF73Y.js";
|
|
25
|
+
|
|
26
|
+
// src/cli/index.ts
|
|
27
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
28
|
+
|
|
29
|
+
// src/daemon/process.ts
|
|
30
|
+
import { execFile, spawn } from "child_process";
|
|
31
|
+
import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
32
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
33
|
+
import path from "path";
|
|
34
|
+
import { promisify } from "util";
|
|
35
|
+
var execFileAsync = promisify(execFile);
|
|
36
|
+
function pidFilePath(paths) {
|
|
37
|
+
return path.join(paths.homeDir, "daemon.pid");
|
|
38
|
+
}
|
|
39
|
+
async function readPid(paths) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = await readFile(pidFilePath(paths), "utf8");
|
|
42
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
43
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function writePid(paths, pid) {
|
|
49
|
+
await mkdir(paths.homeDir, { recursive: true, mode: 448 });
|
|
50
|
+
await writeFile(pidFilePath(paths), String(pid), { mode: 384 });
|
|
51
|
+
}
|
|
52
|
+
async function clearPid(paths) {
|
|
53
|
+
await rm(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
54
|
+
}
|
|
55
|
+
function isProcessAlive(pid) {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function wait(ms) {
|
|
64
|
+
return sleep(ms);
|
|
65
|
+
}
|
|
66
|
+
async function getDaemonStatus(paths) {
|
|
67
|
+
const runtimePaths = paths ?? resolveRuntimePaths();
|
|
68
|
+
const pid = await readPid(runtimePaths);
|
|
69
|
+
if (pid === null) {
|
|
70
|
+
return { state: "stopped", pid: null };
|
|
71
|
+
}
|
|
72
|
+
if (isProcessAlive(pid)) {
|
|
73
|
+
return { state: "running", pid };
|
|
74
|
+
}
|
|
75
|
+
await clearPid(runtimePaths);
|
|
76
|
+
return { state: "stopped", pid: null };
|
|
77
|
+
}
|
|
78
|
+
async function probeLocalLinkService(options) {
|
|
79
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetcher(`http://127.0.0.1:${options.port}/api/v1/system/status`, {
|
|
82
|
+
signal: AbortSignal.timeout(3e3)
|
|
83
|
+
});
|
|
84
|
+
return { reachable: true, statusCode: response.status };
|
|
85
|
+
} catch {
|
|
86
|
+
return { reachable: false };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function startDaemonProcess(options) {
|
|
90
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
91
|
+
const status = await getDaemonStatus(paths);
|
|
92
|
+
if (status.state === "running") return;
|
|
93
|
+
const child = spawn(
|
|
94
|
+
process.execPath,
|
|
95
|
+
[currentCliScriptPath(), "daemon-supervisor"],
|
|
96
|
+
{
|
|
97
|
+
detached: true,
|
|
98
|
+
stdio: "ignore"
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
child.unref();
|
|
102
|
+
for (let i = 0; i < 6; i++) {
|
|
103
|
+
await wait(500);
|
|
104
|
+
const newStatus = await getDaemonStatus(paths);
|
|
105
|
+
if (newStatus.state === "running") return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function stopDaemonProcess(options) {
|
|
109
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
110
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
111
|
+
const status = await getDaemonStatus(paths);
|
|
112
|
+
if (status.state !== "running" || status.pid === null) return;
|
|
113
|
+
try {
|
|
114
|
+
process.kill(status.pid, "SIGTERM");
|
|
115
|
+
} catch {
|
|
116
|
+
await clearPid(paths);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const deadline = Date.now() + timeoutMs;
|
|
120
|
+
while (Date.now() < deadline) {
|
|
121
|
+
await wait(200);
|
|
122
|
+
if (!isProcessAlive(status.pid)) {
|
|
123
|
+
await clearPid(paths);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
process.kill(status.pid, "SIGKILL");
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
await clearPid(paths);
|
|
132
|
+
}
|
|
133
|
+
async function runDaemonSupervisor(options) {
|
|
134
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
135
|
+
await mkdir(paths.homeDir, { recursive: true, mode: 448 });
|
|
136
|
+
await writePid(paths, process.pid);
|
|
137
|
+
const logWriter = createRotatingTextLogWriter({ paths, fileName: DAEMON_LOG_FILE });
|
|
138
|
+
const cleanup = async () => {
|
|
139
|
+
await clearPid(paths).catch(() => void 0);
|
|
140
|
+
await logWriter.flush().catch(() => void 0);
|
|
141
|
+
};
|
|
142
|
+
process.once("SIGTERM", () => {
|
|
143
|
+
cleanup().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
144
|
+
});
|
|
145
|
+
process.once("SIGINT", () => {
|
|
146
|
+
cleanup().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
147
|
+
});
|
|
148
|
+
const runDaemon = () => new Promise((resolve) => {
|
|
149
|
+
const args2 = [currentCliScriptPath(), "daemon", "--foreground"];
|
|
150
|
+
if (options.port) {
|
|
151
|
+
args2.push("--port", String(options.port));
|
|
152
|
+
}
|
|
153
|
+
const child = spawn(process.execPath, args2, {
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
155
|
+
});
|
|
156
|
+
child.stdout?.on("data", (chunk) => {
|
|
157
|
+
logWriter.write(chunk).catch(() => void 0);
|
|
158
|
+
});
|
|
159
|
+
child.stderr?.on("data", (chunk) => {
|
|
160
|
+
logWriter.write(chunk).catch(() => void 0);
|
|
161
|
+
});
|
|
162
|
+
child.once("exit", (code) => {
|
|
163
|
+
resolve(code ?? 1);
|
|
164
|
+
});
|
|
165
|
+
child.once("error", () => {
|
|
166
|
+
resolve(1);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
let consecutiveFastExits = 0;
|
|
170
|
+
while (true) {
|
|
171
|
+
const startTime = Date.now();
|
|
172
|
+
const code = await runDaemon();
|
|
173
|
+
const elapsed = Date.now() - startTime;
|
|
174
|
+
if (code === 0) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (elapsed < 2e3) {
|
|
178
|
+
consecutiveFastExits++;
|
|
179
|
+
} else {
|
|
180
|
+
consecutiveFastExits = 0;
|
|
181
|
+
}
|
|
182
|
+
if (consecutiveFastExits >= 5) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
const backoffMs = Math.min(1e3 * consecutiveFastExits, 5e3);
|
|
186
|
+
await wait(backoffMs);
|
|
187
|
+
}
|
|
188
|
+
await cleanup();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/runtime/browser.ts
|
|
192
|
+
import { spawn as spawn2 } from "child_process";
|
|
193
|
+
async function openSystemBrowser(url) {
|
|
194
|
+
const platform = process.platform;
|
|
195
|
+
if (platform === "win32") {
|
|
196
|
+
return spawnDetached("cmd", ["/c", "start", "", url]);
|
|
197
|
+
}
|
|
198
|
+
if (platform === "darwin") {
|
|
199
|
+
return spawnDetached("open", [url]);
|
|
200
|
+
}
|
|
201
|
+
return spawnDetached("xdg-open", [url]);
|
|
202
|
+
}
|
|
203
|
+
async function spawnDetached(command2, args2) {
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
let settled = false;
|
|
206
|
+
const settle = (ok) => {
|
|
207
|
+
if (settled) return;
|
|
208
|
+
settled = true;
|
|
209
|
+
resolve(ok);
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
const child = spawn2(command2, args2, { detached: true, stdio: "ignore" });
|
|
213
|
+
child.once("error", () => settle(false));
|
|
214
|
+
child.once("spawn", () => {
|
|
215
|
+
child.unref();
|
|
216
|
+
settle(true);
|
|
217
|
+
});
|
|
218
|
+
} catch {
|
|
219
|
+
settle(false);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/pairing/preflight.ts
|
|
225
|
+
async function runPairingPreflight(options) {
|
|
226
|
+
const token = await generateAppConnectToken(options.paths);
|
|
227
|
+
const baseUrl = (options.serverBaseUrl ?? options.config.serverBaseUrl).replace(/\/+$/u, "");
|
|
228
|
+
const pairingUrl = buildPairingUrl(baseUrl, {
|
|
229
|
+
linkId: options.identity.link_id ?? "",
|
|
230
|
+
installId: options.identity.install_id,
|
|
231
|
+
connectToken: token.token,
|
|
232
|
+
port: options.config.port
|
|
233
|
+
});
|
|
234
|
+
if (options.openBrowser !== false) {
|
|
235
|
+
await openSystemBrowser(pairingUrl).catch(() => void 0);
|
|
236
|
+
}
|
|
237
|
+
return { pairingUrl, connectToken: token.token };
|
|
238
|
+
}
|
|
239
|
+
function buildPairingUrl(serverBaseUrl, params) {
|
|
240
|
+
const qs = new URLSearchParams({
|
|
241
|
+
link_id: params.linkId,
|
|
242
|
+
install_id: params.installId,
|
|
243
|
+
connect_token: params.connectToken,
|
|
244
|
+
port: String(params.port)
|
|
245
|
+
});
|
|
246
|
+
return `${serverBaseUrl}/link/pair?${qs.toString()}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/cli/index.ts
|
|
250
|
+
var args = process.argv.slice(2);
|
|
251
|
+
var command = args[0];
|
|
252
|
+
function hasFlag(...flags) {
|
|
253
|
+
return args.some((a) => flags.includes(a));
|
|
254
|
+
}
|
|
255
|
+
function getFlagValue(...flags) {
|
|
256
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
257
|
+
if (flags.includes(args[i])) return args[i + 1];
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
async function main() {
|
|
262
|
+
if (hasFlag("--version", "-v")) {
|
|
263
|
+
process.stdout.write(`${LINK_VERSION}
|
|
264
|
+
`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!command || hasFlag("--help", "-h")) {
|
|
268
|
+
printHelp();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const paths = resolveRuntimePaths();
|
|
272
|
+
await mkdir2(paths.homeDir, { recursive: true, mode: 448 });
|
|
273
|
+
switch (command) {
|
|
274
|
+
case "start":
|
|
275
|
+
await cmdStart(paths);
|
|
276
|
+
break;
|
|
277
|
+
case "stop":
|
|
278
|
+
await cmdStop(paths);
|
|
279
|
+
break;
|
|
280
|
+
case "status":
|
|
281
|
+
await cmdStatus(paths);
|
|
282
|
+
break;
|
|
283
|
+
case "restart":
|
|
284
|
+
await cmdStop(paths);
|
|
285
|
+
await cmdStart(paths);
|
|
286
|
+
break;
|
|
287
|
+
case "daemon":
|
|
288
|
+
await cmdDaemon(paths);
|
|
289
|
+
break;
|
|
290
|
+
case "daemon-supervisor":
|
|
291
|
+
await cmdDaemonSupervisor(paths);
|
|
292
|
+
break;
|
|
293
|
+
case "pair":
|
|
294
|
+
await cmdPair(paths);
|
|
295
|
+
break;
|
|
296
|
+
case "config":
|
|
297
|
+
await cmdConfig(paths);
|
|
298
|
+
break;
|
|
299
|
+
case "logs":
|
|
300
|
+
await cmdLogs(paths);
|
|
301
|
+
break;
|
|
302
|
+
case "autostart":
|
|
303
|
+
await cmdAutostart(paths);
|
|
304
|
+
break;
|
|
305
|
+
case "version":
|
|
306
|
+
process.stdout.write(`${LINK_VERSION}
|
|
307
|
+
`);
|
|
308
|
+
break;
|
|
309
|
+
default:
|
|
310
|
+
process.stderr.write(`Unknown command: ${command}
|
|
311
|
+
`);
|
|
312
|
+
printHelp();
|
|
313
|
+
process.exitCode = 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function cmdStart(paths) {
|
|
317
|
+
const status = await getDaemonStatus(paths);
|
|
318
|
+
if (status.state === "running") {
|
|
319
|
+
process.stdout.write(`Hermes Link is already running (PID ${status.pid}).
|
|
320
|
+
`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
await startDaemonProcess({ paths });
|
|
324
|
+
process.stdout.write("Hermes Link started.\n");
|
|
325
|
+
}
|
|
326
|
+
async function cmdStop(paths) {
|
|
327
|
+
const status = await getDaemonStatus(paths);
|
|
328
|
+
if (status.state !== "running") {
|
|
329
|
+
process.stdout.write("Hermes Link is not running.\n");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await stopDaemonProcess({ paths });
|
|
333
|
+
process.stdout.write("Hermes Link stopped.\n");
|
|
334
|
+
}
|
|
335
|
+
async function cmdStatus(paths) {
|
|
336
|
+
const config = await loadConfig(paths);
|
|
337
|
+
const daemonStatus = await getDaemonStatus(paths);
|
|
338
|
+
const probe = await probeLocalLinkService({ port: config.port });
|
|
339
|
+
const identity = await loadIdentity(paths).catch(() => null);
|
|
340
|
+
const autostartStatus = await getAutostartStatus();
|
|
341
|
+
const env = detectRuntimeEnvironment();
|
|
342
|
+
process.stdout.write(`Hermes Link ${LINK_VERSION}
|
|
343
|
+
`);
|
|
344
|
+
process.stdout.write(`Daemon: ${daemonStatus.state}${daemonStatus.pid ? ` (PID ${daemonStatus.pid})` : ""}
|
|
345
|
+
`);
|
|
346
|
+
process.stdout.write(`HTTP: ${probe.reachable ? `reachable on port ${config.port}` : `not reachable`}
|
|
347
|
+
`);
|
|
348
|
+
process.stdout.write(`Link ID: ${identity?.link_id ?? "unassigned"}
|
|
349
|
+
`);
|
|
350
|
+
process.stdout.write(`Autostart: ${autostartStatus.enabled ? "enabled" : "disabled"} (${autostartStatus.method})
|
|
351
|
+
`);
|
|
352
|
+
process.stdout.write(`Env: ${env.kind}
|
|
353
|
+
`);
|
|
354
|
+
if (env.warning) process.stdout.write(`Warning: ${env.warning}
|
|
355
|
+
`);
|
|
356
|
+
}
|
|
357
|
+
async function cmdDaemon(paths) {
|
|
358
|
+
if (!hasFlag("--foreground")) {
|
|
359
|
+
process.stderr.write("Use 'hermeslink start' or 'hermeslink daemon-supervisor'\n");
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const config = await loadConfig(paths);
|
|
364
|
+
const identity = await ensureIdentity(paths);
|
|
365
|
+
let relayToken = "";
|
|
366
|
+
try {
|
|
367
|
+
const bootstrapResult = await bootstrapWithRelay({
|
|
368
|
+
relayBaseUrl: config.relayBaseUrl,
|
|
369
|
+
identity,
|
|
370
|
+
port: config.port
|
|
371
|
+
});
|
|
372
|
+
if (!identity.link_id) {
|
|
373
|
+
await saveAssignedLinkId(bootstrapResult.linkId, paths);
|
|
374
|
+
}
|
|
375
|
+
relayToken = bootstrapResult.token;
|
|
376
|
+
} catch (err) {
|
|
377
|
+
process.stderr.write(`Warning: Relay bootstrap failed: ${err.message}
|
|
378
|
+
`);
|
|
379
|
+
}
|
|
380
|
+
const service = await startLinkService({ config, identity, paths, relayToken });
|
|
381
|
+
process.stdout.write(`Hermes Link running on port ${config.port}
|
|
382
|
+
`);
|
|
383
|
+
const shutdown = async () => {
|
|
384
|
+
await service.stop();
|
|
385
|
+
process.exit(0);
|
|
386
|
+
};
|
|
387
|
+
process.once("SIGTERM", shutdown);
|
|
388
|
+
process.once("SIGINT", shutdown);
|
|
389
|
+
}
|
|
390
|
+
async function cmdDaemonSupervisor(paths) {
|
|
391
|
+
const config = await loadConfig(paths);
|
|
392
|
+
await runDaemonSupervisor({ paths, port: config.port });
|
|
393
|
+
}
|
|
394
|
+
async function cmdPair(paths) {
|
|
395
|
+
const config = await loadConfig(paths);
|
|
396
|
+
const identity = await ensureIdentity(paths);
|
|
397
|
+
if (!identity.link_id) {
|
|
398
|
+
process.stderr.write("Error: Hermes Link is not connected to relay. Run 'hermeslink start' first.\n");
|
|
399
|
+
process.exitCode = 1;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const result = await runPairingPreflight({ identity, config, paths });
|
|
403
|
+
process.stdout.write(`Pairing URL: ${result.pairingUrl}
|
|
404
|
+
`);
|
|
405
|
+
process.stdout.write(`Connect token: ${result.connectToken}
|
|
406
|
+
`);
|
|
407
|
+
}
|
|
408
|
+
async function cmdConfig(paths) {
|
|
409
|
+
const subcommand = args[1];
|
|
410
|
+
if (subcommand === "get") {
|
|
411
|
+
const config = await loadConfig(paths);
|
|
412
|
+
process.stdout.write(JSON.stringify(config, null, 2) + "\n");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (subcommand === "set") {
|
|
416
|
+
const key = args[2];
|
|
417
|
+
const value = args[3];
|
|
418
|
+
if (!key || value === void 0) {
|
|
419
|
+
process.stderr.write("Usage: hermeslink config set <key> <value>\n");
|
|
420
|
+
process.exitCode = 1;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
await applyConfigSet(key, value, paths);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
process.stderr.write("Usage: hermeslink config [get|set]\n");
|
|
427
|
+
process.exitCode = 1;
|
|
428
|
+
}
|
|
429
|
+
async function applyConfigSet(key, value, paths) {
|
|
430
|
+
switch (key) {
|
|
431
|
+
case "port": {
|
|
432
|
+
const port = Number.parseInt(value, 10);
|
|
433
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
434
|
+
process.stderr.write("Invalid port number\n");
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await saveConfig({ port }, paths);
|
|
439
|
+
process.stdout.write(`Port set to ${port}
|
|
440
|
+
`);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case "lan-host": {
|
|
444
|
+
const lanHost = normalizeLanHost(value) ?? null;
|
|
445
|
+
await saveConfig({ lanHost }, paths);
|
|
446
|
+
process.stdout.write(`LAN host set to ${lanHost ?? "(auto)"}
|
|
447
|
+
`);
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case "language": {
|
|
451
|
+
const lang = value;
|
|
452
|
+
await saveConfig({ language: lang }, paths);
|
|
453
|
+
process.stdout.write(`Language set to ${lang}
|
|
454
|
+
`);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "log-level": {
|
|
458
|
+
const level = value;
|
|
459
|
+
await saveConfig({ logLevel: level }, paths);
|
|
460
|
+
process.stdout.write(`Log level set to ${level}
|
|
461
|
+
`);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
default:
|
|
465
|
+
process.stderr.write(`Unknown config key: ${key}
|
|
466
|
+
`);
|
|
467
|
+
process.exitCode = 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function cmdLogs(paths) {
|
|
471
|
+
const isGateway = hasFlag("--gateway");
|
|
472
|
+
const limit = getFlagValue("--limit", "-n");
|
|
473
|
+
const limitNum = limit ? Number.parseInt(limit, 10) : 50;
|
|
474
|
+
const entries = isGateway ? await readRecentGatewayLogEntries({ paths, limit: limitNum }) : await readRecentLogEntries({ paths, limit: limitNum });
|
|
475
|
+
for (const entry of entries) {
|
|
476
|
+
const ts = entry.ts ? new Date(entry.ts).toLocaleString() : "??";
|
|
477
|
+
process.stdout.write(`[${ts}] ${entry.level.toUpperCase()} ${entry.message}
|
|
478
|
+
`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function cmdAutostart(paths) {
|
|
482
|
+
const subcommand = args[1];
|
|
483
|
+
if (subcommand === "enable") {
|
|
484
|
+
const status2 = await enableAutostart();
|
|
485
|
+
process.stdout.write(`Autostart ${status2.enabled ? "enabled" : "could not be enabled"} (${status2.method})
|
|
486
|
+
`);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (subcommand === "disable") {
|
|
490
|
+
const status2 = await disableAutostart();
|
|
491
|
+
process.stdout.write(`Autostart ${status2.enabled ? "still enabled" : "disabled"} (${status2.method})
|
|
492
|
+
`);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const status = await getAutostartStatus();
|
|
496
|
+
process.stdout.write(`Autostart: ${status.enabled ? "enabled" : "disabled"} (${status.method})
|
|
497
|
+
`);
|
|
498
|
+
}
|
|
499
|
+
function printHelp() {
|
|
500
|
+
process.stdout.write(`Hermes Link ${LINK_VERSION} \u2014 Local service for Hermes Agent
|
|
501
|
+
|
|
502
|
+
Usage: ${LINK_COMMAND} <command> [options]
|
|
503
|
+
|
|
504
|
+
Commands:
|
|
505
|
+
start Start the Hermes Link daemon
|
|
506
|
+
stop Stop the Hermes Link daemon
|
|
507
|
+
restart Restart the daemon
|
|
508
|
+
status Show daemon and service status
|
|
509
|
+
pair Generate a pairing URL for your device
|
|
510
|
+
config get Show current configuration
|
|
511
|
+
config set Set a configuration value
|
|
512
|
+
autostart Show/enable/disable autostart
|
|
513
|
+
logs Show recent log entries (--gateway for gateway logs)
|
|
514
|
+
version Print version
|
|
515
|
+
|
|
516
|
+
Options:
|
|
517
|
+
--version, -v Print version
|
|
518
|
+
--help, -h Show this help
|
|
519
|
+
`);
|
|
520
|
+
}
|
|
521
|
+
main().catch((err) => {
|
|
522
|
+
process.stderr.write(`${err.message}
|
|
523
|
+
`);
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import Koa from 'koa';
|
|
2
|
+
import { Server } from 'http';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
interface RuntimePaths {
|
|
7
|
+
homeDir: string;
|
|
8
|
+
identityFile: string;
|
|
9
|
+
configFile: string;
|
|
10
|
+
stateFile: string;
|
|
11
|
+
credentialsFile: string;
|
|
12
|
+
databaseFile: string;
|
|
13
|
+
conversationsDir: string;
|
|
14
|
+
blobsDir: string;
|
|
15
|
+
indexesDir: string;
|
|
16
|
+
logsDir: string;
|
|
17
|
+
runDir: string;
|
|
18
|
+
pairingDir: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
22
|
+
type Language = "auto" | "en" | "zh-CN";
|
|
23
|
+
interface LinkConfig {
|
|
24
|
+
port: number;
|
|
25
|
+
lanHost: string | null;
|
|
26
|
+
serverBaseUrl: string;
|
|
27
|
+
relayBaseUrl: string;
|
|
28
|
+
appConnectTokenIssuer: string;
|
|
29
|
+
appConnectTokenAudience: string;
|
|
30
|
+
language: Language;
|
|
31
|
+
logLevel: LogLevel;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare const linkIdentitySchema: z.ZodObject<{
|
|
35
|
+
install_id: z.ZodString;
|
|
36
|
+
link_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
+
public_key_pem: z.ZodString;
|
|
38
|
+
private_key_pem: z.ZodString;
|
|
39
|
+
created_at: z.ZodString;
|
|
40
|
+
updated_at: z.ZodString;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
install_id: string;
|
|
43
|
+
public_key_pem: string;
|
|
44
|
+
private_key_pem: string;
|
|
45
|
+
created_at: string;
|
|
46
|
+
updated_at: string;
|
|
47
|
+
link_id?: string | null | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
install_id: string;
|
|
50
|
+
public_key_pem: string;
|
|
51
|
+
private_key_pem: string;
|
|
52
|
+
created_at: string;
|
|
53
|
+
updated_at: string;
|
|
54
|
+
link_id?: string | null | undefined;
|
|
55
|
+
}>;
|
|
56
|
+
type LinkIdentity = z.infer<typeof linkIdentitySchema>;
|
|
57
|
+
|
|
58
|
+
type RelayClientState = "disconnected" | "connecting" | "connected" | "closing";
|
|
59
|
+
interface RelayClientOptions {
|
|
60
|
+
relayBaseUrl: string;
|
|
61
|
+
identity: LinkIdentity;
|
|
62
|
+
token: string;
|
|
63
|
+
paths?: RuntimePaths;
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
reconnectDelayMs?: number;
|
|
66
|
+
maxReconnectDelayMs?: number;
|
|
67
|
+
pingIntervalMs?: number;
|
|
68
|
+
}
|
|
69
|
+
interface RelayMessage {
|
|
70
|
+
type: string;
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
declare class RelayClient extends EventEmitter {
|
|
74
|
+
private options;
|
|
75
|
+
private ws;
|
|
76
|
+
private state;
|
|
77
|
+
private token;
|
|
78
|
+
private logger;
|
|
79
|
+
private reconnectTimer;
|
|
80
|
+
private pingTimer;
|
|
81
|
+
private reconnectDelay;
|
|
82
|
+
private closed;
|
|
83
|
+
constructor(options: RelayClientOptions);
|
|
84
|
+
get currentState(): RelayClientState;
|
|
85
|
+
start(): void;
|
|
86
|
+
stop(): void;
|
|
87
|
+
send(message: RelayMessage): void;
|
|
88
|
+
private connect;
|
|
89
|
+
private buildWsUrl;
|
|
90
|
+
private scheduleReconnect;
|
|
91
|
+
private maybeRefreshTokenAndReconnect;
|
|
92
|
+
private startPing;
|
|
93
|
+
private stopPing;
|
|
94
|
+
private clearTimers;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface LinkServiceOptions {
|
|
98
|
+
config: LinkConfig;
|
|
99
|
+
identity: LinkIdentity;
|
|
100
|
+
paths: RuntimePaths;
|
|
101
|
+
relayToken: string;
|
|
102
|
+
}
|
|
103
|
+
interface LinkService {
|
|
104
|
+
app: Koa;
|
|
105
|
+
server: Server;
|
|
106
|
+
relayClient: RelayClient;
|
|
107
|
+
stop(): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
declare function startLinkService(options: LinkServiceOptions): Promise<LinkService>;
|
|
110
|
+
|
|
111
|
+
export { type LinkService, type LinkServiceOptions, startLinkService };
|
package/dist/http/app.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bulolo/hermes-link",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Hermes Link companion service and CLI for connecting hermes-agent through zhiji",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"hermeslink": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"scripts/check-node-version.mjs",
|
|
13
|
+
"scripts/postinstall.mjs",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"hermes",
|
|
19
|
+
"hermes-agent",
|
|
20
|
+
"relay",
|
|
21
|
+
"link",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup src/cli/index.ts src/http/app.ts --format esm --target node20 --dts --clean",
|
|
32
|
+
"check": "tsc --noEmit",
|
|
33
|
+
"dev": "tsx src/cli/index.ts",
|
|
34
|
+
"preinstall": "node ./scripts/check-node-version.mjs",
|
|
35
|
+
"postinstall": "node ./scripts/postinstall.mjs",
|
|
36
|
+
"prepack": "npm run build",
|
|
37
|
+
"start": "node ./dist/cli/index.js",
|
|
38
|
+
"test": "vitest",
|
|
39
|
+
"publish:npm": "npm publish --access public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@koa/cors": "^5.0.0",
|
|
43
|
+
"@koa/router": "^15.4.0",
|
|
44
|
+
"better-sqlite3": "^12.9.0",
|
|
45
|
+
"koa": "^2.15.3",
|
|
46
|
+
"koa-bodyparser": "^4.4.1",
|
|
47
|
+
"pino": "^9.0.0",
|
|
48
|
+
"qrcode": "^1.5.4",
|
|
49
|
+
"qrcode-terminal": "^0.12.0",
|
|
50
|
+
"ws": "^8.18.0",
|
|
51
|
+
"yaml": "^2.6.1",
|
|
52
|
+
"zod": "^3.24.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
56
|
+
"@types/koa": "^2.15.0",
|
|
57
|
+
"@types/koa__cors": "^5.0.1",
|
|
58
|
+
"@types/koa-bodyparser": "^4.3.12",
|
|
59
|
+
"@types/node": "^20.19.39",
|
|
60
|
+
"@types/qrcode": "^1.5.6",
|
|
61
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
62
|
+
"@types/ws": "^8.5.13",
|
|
63
|
+
"pino-pretty": "^13.0.0",
|
|
64
|
+
"tsup": "^8.3.5",
|
|
65
|
+
"tsx": "^4.19.2",
|
|
66
|
+
"typescript": "^5.7.2",
|
|
67
|
+
"vitest": "^2.1.8"
|
|
68
|
+
}
|
|
69
|
+
}
|