@hermespilot/link 0.1.1 → 0.1.3
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/README.md +8 -0
- package/dist/{chunk-4CDHEW3J.js → chunk-7M3UZCA7.js} +393 -46
- package/dist/chunk-7M3UZCA7.js.map +1 -0
- package/dist/cli/index.js +644 -107
- package/dist/cli/index.js.map +1 -1
- package/dist/http/app.d.ts +45 -2
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-4CDHEW3J.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -3,19 +3,500 @@ import {
|
|
|
3
3
|
LINK_COMMAND,
|
|
4
4
|
LINK_VERSION,
|
|
5
5
|
createApp,
|
|
6
|
+
createFileLogger,
|
|
6
7
|
ensureHermesApiServerKey,
|
|
7
8
|
ensureIdentity,
|
|
8
9
|
getIdentityStatus,
|
|
10
|
+
getLinkLogFile,
|
|
9
11
|
loadConfig,
|
|
10
12
|
loadIdentity,
|
|
11
13
|
preparePairing,
|
|
12
14
|
resolveRuntimePaths
|
|
13
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-7M3UZCA7.js";
|
|
14
16
|
|
|
15
17
|
// src/cli/index.ts
|
|
16
18
|
import { Command } from "commander";
|
|
17
19
|
import qrcode from "qrcode-terminal";
|
|
18
20
|
|
|
21
|
+
// src/autostart/autostart.ts
|
|
22
|
+
import { execFile } from "child_process";
|
|
23
|
+
import { mkdir as mkdir3, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
24
|
+
import os from "os";
|
|
25
|
+
import path2 from "path";
|
|
26
|
+
import { promisify } from "util";
|
|
27
|
+
|
|
28
|
+
// src/daemon/process.ts
|
|
29
|
+
import { spawn } from "child_process";
|
|
30
|
+
import { mkdir as mkdir2, open, readFile, rm as rm2 } from "fs/promises";
|
|
31
|
+
import path from "path";
|
|
32
|
+
|
|
33
|
+
// src/daemon/service.ts
|
|
34
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
35
|
+
|
|
36
|
+
// src/relay/control-client.ts
|
|
37
|
+
import WebSocket from "ws";
|
|
38
|
+
function connectRelayControl(options) {
|
|
39
|
+
const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
|
|
40
|
+
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
41
|
+
wsUrl.searchParams.set("link_id", options.linkId);
|
|
42
|
+
const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
43
|
+
const backoffBaseMs = options.backoffBaseMs ?? 1e3;
|
|
44
|
+
const backoffMaxMs = options.backoffMaxMs ?? 3e4;
|
|
45
|
+
let reconnectAttempts = 0;
|
|
46
|
+
let closedByUser = false;
|
|
47
|
+
let socket = null;
|
|
48
|
+
let retryTimer = null;
|
|
49
|
+
let abortControllers = /* @__PURE__ */ new Map();
|
|
50
|
+
const connect = () => {
|
|
51
|
+
options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
|
|
52
|
+
socket = new WebSocket(wsUrl, {
|
|
53
|
+
headers: {
|
|
54
|
+
"x-hermes-link-version": LINK_VERSION
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
socket.on("open", () => {
|
|
58
|
+
reconnectAttempts = 0;
|
|
59
|
+
options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
|
|
60
|
+
});
|
|
61
|
+
socket.on("message", (raw) => {
|
|
62
|
+
if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {
|
|
66
|
+
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
67
|
+
socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
socket.on("error", (error) => {
|
|
71
|
+
const message = error instanceof Error ? error.message : "Relay websocket error";
|
|
72
|
+
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts, message });
|
|
73
|
+
});
|
|
74
|
+
socket.on("close", () => {
|
|
75
|
+
abortAll(abortControllers);
|
|
76
|
+
abortControllers = /* @__PURE__ */ new Map();
|
|
77
|
+
if (closedByUser) {
|
|
78
|
+
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
82
|
+
options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
reconnectAttempts += 1;
|
|
86
|
+
const delay = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
|
|
87
|
+
options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay}ms` });
|
|
88
|
+
retryTimer = setTimeout(connect, delay);
|
|
89
|
+
retryTimer.unref?.();
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
connect();
|
|
93
|
+
return {
|
|
94
|
+
close() {
|
|
95
|
+
closedByUser = true;
|
|
96
|
+
if (retryTimer) {
|
|
97
|
+
clearTimeout(retryTimer);
|
|
98
|
+
}
|
|
99
|
+
abortAll(abortControllers);
|
|
100
|
+
socket?.close();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function abortAll(abortControllers) {
|
|
105
|
+
for (const controller of abortControllers.values()) {
|
|
106
|
+
controller.abort();
|
|
107
|
+
}
|
|
108
|
+
abortControllers.clear();
|
|
109
|
+
}
|
|
110
|
+
function computeBackoffMs(attempt, baseMs, maxMs) {
|
|
111
|
+
const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
|
|
112
|
+
const jitter = Math.floor(Math.random() * Math.min(1e3, exponential * 0.2));
|
|
113
|
+
return exponential + jitter;
|
|
114
|
+
}
|
|
115
|
+
async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
116
|
+
const frame = JSON.parse(raw);
|
|
117
|
+
if (frame.type === "http.cancel") {
|
|
118
|
+
abortControllers.get(frame.id)?.abort();
|
|
119
|
+
abortControllers.delete(frame.id);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (frame.type !== "http.request") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const abortController = new AbortController();
|
|
126
|
+
abortControllers.set(frame.id, abortController);
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
|
|
129
|
+
method: frame.method,
|
|
130
|
+
headers: frame.headers ?? {},
|
|
131
|
+
body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, "base64") : void 0,
|
|
132
|
+
signal: abortController.signal
|
|
133
|
+
});
|
|
134
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
135
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
136
|
+
if (response.body && contentType.includes("text/event-stream")) {
|
|
137
|
+
socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
|
|
138
|
+
const reader = response.body.getReader();
|
|
139
|
+
while (true) {
|
|
140
|
+
const next = await reader.read();
|
|
141
|
+
if (next.done) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
socket.send(JSON.stringify({ type: "http.stream.chunk", id: frame.id, bodyBase64: Buffer.from(next.value).toString("base64") }));
|
|
145
|
+
}
|
|
146
|
+
socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const body = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
150
|
+
socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
153
|
+
socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
|
|
154
|
+
} finally {
|
|
155
|
+
abortControllers.delete(frame.id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/daemon/service.ts
|
|
160
|
+
async function startLinkService(options = {}) {
|
|
161
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
162
|
+
const logger = createFileLogger({ paths });
|
|
163
|
+
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
164
|
+
await logger.info("service_starting", {
|
|
165
|
+
port: config.port,
|
|
166
|
+
mode: identity?.link_id ? "paired" : "local-only"
|
|
167
|
+
});
|
|
168
|
+
const app = await createApp({ paths, logger, onPairingClaimed: options.onPairingClaimed });
|
|
169
|
+
const server = app.listen(config.port);
|
|
170
|
+
server.on("error", (error) => {
|
|
171
|
+
void logger.error("service_error", { error: error.message });
|
|
172
|
+
});
|
|
173
|
+
void logger.info("service_started", {
|
|
174
|
+
port: config.port,
|
|
175
|
+
link_id: identity?.link_id ?? null
|
|
176
|
+
});
|
|
177
|
+
let relay = null;
|
|
178
|
+
if (identity?.link_id) {
|
|
179
|
+
relay = connectRelayControl({
|
|
180
|
+
relayBaseUrl: config.relayBaseUrl,
|
|
181
|
+
linkId: identity.link_id,
|
|
182
|
+
localPort: config.port,
|
|
183
|
+
maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
|
|
184
|
+
backoffBaseMs: 1e3,
|
|
185
|
+
backoffMaxMs: 3e4,
|
|
186
|
+
onStatus: (status) => {
|
|
187
|
+
void logger.info("relay_status", status);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
192
|
+
}
|
|
193
|
+
if (options.writePidFile) {
|
|
194
|
+
await writePidFile(paths);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
async close() {
|
|
198
|
+
relay?.close();
|
|
199
|
+
await closeServer(server);
|
|
200
|
+
await logger.info("service_stopped");
|
|
201
|
+
await logger.flush();
|
|
202
|
+
if (options.writePidFile) {
|
|
203
|
+
await rm(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function pidFilePath(paths = resolveRuntimePaths()) {
|
|
209
|
+
return `${paths.runDir}/hermeslink.pid`;
|
|
210
|
+
}
|
|
211
|
+
async function writePidFile(paths) {
|
|
212
|
+
await mkdir(paths.runDir, { recursive: true, mode: 448 });
|
|
213
|
+
await writeFile(pidFilePath(paths), `${process.pid}
|
|
214
|
+
`, { mode: 384 });
|
|
215
|
+
}
|
|
216
|
+
async function closeServer(server) {
|
|
217
|
+
await new Promise((resolve, reject) => {
|
|
218
|
+
server.close((error) => {
|
|
219
|
+
if (error) {
|
|
220
|
+
reject(error);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
resolve();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/daemon/process.ts
|
|
229
|
+
async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
230
|
+
const status = await getDaemonStatus(paths);
|
|
231
|
+
if (status.running) {
|
|
232
|
+
return status;
|
|
233
|
+
}
|
|
234
|
+
await mkdir2(paths.logsDir, { recursive: true, mode: 448 });
|
|
235
|
+
await mkdir2(paths.runDir, { recursive: true, mode: 448 });
|
|
236
|
+
const log = await open(daemonLogFile(paths), "a", 384);
|
|
237
|
+
const scriptPath = currentCliScriptPath();
|
|
238
|
+
const child = spawn(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
239
|
+
detached: true,
|
|
240
|
+
stdio: ["ignore", log.fd, log.fd],
|
|
241
|
+
env: process.env
|
|
242
|
+
});
|
|
243
|
+
child.unref();
|
|
244
|
+
await log.close();
|
|
245
|
+
for (let index = 0; index < 12; index += 1) {
|
|
246
|
+
await wait(250);
|
|
247
|
+
const next = await getDaemonStatus(paths);
|
|
248
|
+
if (next.running) {
|
|
249
|
+
return next;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return await getDaemonStatus(paths);
|
|
253
|
+
}
|
|
254
|
+
async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
255
|
+
const status = await getDaemonStatus(paths);
|
|
256
|
+
if (!status.running || !status.pid) {
|
|
257
|
+
return status;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
process.kill(status.pid, "SIGTERM");
|
|
261
|
+
} catch {
|
|
262
|
+
await rm2(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
263
|
+
return await getDaemonStatus(paths);
|
|
264
|
+
}
|
|
265
|
+
for (let index = 0; index < 20; index += 1) {
|
|
266
|
+
await wait(250);
|
|
267
|
+
if (!isProcessAlive(status.pid)) {
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!isProcessAlive(status.pid)) {
|
|
272
|
+
await rm2(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
273
|
+
}
|
|
274
|
+
return await getDaemonStatus(paths);
|
|
275
|
+
}
|
|
276
|
+
async function getDaemonStatus(paths = resolveRuntimePaths()) {
|
|
277
|
+
const pidFile = pidFilePath(paths);
|
|
278
|
+
const pid = await readPid(pidFile);
|
|
279
|
+
if (pid && !isProcessAlive(pid)) {
|
|
280
|
+
await rm2(pidFile, { force: true }).catch(() => void 0);
|
|
281
|
+
return {
|
|
282
|
+
running: false,
|
|
283
|
+
pid: null,
|
|
284
|
+
pidFile,
|
|
285
|
+
logFile: daemonLogFile(paths)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
running: Boolean(pid),
|
|
290
|
+
pid,
|
|
291
|
+
pidFile,
|
|
292
|
+
logFile: daemonLogFile(paths)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function daemonLogFile(paths = resolveRuntimePaths()) {
|
|
296
|
+
return path.join(paths.logsDir, "daemon.log");
|
|
297
|
+
}
|
|
298
|
+
function currentCliScriptPath() {
|
|
299
|
+
return process.argv[1];
|
|
300
|
+
}
|
|
301
|
+
async function readPid(filePath) {
|
|
302
|
+
const raw = await readFile(filePath, "utf8").catch(() => null);
|
|
303
|
+
if (!raw) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
307
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
308
|
+
}
|
|
309
|
+
function isProcessAlive(pid) {
|
|
310
|
+
try {
|
|
311
|
+
process.kill(pid, 0);
|
|
312
|
+
return true;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function wait(ms) {
|
|
318
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/autostart/autostart.ts
|
|
322
|
+
var execFileAsync = promisify(execFile);
|
|
323
|
+
var MACOS_LABEL = "com.hermespilot.link";
|
|
324
|
+
async function enableAutostart() {
|
|
325
|
+
const definition = await resolveAutostartDefinition();
|
|
326
|
+
if (!definition) {
|
|
327
|
+
return unsupportedStatus();
|
|
328
|
+
}
|
|
329
|
+
await mkdir3(path2.dirname(definition.filePath), { recursive: true, mode: 448 });
|
|
330
|
+
await writeFile2(definition.filePath, definition.content, { mode: 384 });
|
|
331
|
+
if (definition.method === "systemd-user") {
|
|
332
|
+
await execFileAsync("systemctl", ["--user", "enable", path2.basename(definition.filePath)]).catch(async () => {
|
|
333
|
+
await rm3(definition.filePath, { force: true }).catch(() => void 0);
|
|
334
|
+
const fallback = xdgAutostartDefinition();
|
|
335
|
+
await mkdir3(path2.dirname(fallback.filePath), { recursive: true, mode: 448 });
|
|
336
|
+
await writeFile2(fallback.filePath, fallback.content, { mode: 384 });
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return await getAutostartStatus();
|
|
340
|
+
}
|
|
341
|
+
async function disableAutostart() {
|
|
342
|
+
const definitions = await allAutostartDefinitions();
|
|
343
|
+
for (const definition of definitions) {
|
|
344
|
+
if (definition.method === "systemd-user") {
|
|
345
|
+
await execFileAsync("systemctl", ["--user", "disable", path2.basename(definition.filePath)]).catch(() => void 0);
|
|
346
|
+
}
|
|
347
|
+
await rm3(definition.filePath, { force: true }).catch(() => void 0);
|
|
348
|
+
}
|
|
349
|
+
return await getAutostartStatus();
|
|
350
|
+
}
|
|
351
|
+
async function getAutostartStatus() {
|
|
352
|
+
const definitions = await allAutostartDefinitions();
|
|
353
|
+
if (definitions.length === 0) {
|
|
354
|
+
return unsupportedStatus();
|
|
355
|
+
}
|
|
356
|
+
for (const definition of definitions) {
|
|
357
|
+
const content = await readFile2(definition.filePath, "utf8").catch(() => null);
|
|
358
|
+
if (content !== null) {
|
|
359
|
+
return {
|
|
360
|
+
supported: true,
|
|
361
|
+
enabled: true,
|
|
362
|
+
method: definition.method,
|
|
363
|
+
filePath: definition.filePath
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const primary = definitions[0];
|
|
368
|
+
return {
|
|
369
|
+
supported: true,
|
|
370
|
+
enabled: false,
|
|
371
|
+
method: primary.method,
|
|
372
|
+
filePath: primary.filePath
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function resolveAutostartDefinition() {
|
|
376
|
+
if (process.platform === "darwin") {
|
|
377
|
+
return launchdDefinition();
|
|
378
|
+
}
|
|
379
|
+
if (process.platform === "win32") {
|
|
380
|
+
return windowsStartupDefinition();
|
|
381
|
+
}
|
|
382
|
+
if (process.platform === "linux") {
|
|
383
|
+
return await hasSystemctlUser() ? systemdUserDefinition() : xdgAutostartDefinition();
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
async function allAutostartDefinitions() {
|
|
388
|
+
if (process.platform === "darwin") {
|
|
389
|
+
return [launchdDefinition()];
|
|
390
|
+
}
|
|
391
|
+
if (process.platform === "win32") {
|
|
392
|
+
return [windowsStartupDefinition()];
|
|
393
|
+
}
|
|
394
|
+
if (process.platform === "linux") {
|
|
395
|
+
return [systemdUserDefinition(), xdgAutostartDefinition()];
|
|
396
|
+
}
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
async function hasSystemctlUser() {
|
|
400
|
+
try {
|
|
401
|
+
await execFileAsync("systemctl", ["--user", "show-environment"]);
|
|
402
|
+
return true;
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function launchdDefinition() {
|
|
408
|
+
const filePath = path2.join(os.homedir(), "Library", "LaunchAgents", `${MACOS_LABEL}.plist`);
|
|
409
|
+
return {
|
|
410
|
+
method: "launchd",
|
|
411
|
+
filePath,
|
|
412
|
+
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
413
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
414
|
+
<plist version="1.0">
|
|
415
|
+
<dict>
|
|
416
|
+
<key>Label</key>
|
|
417
|
+
<string>${MACOS_LABEL}</string>
|
|
418
|
+
<key>ProgramArguments</key>
|
|
419
|
+
<array>
|
|
420
|
+
<string>${xmlEscape(process.execPath)}</string>
|
|
421
|
+
<string>${xmlEscape(currentCliScriptPath())}</string>
|
|
422
|
+
<string>daemon</string>
|
|
423
|
+
<string>--foreground</string>
|
|
424
|
+
</array>
|
|
425
|
+
<key>RunAtLoad</key>
|
|
426
|
+
<true/>
|
|
427
|
+
<key>KeepAlive</key>
|
|
428
|
+
<false/>
|
|
429
|
+
<key>StandardOutPath</key>
|
|
430
|
+
<string>${xmlEscape(path2.join(os.homedir(), ".hermeslink", "logs", "daemon.log"))}</string>
|
|
431
|
+
<key>StandardErrorPath</key>
|
|
432
|
+
<string>${xmlEscape(path2.join(os.homedir(), ".hermeslink", "logs", "daemon.log"))}</string>
|
|
433
|
+
</dict>
|
|
434
|
+
</plist>
|
|
435
|
+
`
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function systemdUserDefinition() {
|
|
439
|
+
const filePath = path2.join(os.homedir(), ".config", "systemd", "user", "hermeslink.service");
|
|
440
|
+
return {
|
|
441
|
+
method: "systemd-user",
|
|
442
|
+
filePath,
|
|
443
|
+
content: `[Unit]
|
|
444
|
+
Description=Hermes Link
|
|
445
|
+
After=network-online.target
|
|
446
|
+
|
|
447
|
+
[Service]
|
|
448
|
+
Type=simple
|
|
449
|
+
ExecStart=${systemdQuote(process.execPath)} ${systemdQuote(currentCliScriptPath())} daemon --foreground
|
|
450
|
+
Restart=no
|
|
451
|
+
|
|
452
|
+
[Install]
|
|
453
|
+
WantedBy=default.target
|
|
454
|
+
`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function xdgAutostartDefinition() {
|
|
458
|
+
const filePath = path2.join(os.homedir(), ".config", "autostart", "hermeslink.desktop");
|
|
459
|
+
return {
|
|
460
|
+
method: "xdg-autostart",
|
|
461
|
+
filePath,
|
|
462
|
+
content: `[Desktop Entry]
|
|
463
|
+
Type=Application
|
|
464
|
+
Name=Hermes Link
|
|
465
|
+
Exec=${desktopQuote(process.execPath)} ${desktopQuote(currentCliScriptPath())} daemon --foreground
|
|
466
|
+
Terminal=false
|
|
467
|
+
X-GNOME-Autostart-enabled=true
|
|
468
|
+
`
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function windowsStartupDefinition() {
|
|
472
|
+
const appData = process.env.APPDATA ?? path2.join(os.homedir(), "AppData", "Roaming");
|
|
473
|
+
const filePath = path2.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "HermesLink.cmd");
|
|
474
|
+
return {
|
|
475
|
+
method: "windows-startup",
|
|
476
|
+
filePath,
|
|
477
|
+
content: `@echo off\r
|
|
478
|
+
start "" /min "${process.execPath}" "${currentCliScriptPath()}" daemon --foreground\r
|
|
479
|
+
`
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function unsupportedStatus() {
|
|
483
|
+
return {
|
|
484
|
+
supported: false,
|
|
485
|
+
enabled: false,
|
|
486
|
+
method: "unsupported",
|
|
487
|
+
filePath: null
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function xmlEscape(value) {
|
|
491
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
492
|
+
}
|
|
493
|
+
function systemdQuote(value) {
|
|
494
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
495
|
+
}
|
|
496
|
+
function desktopQuote(value) {
|
|
497
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
498
|
+
}
|
|
499
|
+
|
|
19
500
|
// src/i18n.ts
|
|
20
501
|
var messages = {
|
|
21
502
|
en: {
|
|
@@ -29,10 +510,30 @@ var messages = {
|
|
|
29
510
|
"status.linkId": "Link ID: {value}",
|
|
30
511
|
"status.notPaired": "not paired",
|
|
31
512
|
"start.description": "Start Hermes Link daemon",
|
|
513
|
+
"start.backgroundStarted": "Hermes Link is running in the background. PID: {pid}",
|
|
514
|
+
"start.alreadyRunning": "Hermes Link is already running. PID: {pid}",
|
|
32
515
|
"start.notPaired": "Hermes Link is not paired yet. Starting in local-only maintenance mode.",
|
|
33
516
|
"start.notPaired.detail": "Relay, Server polling, and LAN entrypoints stay disabled until you run `hermeslink pair`.",
|
|
34
517
|
"start.listening": "Hermes Link API listening on http://127.0.0.1:{port}",
|
|
35
518
|
"start.relayConnecting": "Relay control connecting for {linkId}",
|
|
519
|
+
"stop.description": "Stop the background Hermes Link daemon",
|
|
520
|
+
"stop.stopped": "Hermes Link stopped.",
|
|
521
|
+
"stop.notRunning": "Hermes Link is not running.",
|
|
522
|
+
"restart.description": "Restart the background Hermes Link daemon",
|
|
523
|
+
"daemon.description": "Run Hermes Link in the foreground",
|
|
524
|
+
"daemon.foreground": "Hermes Link foreground daemon is running. Press Ctrl+C to stop.",
|
|
525
|
+
"logs.description": "Show Hermes Link log paths",
|
|
526
|
+
"logs.servicePath": "Service log: {path}",
|
|
527
|
+
"logs.daemonPath": "Daemon stdout/stderr log: {path}",
|
|
528
|
+
"autostart.description": "Manage boot autostart",
|
|
529
|
+
"autostart.on.description": "Enable boot autostart",
|
|
530
|
+
"autostart.off.description": "Disable boot autostart",
|
|
531
|
+
"autostart.status.description": "Show boot autostart status",
|
|
532
|
+
"autostart.enabled": "Boot autostart enabled via {method}: {path}",
|
|
533
|
+
"autostart.disabled": "Boot autostart disabled.",
|
|
534
|
+
"autostart.status.enabled": "Boot autostart: enabled via {method}: {path}",
|
|
535
|
+
"autostart.status.disabled": "Boot autostart: disabled. Method: {method}. File: {path}",
|
|
536
|
+
"autostart.unsupported": "Boot autostart is not supported on this platform yet.",
|
|
36
537
|
"pair.description": "Create a Hermes Link pairing session",
|
|
37
538
|
"pair.preparing": "Preparing pairing session through HermesPilot Server and Relay...",
|
|
38
539
|
"pair.server": "Server: {url}",
|
|
@@ -42,6 +543,8 @@ var messages = {
|
|
|
42
543
|
"pair.localApi": "Local API: http://127.0.0.1:{port}",
|
|
43
544
|
"pair.scan": "Scan this QR code with the HermesPilot App:",
|
|
44
545
|
"pair.expires": "Pairing expires in 10 minutes. Press Ctrl+C to stop Hermes Link.",
|
|
546
|
+
"pair.claimed": "Pairing succeeded. Starting Hermes Link in the background...",
|
|
547
|
+
"pair.autostartFailed": "Pairing succeeded, but boot autostart could not be enabled: {message}",
|
|
45
548
|
"doctor.description": "Run local diagnostics",
|
|
46
549
|
"doctor.identityOk": "Runtime identity: OK",
|
|
47
550
|
"doctor.installId": "Install ID: {value}",
|
|
@@ -66,10 +569,30 @@ var messages = {
|
|
|
66
569
|
"status.linkId": "Link ID\uFF1A{value}",
|
|
67
570
|
"status.notPaired": "\u5C1A\u672A\u914D\u5BF9",
|
|
68
571
|
"start.description": "\u542F\u52A8 Hermes Link \u670D\u52A1",
|
|
572
|
+
"start.backgroundStarted": "Hermes Link \u5DF2\u5728\u540E\u53F0\u8FD0\u884C\u3002PID\uFF1A{pid}",
|
|
573
|
+
"start.alreadyRunning": "Hermes Link \u5DF2\u7ECF\u5728\u8FD0\u884C\u3002PID\uFF1A{pid}",
|
|
69
574
|
"start.notPaired": "Hermes Link \u8FD8\u6CA1\u6709\u914D\u5BF9\uFF0C\u5C06\u4EE5\u672C\u5730\u7EF4\u62A4\u6A21\u5F0F\u542F\u52A8\u3002",
|
|
70
575
|
"start.notPaired.detail": "\u5728\u4F60\u8FD0\u884C `hermeslink pair` \u524D\uFF0CRelay\u3001Server \u8F6E\u8BE2\u548C\u5C40\u57DF\u7F51\u5165\u53E3\u90FD\u4F1A\u4FDD\u6301\u5173\u95ED\u3002",
|
|
71
576
|
"start.listening": "Hermes Link API \u6B63\u5728\u76D1\u542C http://127.0.0.1:{port}",
|
|
72
577
|
"start.relayConnecting": "\u6B63\u5728\u4E3A {linkId} \u8FDE\u63A5 Relay \u63A7\u5236\u901A\u9053",
|
|
578
|
+
"stop.description": "\u505C\u6B62\u540E\u53F0 Hermes Link \u670D\u52A1",
|
|
579
|
+
"stop.stopped": "Hermes Link \u5DF2\u505C\u6B62\u3002",
|
|
580
|
+
"stop.notRunning": "Hermes Link \u6CA1\u6709\u5728\u8FD0\u884C\u3002",
|
|
581
|
+
"restart.description": "\u91CD\u542F\u540E\u53F0 Hermes Link \u670D\u52A1",
|
|
582
|
+
"daemon.description": "\u4EE5\u524D\u53F0\u65B9\u5F0F\u8FD0\u884C Hermes Link",
|
|
583
|
+
"daemon.foreground": "Hermes Link \u524D\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
|
|
584
|
+
"logs.description": "\u663E\u793A Hermes Link \u65E5\u5FD7\u8DEF\u5F84",
|
|
585
|
+
"logs.servicePath": "\u670D\u52A1\u65E5\u5FD7\uFF1A{path}",
|
|
586
|
+
"logs.daemonPath": "Daemon \u6807\u51C6\u8F93\u51FA/\u9519\u8BEF\u65E5\u5FD7\uFF1A{path}",
|
|
587
|
+
"autostart.description": "\u7BA1\u7406\u5F00\u673A\u81EA\u542F",
|
|
588
|
+
"autostart.on.description": "\u542F\u7528\u5F00\u673A\u81EA\u542F",
|
|
589
|
+
"autostart.off.description": "\u5173\u95ED\u5F00\u673A\u81EA\u542F",
|
|
590
|
+
"autostart.status.description": "\u67E5\u770B\u5F00\u673A\u81EA\u542F\u72B6\u6001",
|
|
591
|
+
"autostart.enabled": "\u5DF2\u542F\u7528\u5F00\u673A\u81EA\u542F\uFF0C\u65B9\u5F0F\uFF1A{method}\uFF0C\u6587\u4EF6\uFF1A{path}",
|
|
592
|
+
"autostart.disabled": "\u5DF2\u5173\u95ED\u5F00\u673A\u81EA\u542F\u3002",
|
|
593
|
+
"autostart.status.enabled": "\u5F00\u673A\u81EA\u542F\uFF1A\u5DF2\u542F\u7528\uFF0C\u65B9\u5F0F\uFF1A{method}\uFF0C\u6587\u4EF6\uFF1A{path}",
|
|
594
|
+
"autostart.status.disabled": "\u5F00\u673A\u81EA\u542F\uFF1A\u672A\u542F\u7528\u3002\u65B9\u5F0F\uFF1A{method}\uFF0C\u6587\u4EF6\uFF1A{path}",
|
|
595
|
+
"autostart.unsupported": "\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u5F00\u673A\u81EA\u542F\u3002",
|
|
73
596
|
"pair.description": "\u521B\u5EFA Hermes Link \u914D\u5BF9\u4F1A\u8BDD",
|
|
74
597
|
"pair.preparing": "\u6B63\u5728\u901A\u8FC7 HermesPilot Server \u548C Relay \u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD...",
|
|
75
598
|
"pair.server": "Server\uFF1A{url}",
|
|
@@ -79,6 +602,8 @@ var messages = {
|
|
|
79
602
|
"pair.localApi": "\u672C\u5730 API\uFF1Ahttp://127.0.0.1:{port}",
|
|
80
603
|
"pair.scan": "\u8BF7\u4F7F\u7528 HermesPilot App \u626B\u63CF\u8FD9\u4E2A\u4E8C\u7EF4\u7801\uFF1A",
|
|
81
604
|
"pair.expires": "\u914D\u5BF9\u4F1A\u8BDD 10 \u5206\u949F\u540E\u8FC7\u671F\u3002\u6309 Ctrl+C \u505C\u6B62 Hermes Link\u3002",
|
|
605
|
+
"pair.claimed": "\u914D\u5BF9\u5DF2\u6210\u529F\u3002\u6B63\u5728\u628A Hermes Link \u5207\u6362\u5230\u540E\u53F0\u8FD0\u884C...",
|
|
606
|
+
"pair.autostartFailed": "\u914D\u5BF9\u5DF2\u6210\u529F\uFF0C\u4F46\u542F\u7528\u5F00\u673A\u81EA\u542F\u5931\u8D25\uFF1A{message}",
|
|
82
607
|
"doctor.description": "\u8FD0\u884C\u672C\u673A\u8BCA\u65AD",
|
|
83
608
|
"doctor.identityOk": "\u8FD0\u884C\u8EAB\u4EFD\uFF1A\u6B63\u5E38",
|
|
84
609
|
"doctor.installId": "Install ID\uFF1A{value}",
|
|
@@ -166,83 +691,6 @@ function parseLanguage(value) {
|
|
|
166
691
|
return null;
|
|
167
692
|
}
|
|
168
693
|
|
|
169
|
-
// src/relay/control-client.ts
|
|
170
|
-
import WebSocket from "ws";
|
|
171
|
-
function connectRelayControl(options) {
|
|
172
|
-
const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
|
|
173
|
-
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
174
|
-
wsUrl.searchParams.set("link_id", options.linkId);
|
|
175
|
-
const socket = new WebSocket(wsUrl, {
|
|
176
|
-
headers: {
|
|
177
|
-
"x-hermes-link-version": LINK_VERSION
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
const abortControllers = /* @__PURE__ */ new Map();
|
|
181
|
-
socket.on("message", (raw) => {
|
|
182
|
-
if (typeof raw !== "string" && !Buffer.isBuffer(raw)) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {
|
|
186
|
-
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
187
|
-
socket.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
socket.on("close", () => {
|
|
191
|
-
for (const controller of abortControllers.values()) {
|
|
192
|
-
controller.abort();
|
|
193
|
-
}
|
|
194
|
-
abortControllers.clear();
|
|
195
|
-
});
|
|
196
|
-
return {
|
|
197
|
-
close() {
|
|
198
|
-
socket.close();
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
203
|
-
const frame = JSON.parse(raw);
|
|
204
|
-
if (frame.type === "http.cancel") {
|
|
205
|
-
abortControllers.get(frame.id)?.abort();
|
|
206
|
-
abortControllers.delete(frame.id);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (frame.type !== "http.request") {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const abortController = new AbortController();
|
|
213
|
-
abortControllers.set(frame.id, abortController);
|
|
214
|
-
try {
|
|
215
|
-
const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
|
|
216
|
-
method: frame.method,
|
|
217
|
-
headers: frame.headers ?? {},
|
|
218
|
-
body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, "base64") : void 0,
|
|
219
|
-
signal: abortController.signal
|
|
220
|
-
});
|
|
221
|
-
const headers = Object.fromEntries(response.headers.entries());
|
|
222
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
223
|
-
if (response.body && contentType.includes("text/event-stream")) {
|
|
224
|
-
socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
|
|
225
|
-
const reader = response.body.getReader();
|
|
226
|
-
while (true) {
|
|
227
|
-
const next = await reader.read();
|
|
228
|
-
if (next.done) {
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
socket.send(JSON.stringify({ type: "http.stream.chunk", id: frame.id, bodyBase64: Buffer.from(next.value).toString("base64") }));
|
|
232
|
-
}
|
|
233
|
-
socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const body = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
237
|
-
socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
|
|
238
|
-
} catch (error) {
|
|
239
|
-
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
240
|
-
socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
|
|
241
|
-
} finally {
|
|
242
|
-
abortControllers.delete(frame.id);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
694
|
// src/cli/index.ts
|
|
247
695
|
var program = new Command();
|
|
248
696
|
var helpLanguage = detectSystemLanguage();
|
|
@@ -276,26 +724,44 @@ program.command("status").option("--json", helpText("status.json")).description(
|
|
|
276
724
|
console.log(t("status.linkId", { value: payload.identity?.linkId ?? t("status.notPaired") }));
|
|
277
725
|
});
|
|
278
726
|
program.command("start").description(helpText("start.description")).action(async () => {
|
|
279
|
-
const [
|
|
727
|
+
const [config, status] = await Promise.all([loadConfig(), getDaemonStatus()]);
|
|
280
728
|
const language = resolveLanguage(config.language);
|
|
281
729
|
const t = translate.bind(null, language);
|
|
282
|
-
if (
|
|
283
|
-
console.log(t("start.
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
const server = await startHttpServer(config.port);
|
|
287
|
-
const relay = identity?.link_id ? connectRelayControl({
|
|
288
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
289
|
-
linkId: identity.link_id,
|
|
290
|
-
localPort: config.port
|
|
291
|
-
}) : null;
|
|
292
|
-
console.log(t("start.listening", { port: config.port }));
|
|
293
|
-
if (identity?.link_id) {
|
|
294
|
-
console.log(t("start.relayConnecting", { linkId: identity.link_id }));
|
|
730
|
+
if (status.running && status.pid) {
|
|
731
|
+
console.log(t("start.alreadyRunning", { pid: status.pid }));
|
|
732
|
+
return;
|
|
295
733
|
}
|
|
734
|
+
const nextStatus = await startDaemonProcess();
|
|
735
|
+
console.log(t("start.backgroundStarted", { pid: nextStatus.pid ?? "unknown" }));
|
|
736
|
+
});
|
|
737
|
+
program.command("stop").description(helpText("stop.description")).action(async () => {
|
|
738
|
+
const config = await loadConfig();
|
|
739
|
+
const language = resolveLanguage(config.language);
|
|
740
|
+
const t = translate.bind(null, language);
|
|
741
|
+
const before = await getDaemonStatus();
|
|
742
|
+
if (!before.running) {
|
|
743
|
+
console.log(t("stop.notRunning"));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
await stopDaemonProcess();
|
|
747
|
+
console.log(t("stop.stopped"));
|
|
748
|
+
});
|
|
749
|
+
program.command("restart").description(helpText("restart.description")).action(async () => {
|
|
750
|
+
const config = await loadConfig();
|
|
751
|
+
const language = resolveLanguage(config.language);
|
|
752
|
+
const t = translate.bind(null, language);
|
|
753
|
+
await stopDaemonProcess();
|
|
754
|
+
const status = await startDaemonProcess();
|
|
755
|
+
console.log(t("start.backgroundStarted", { pid: status.pid ?? "unknown" }));
|
|
756
|
+
});
|
|
757
|
+
program.command("daemon").option("--foreground", "run in foreground").description(helpText("daemon.description")).action(async () => {
|
|
758
|
+
const config = await loadConfig();
|
|
759
|
+
const language = resolveLanguage(config.language);
|
|
760
|
+
const t = translate.bind(null, language);
|
|
761
|
+
const service = await startLinkService({ writePidFile: true });
|
|
762
|
+
console.log(t("daemon.foreground"));
|
|
296
763
|
await waitForShutdown(async () => {
|
|
297
|
-
|
|
298
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
764
|
+
await service.close();
|
|
299
765
|
});
|
|
300
766
|
});
|
|
301
767
|
program.command("pair").description(helpText("pair.description")).action(async () => {
|
|
@@ -308,11 +774,10 @@ program.command("pair").description(helpText("pair.description")).action(async (
|
|
|
308
774
|
console.log(t("pair.relay", { url: config.relayBaseUrl }));
|
|
309
775
|
await ensureIdentity(paths);
|
|
310
776
|
const prepared = await preparePairing(paths);
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
localPort: config.port
|
|
777
|
+
const pairingClaimed = createDeferred();
|
|
778
|
+
const service = await startLinkService({
|
|
779
|
+
paths,
|
|
780
|
+
onPairingClaimed: () => pairingClaimed.resolve()
|
|
316
781
|
});
|
|
317
782
|
const qrValue = JSON.stringify(prepared.qrPayload);
|
|
318
783
|
console.log(t("pair.linkId", { value: prepared.linkId }));
|
|
@@ -321,10 +786,65 @@ program.command("pair").description(helpText("pair.description")).action(async (
|
|
|
321
786
|
console.log(t("pair.scan"));
|
|
322
787
|
qrcode.generate(qrValue, { small: true });
|
|
323
788
|
console.log(t("pair.expires"));
|
|
324
|
-
await
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
789
|
+
const result = await waitForPairingOrShutdown(pairingClaimed.promise);
|
|
790
|
+
await service.close();
|
|
791
|
+
if (result === "claimed") {
|
|
792
|
+
console.log(t("pair.claimed"));
|
|
793
|
+
try {
|
|
794
|
+
const autostart2 = await enableAutostart();
|
|
795
|
+
if (autostart2.supported && autostart2.enabled) {
|
|
796
|
+
console.log(t("autostart.enabled", { method: autostart2.method, path: autostart2.filePath ?? "" }));
|
|
797
|
+
}
|
|
798
|
+
} catch (error) {
|
|
799
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
800
|
+
console.log(t("pair.autostartFailed", { message }));
|
|
801
|
+
}
|
|
802
|
+
const status = await startDaemonProcess(paths);
|
|
803
|
+
console.log(t("start.backgroundStarted", { pid: status.pid ?? "unknown" }));
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
var autostart = program.command("autostart").description(helpText("autostart.description"));
|
|
807
|
+
autostart.command("on").description(helpText("autostart.on.description")).action(async () => {
|
|
808
|
+
const config = await loadConfig();
|
|
809
|
+
const language = resolveLanguage(config.language);
|
|
810
|
+
const t = translate.bind(null, language);
|
|
811
|
+
const status = await enableAutostart();
|
|
812
|
+
if (!status.supported) {
|
|
813
|
+
console.log(t("autostart.unsupported"));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
console.log(t("autostart.enabled", { method: status.method, path: status.filePath ?? "" }));
|
|
817
|
+
});
|
|
818
|
+
autostart.command("off").description(helpText("autostart.off.description")).action(async () => {
|
|
819
|
+
const config = await loadConfig();
|
|
820
|
+
const language = resolveLanguage(config.language);
|
|
821
|
+
const t = translate.bind(null, language);
|
|
822
|
+
await disableAutostart();
|
|
823
|
+
console.log(t("autostart.disabled"));
|
|
824
|
+
});
|
|
825
|
+
autostart.command("status").description(helpText("autostart.status.description")).action(async () => {
|
|
826
|
+
const config = await loadConfig();
|
|
827
|
+
const language = resolveLanguage(config.language);
|
|
828
|
+
const t = translate.bind(null, language);
|
|
829
|
+
const status = await getAutostartStatus();
|
|
830
|
+
if (!status.supported) {
|
|
831
|
+
console.log(t("autostart.unsupported"));
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
console.log(
|
|
835
|
+
t(status.enabled ? "autostart.status.enabled" : "autostart.status.disabled", {
|
|
836
|
+
method: status.method,
|
|
837
|
+
path: status.filePath ?? ""
|
|
838
|
+
})
|
|
839
|
+
);
|
|
840
|
+
});
|
|
841
|
+
program.command("logs").description(helpText("logs.description")).action(async () => {
|
|
842
|
+
const paths = resolveRuntimePaths();
|
|
843
|
+
const config = await loadConfig(paths);
|
|
844
|
+
const language = resolveLanguage(config.language);
|
|
845
|
+
const t = translate.bind(null, language);
|
|
846
|
+
console.log(t("logs.servicePath", { path: getLinkLogFile(paths) }));
|
|
847
|
+
console.log(t("logs.daemonPath", { path: daemonLogFile(paths) }));
|
|
328
848
|
});
|
|
329
849
|
program.command("doctor").description(helpText("doctor.description")).action(async () => {
|
|
330
850
|
const [identity, config] = await Promise.all([ensureIdentity(), loadConfig()]);
|
|
@@ -350,10 +870,6 @@ async function loadCliLanguage() {
|
|
|
350
870
|
const config = await loadConfig();
|
|
351
871
|
return resolveLanguage(config.language);
|
|
352
872
|
}
|
|
353
|
-
async function startHttpServer(port) {
|
|
354
|
-
const app = await createApp();
|
|
355
|
-
return app.listen(port);
|
|
356
|
-
}
|
|
357
873
|
async function waitForShutdown(cleanup) {
|
|
358
874
|
await new Promise((resolve) => {
|
|
359
875
|
const stop = () => resolve();
|
|
@@ -362,4 +878,25 @@ async function waitForShutdown(cleanup) {
|
|
|
362
878
|
});
|
|
363
879
|
await cleanup();
|
|
364
880
|
}
|
|
881
|
+
async function waitForPairingOrShutdown(pairingClaimed) {
|
|
882
|
+
let stop = null;
|
|
883
|
+
const shutdown = new Promise((resolve) => {
|
|
884
|
+
stop = () => resolve("shutdown");
|
|
885
|
+
process.once("SIGINT", stop);
|
|
886
|
+
process.once("SIGTERM", stop);
|
|
887
|
+
});
|
|
888
|
+
const result = await Promise.race([pairingClaimed.then(() => "claimed"), shutdown]);
|
|
889
|
+
if (stop) {
|
|
890
|
+
process.off("SIGINT", stop);
|
|
891
|
+
process.off("SIGTERM", stop);
|
|
892
|
+
}
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
function createDeferred() {
|
|
896
|
+
let resolve;
|
|
897
|
+
const promise = new Promise((innerResolve) => {
|
|
898
|
+
resolve = innerResolve;
|
|
899
|
+
});
|
|
900
|
+
return { promise, resolve };
|
|
901
|
+
}
|
|
365
902
|
//# sourceMappingURL=index.js.map
|