@hermespilot/link 0.5.2 → 0.5.4
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-DZMN5RIV.js → chunk-KFEEZ4LM.js} +2334 -830
- package/dist/cli/index.js +39 -1216
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -5,1231 +5,54 @@ import {
|
|
|
5
5
|
LINK_VERSION,
|
|
6
6
|
LinkHttpError,
|
|
7
7
|
clearPairingClaim,
|
|
8
|
-
|
|
8
|
+
connectRelayControl,
|
|
9
9
|
createFileLogger,
|
|
10
|
-
|
|
10
|
+
currentCliScriptPath,
|
|
11
|
+
daemonLogFile,
|
|
11
12
|
defaultLinkConfig,
|
|
12
13
|
detectRuntimeEnvironment,
|
|
13
|
-
discoverRouteCandidates,
|
|
14
14
|
ensureHermesApiServerAvailable,
|
|
15
15
|
ensureHermesApiServerConfig,
|
|
16
16
|
ensureIdentity,
|
|
17
|
-
|
|
17
|
+
fetchRelayStreamBatchPolicy,
|
|
18
|
+
getDaemonStatus,
|
|
18
19
|
getIdentityStatus,
|
|
19
20
|
getLinkLogFile,
|
|
20
21
|
hasActiveDevices,
|
|
21
22
|
loadConfig,
|
|
22
23
|
loadIdentity,
|
|
23
|
-
migrateLinkDatabase,
|
|
24
24
|
normalizeLanHost,
|
|
25
25
|
parseLogLevel,
|
|
26
26
|
preparePairing,
|
|
27
|
+
probeLocalLinkService,
|
|
27
28
|
readHermesApiServerConfig,
|
|
28
29
|
readHermesVersion,
|
|
29
|
-
readJsonFile,
|
|
30
|
-
readLinkSystemInfo,
|
|
31
30
|
readPairingClaim,
|
|
31
|
+
reportLinkStatusToServer,
|
|
32
32
|
resolveHermesConfigPath,
|
|
33
33
|
resolveHermesProfileDir,
|
|
34
34
|
resolveRuntimePaths,
|
|
35
|
+
runDaemonSupervisor,
|
|
35
36
|
saveConfig,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} from "../chunk-
|
|
37
|
+
startDaemonProcess,
|
|
38
|
+
startLinkService,
|
|
39
|
+
stopDaemonProcess
|
|
40
|
+
} from "../chunk-KFEEZ4LM.js";
|
|
40
41
|
|
|
41
42
|
// src/cli/index.ts
|
|
42
43
|
import { Command } from "commander";
|
|
43
44
|
import { realpathSync as realpathSync2 } from "fs";
|
|
44
|
-
import
|
|
45
|
+
import path4 from "path";
|
|
45
46
|
import { createInterface } from "readline/promises";
|
|
46
47
|
import { pathToFileURL } from "url";
|
|
47
48
|
import qrcode from "qrcode-terminal";
|
|
48
49
|
|
|
49
50
|
// src/autostart/autostart.ts
|
|
50
51
|
import { execFile } from "child_process";
|
|
51
|
-
import { mkdir
|
|
52
|
+
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
52
53
|
import os from "os";
|
|
53
|
-
import path2 from "path";
|
|
54
|
-
import { promisify } from "util";
|
|
55
|
-
|
|
56
|
-
// src/daemon/process.ts
|
|
57
|
-
import { spawn } from "child_process";
|
|
58
|
-
import { mkdir as mkdir2, readFile, rm as rm2 } from "fs/promises";
|
|
59
54
|
import path from "path";
|
|
60
|
-
|
|
61
|
-
// src/daemon/service.ts
|
|
62
|
-
import { createServer } from "http";
|
|
63
|
-
import { mkdir, rm, writeFile } from "fs/promises";
|
|
64
|
-
|
|
65
|
-
// src/relay/control-client.ts
|
|
66
|
-
import WebSocket from "ws";
|
|
67
|
-
|
|
68
|
-
// src/relay/stream-policy.ts
|
|
69
|
-
var DEFAULT_RELAY_STREAM_BATCH_POLICY = {
|
|
70
|
-
flushIntervalMs: 50,
|
|
71
|
-
flushBytes: 2 * 1024
|
|
72
|
-
};
|
|
73
|
-
var RELAY_STREAM_POLICY_CONSTRAINTS = {
|
|
74
|
-
flushIntervalMs: {
|
|
75
|
-
min: 50,
|
|
76
|
-
max: 1e3
|
|
77
|
-
},
|
|
78
|
-
flushBytes: {
|
|
79
|
-
min: 1024,
|
|
80
|
-
max: 64 * 1024
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
async function fetchRelayStreamBatchPolicy(serverBaseUrl, options = {}) {
|
|
84
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
85
|
-
const controller = new AbortController();
|
|
86
|
-
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
|
|
87
|
-
timeout.unref?.();
|
|
88
|
-
try {
|
|
89
|
-
const response = await fetchImpl(`${serverBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/stream-policy`, {
|
|
90
|
-
headers: {
|
|
91
|
-
accept: "application/json"
|
|
92
|
-
},
|
|
93
|
-
signal: controller.signal
|
|
94
|
-
});
|
|
95
|
-
if (!response.ok) {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
const payload = await response.json().catch(() => null);
|
|
99
|
-
return readRelayStreamBatchPolicy(payload);
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
} finally {
|
|
103
|
-
clearTimeout(timeout);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function readRelayStreamBatchPolicy(input) {
|
|
107
|
-
const record = readRecord(input);
|
|
108
|
-
const body = readRecord(record?.policy) ?? readRecord(record?.stream_batching) ?? record;
|
|
109
|
-
return normalizeRelayStreamBatchPolicy(body);
|
|
110
|
-
}
|
|
111
|
-
function normalizeRelayStreamBatchPolicy(input) {
|
|
112
|
-
const record = readRecord(input);
|
|
113
|
-
if (!record) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
const flushIntervalMs = readInteger(record.flushIntervalMs ?? record.flush_interval_ms);
|
|
117
|
-
const flushBytes = readInteger(record.flushBytes ?? record.flush_bytes);
|
|
118
|
-
if (flushIntervalMs === null || flushBytes === null || flushIntervalMs < RELAY_STREAM_POLICY_CONSTRAINTS.flushIntervalMs.min || flushIntervalMs > RELAY_STREAM_POLICY_CONSTRAINTS.flushIntervalMs.max || flushBytes < RELAY_STREAM_POLICY_CONSTRAINTS.flushBytes.min || flushBytes > RELAY_STREAM_POLICY_CONSTRAINTS.flushBytes.max) {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
flushIntervalMs,
|
|
123
|
-
flushBytes
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function readRecord(value) {
|
|
127
|
-
return value && typeof value === "object" ? value : null;
|
|
128
|
-
}
|
|
129
|
-
function readInteger(value) {
|
|
130
|
-
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// src/relay/control-client.ts
|
|
134
|
-
function connectRelayControl(options) {
|
|
135
|
-
const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
|
|
136
|
-
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
137
|
-
wsUrl.searchParams.set("link_id", options.linkId);
|
|
138
|
-
const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
139
|
-
const backoffBaseMs = options.backoffBaseMs ?? 1e3;
|
|
140
|
-
const backoffMaxMs = options.backoffMaxMs ?? 3e4;
|
|
141
|
-
let reconnectAttempts = 0;
|
|
142
|
-
let closedByUser = false;
|
|
143
|
-
let socket = null;
|
|
144
|
-
let retryTimer = null;
|
|
145
|
-
let abortControllers = /* @__PURE__ */ new Map();
|
|
146
|
-
let fatalRelayRejection = null;
|
|
147
|
-
let latestNetworkRoutes = null;
|
|
148
|
-
const streamBatchPolicy = {
|
|
149
|
-
current: options.initialStreamBatchPolicy ?? DEFAULT_RELAY_STREAM_BATCH_POLICY,
|
|
150
|
-
onUpdate: options.onStreamBatchPolicy
|
|
151
|
-
};
|
|
152
|
-
const connect = () => {
|
|
153
|
-
options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
|
|
154
|
-
fatalRelayRejection = null;
|
|
155
|
-
socket = new WebSocket(wsUrl, {
|
|
156
|
-
headers: {
|
|
157
|
-
"x-hermes-link-version": LINK_VERSION
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
socket.on("open", () => {
|
|
161
|
-
reconnectAttempts = 0;
|
|
162
|
-
options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
|
|
163
|
-
const currentSocket = socket;
|
|
164
|
-
if (currentSocket && latestNetworkRoutes) {
|
|
165
|
-
sendNetworkRoutes(currentSocket, options.linkId, latestNetworkRoutes);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
socket.on("message", (raw) => {
|
|
169
|
-
if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
void handleFrame(socket, String(raw), options.localPort, abortControllers, streamBatchPolicy).catch((error) => {
|
|
173
|
-
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
174
|
-
socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
socket.on("error", (error) => {
|
|
178
|
-
const message = error instanceof Error ? error.message : "Relay websocket error";
|
|
179
|
-
fatalRelayRejection = resolveFatalRelayRejection(message);
|
|
180
|
-
options.onStatus?.({
|
|
181
|
-
state: "disconnected",
|
|
182
|
-
attempt: reconnectAttempts,
|
|
183
|
-
message: fatalRelayRejection ?? message
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
socket.on("close", () => {
|
|
187
|
-
abortAll(abortControllers);
|
|
188
|
-
abortControllers = /* @__PURE__ */ new Map();
|
|
189
|
-
if (fatalRelayRejection) {
|
|
190
|
-
options.onStatus?.({
|
|
191
|
-
state: "failed",
|
|
192
|
-
attempt: reconnectAttempts,
|
|
193
|
-
message: fatalRelayRejection
|
|
194
|
-
});
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (closedByUser) {
|
|
198
|
-
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
202
|
-
options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
reconnectAttempts += 1;
|
|
206
|
-
const delay = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
|
|
207
|
-
options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay}ms` });
|
|
208
|
-
retryTimer = setTimeout(connect, delay);
|
|
209
|
-
retryTimer.unref?.();
|
|
210
|
-
});
|
|
211
|
-
};
|
|
212
|
-
connect();
|
|
213
|
-
return {
|
|
214
|
-
publishNetworkRoutes(routes) {
|
|
215
|
-
latestNetworkRoutes = routes;
|
|
216
|
-
if (socket?.readyState === WebSocket.OPEN) {
|
|
217
|
-
sendNetworkRoutes(socket, options.linkId, routes);
|
|
218
|
-
}
|
|
219
|
-
},
|
|
220
|
-
updateStreamBatchPolicy(policy) {
|
|
221
|
-
streamBatchPolicy.current = policy;
|
|
222
|
-
streamBatchPolicy.onUpdate?.(policy);
|
|
223
|
-
},
|
|
224
|
-
close() {
|
|
225
|
-
closedByUser = true;
|
|
226
|
-
if (retryTimer) {
|
|
227
|
-
clearTimeout(retryTimer);
|
|
228
|
-
retryTimer = null;
|
|
229
|
-
}
|
|
230
|
-
abortAll(abortControllers);
|
|
231
|
-
socket?.terminate();
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function sendNetworkRoutes(socket, linkId, routes) {
|
|
236
|
-
socket.send(JSON.stringify({
|
|
237
|
-
type: "network.routes",
|
|
238
|
-
id: `routes_${Date.now().toString(36)}`,
|
|
239
|
-
payload: {
|
|
240
|
-
link_id: linkId,
|
|
241
|
-
lan_ips: routes.lanIps,
|
|
242
|
-
public_ipv4s: routes.publicIpv4s,
|
|
243
|
-
public_ipv6s: routes.publicIpv6s,
|
|
244
|
-
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
245
|
-
}
|
|
246
|
-
}));
|
|
247
|
-
}
|
|
248
|
-
function resolveFatalRelayRejection(message) {
|
|
249
|
-
if (!/Unexpected server response:\s*(400|401|403|426)\b/u.test(message)) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
return "Relay refused the Hermes Link connection. Check Link version and pairing state before retrying.";
|
|
253
|
-
}
|
|
254
|
-
function abortAll(abortControllers) {
|
|
255
|
-
for (const controller of abortControllers.values()) {
|
|
256
|
-
controller.abort();
|
|
257
|
-
}
|
|
258
|
-
abortControllers.clear();
|
|
259
|
-
}
|
|
260
|
-
function computeBackoffMs(attempt, baseMs, maxMs) {
|
|
261
|
-
const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
|
|
262
|
-
const jitter = Math.floor(Math.random() * Math.min(1e3, exponential * 0.2));
|
|
263
|
-
return exponential + jitter;
|
|
264
|
-
}
|
|
265
|
-
async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
|
|
266
|
-
const frame = JSON.parse(raw);
|
|
267
|
-
if (frame.type === "relay.config.update") {
|
|
268
|
-
const nextPolicy = readRelayStreamBatchPolicy(frame.payload);
|
|
269
|
-
if (nextPolicy) {
|
|
270
|
-
streamBatchPolicy.current = nextPolicy;
|
|
271
|
-
streamBatchPolicy.onUpdate?.(nextPolicy);
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (frame.type === "http.cancel") {
|
|
276
|
-
abortControllers.get(frame.id)?.abort();
|
|
277
|
-
abortControllers.delete(frame.id);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (frame.type !== "http.request") {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const abortController = new AbortController();
|
|
284
|
-
abortControllers.set(frame.id, abortController);
|
|
285
|
-
let sseBatcher = null;
|
|
286
|
-
try {
|
|
287
|
-
const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
|
|
288
|
-
method: frame.method,
|
|
289
|
-
headers: frame.headers ?? {},
|
|
290
|
-
body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, "base64") : void 0,
|
|
291
|
-
signal: abortController.signal
|
|
292
|
-
});
|
|
293
|
-
const headers = Object.fromEntries(response.headers.entries());
|
|
294
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
295
|
-
if (response.body && contentType.includes("text/event-stream")) {
|
|
296
|
-
socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
|
|
297
|
-
sseBatcher = createRelayStreamChunkBatcher(socket, frame.id, streamBatchPolicy);
|
|
298
|
-
const reader = response.body.getReader();
|
|
299
|
-
while (true) {
|
|
300
|
-
const next = await reader.read();
|
|
301
|
-
if (next.done) {
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
sseBatcher.push(next.value);
|
|
305
|
-
}
|
|
306
|
-
sseBatcher.flush();
|
|
307
|
-
socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
const body = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
311
|
-
socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
|
|
312
|
-
} catch (error) {
|
|
313
|
-
if (abortController.signal.aborted || isAbortError(error)) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
sseBatcher?.flush();
|
|
317
|
-
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
318
|
-
socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
|
|
319
|
-
} finally {
|
|
320
|
-
sseBatcher?.dispose();
|
|
321
|
-
abortControllers.delete(frame.id);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
function isAbortError(error) {
|
|
325
|
-
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
326
|
-
}
|
|
327
|
-
function createRelayStreamChunkBatcher(socket, id, streamBatchPolicy) {
|
|
328
|
-
let chunks = [];
|
|
329
|
-
let totalBytes = 0;
|
|
330
|
-
let flushTimer = null;
|
|
331
|
-
const clearFlushTimer = () => {
|
|
332
|
-
if (flushTimer == null) {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
clearTimeout(flushTimer);
|
|
336
|
-
flushTimer = null;
|
|
337
|
-
};
|
|
338
|
-
const flush = () => {
|
|
339
|
-
clearFlushTimer();
|
|
340
|
-
if (totalBytes <= 0) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
const bodyBase64 = Buffer.concat(chunks, totalBytes).toString("base64");
|
|
344
|
-
chunks = [];
|
|
345
|
-
totalBytes = 0;
|
|
346
|
-
if (socket.readyState !== WebSocket.OPEN) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
socket.send(JSON.stringify({ type: "http.stream.chunk", id, bodyBase64 }));
|
|
350
|
-
};
|
|
351
|
-
const scheduleFlush = () => {
|
|
352
|
-
if (flushTimer != null) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
flushTimer = setTimeout(() => {
|
|
356
|
-
flushTimer = null;
|
|
357
|
-
flush();
|
|
358
|
-
}, streamBatchPolicy.current.flushIntervalMs);
|
|
359
|
-
flushTimer.unref?.();
|
|
360
|
-
};
|
|
361
|
-
return {
|
|
362
|
-
push(chunk) {
|
|
363
|
-
if (chunk.byteLength <= 0) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const buffer = Buffer.from(chunk);
|
|
367
|
-
chunks.push(buffer);
|
|
368
|
-
totalBytes += buffer.byteLength;
|
|
369
|
-
if (totalBytes >= streamBatchPolicy.current.flushBytes) {
|
|
370
|
-
flush();
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
scheduleFlush();
|
|
374
|
-
},
|
|
375
|
-
flush,
|
|
376
|
-
dispose() {
|
|
377
|
-
clearFlushTimer();
|
|
378
|
-
chunks = [];
|
|
379
|
-
totalBytes = 0;
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// src/link/network-report-state.ts
|
|
385
|
-
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
386
|
-
async function readNetworkReportState(paths) {
|
|
387
|
-
const state = await readLinkState(paths);
|
|
388
|
-
return normalizeNetworkReportState(state.networkReport);
|
|
389
|
-
}
|
|
390
|
-
async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
|
|
391
|
-
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
392
|
-
await updateNetworkReportState(paths, (current) => ({
|
|
393
|
-
...current,
|
|
394
|
-
lastReportedLanIps: snapshot.lanIps,
|
|
395
|
-
lastReportedPublicIpv4s: snapshot.publicIpv4s,
|
|
396
|
-
lastReportedPublicIpv6s: snapshot.publicIpv6s,
|
|
397
|
-
lastReportedAt: reportedAt.toISOString(),
|
|
398
|
-
lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, ...snapshot, success: true } : null
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
401
|
-
async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {}) {
|
|
402
|
-
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
403
|
-
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
404
|
-
const dailyLimit = Math.max(0, Math.floor(options.dailyLimit ?? DEFAULT_AUTO_DAILY_LIMIT));
|
|
405
|
-
let reservation = { allowed: false, reason: "unchanged" };
|
|
406
|
-
await updateNetworkReportState(paths, (current) => {
|
|
407
|
-
if (sameNetworkSnapshot(readReportedSnapshot(current), snapshot)) {
|
|
408
|
-
const lastReportedAt = current.lastReportedAt ? Date.parse(current.lastReportedAt) : Number.NaN;
|
|
409
|
-
const unchangedMinIntervalMs = Math.max(0, Math.floor(options.unchangedMinIntervalMs ?? 0));
|
|
410
|
-
if (!options.force || Number.isFinite(lastReportedAt) && now.getTime() - lastReportedAt < unchangedMinIntervalMs) {
|
|
411
|
-
reservation = { allowed: false, reason: "unchanged" };
|
|
412
|
-
return current;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (current.lastAutoAttempt && !current.lastAutoAttempt.success && sameNetworkSnapshot(readAttemptSnapshot(current.lastAutoAttempt), snapshot)) {
|
|
416
|
-
reservation = { allowed: false, reason: "failed_snapshot_not_retried" };
|
|
417
|
-
return current;
|
|
418
|
-
}
|
|
419
|
-
const quotaDay = formatUtcDay(now);
|
|
420
|
-
const reportsToday = current.autoQuotaDay === quotaDay ? current.autoReportsToday : 0;
|
|
421
|
-
if (reportsToday >= dailyLimit) {
|
|
422
|
-
reservation = { allowed: false, reason: "daily_limit_reached" };
|
|
423
|
-
return current;
|
|
424
|
-
}
|
|
425
|
-
reservation = { allowed: true };
|
|
426
|
-
return {
|
|
427
|
-
...current,
|
|
428
|
-
autoQuotaDay: quotaDay,
|
|
429
|
-
autoReportsToday: reportsToday + 1,
|
|
430
|
-
lastAutoAttempt: {
|
|
431
|
-
...snapshot,
|
|
432
|
-
attemptedAt: now.toISOString(),
|
|
433
|
-
success: false
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
});
|
|
437
|
-
return reservation;
|
|
438
|
-
}
|
|
439
|
-
async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
|
|
440
|
-
const state = await readNetworkReportState(paths);
|
|
441
|
-
return {
|
|
442
|
-
...snapshotInput,
|
|
443
|
-
publicIpv4s: uniqueStrings([
|
|
444
|
-
...snapshotInput.publicIpv4s,
|
|
445
|
-
...state.lastReportedPublicIpv4s
|
|
446
|
-
]).slice(0, 2),
|
|
447
|
-
publicIpv6s: uniqueStrings([
|
|
448
|
-
...snapshotInput.publicIpv6s,
|
|
449
|
-
...state.lastReportedPublicIpv6s
|
|
450
|
-
]).slice(0, 2)
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
async function updateNetworkReportState(paths, update) {
|
|
454
|
-
const state = await readLinkState(paths);
|
|
455
|
-
const next = {
|
|
456
|
-
...state,
|
|
457
|
-
networkReport: update(normalizeNetworkReportState(state.networkReport))
|
|
458
|
-
};
|
|
459
|
-
await writeJsonFile(paths.stateFile, next);
|
|
460
|
-
}
|
|
461
|
-
async function readLinkState(paths) {
|
|
462
|
-
const state = await readJsonFile(paths.stateFile);
|
|
463
|
-
return state && typeof state === "object" ? state : {};
|
|
464
|
-
}
|
|
465
|
-
function normalizeNetworkReportState(value) {
|
|
466
|
-
const record = value && typeof value === "object" ? value : {};
|
|
467
|
-
return {
|
|
468
|
-
lastReportedLanIps: normalizeLanIps(record.lastReportedLanIps),
|
|
469
|
-
lastReportedPublicIpv4s: normalizeLanIps(record.lastReportedPublicIpv4s),
|
|
470
|
-
lastReportedPublicIpv6s: normalizeLanIps(record.lastReportedPublicIpv6s),
|
|
471
|
-
lastReportedAt: typeof record.lastReportedAt === "string" ? record.lastReportedAt : null,
|
|
472
|
-
autoQuotaDay: typeof record.autoQuotaDay === "string" ? record.autoQuotaDay : null,
|
|
473
|
-
autoReportsToday: Number.isFinite(record.autoReportsToday) ? Math.max(0, Math.floor(record.autoReportsToday ?? 0)) : 0,
|
|
474
|
-
lastAutoAttempt: normalizeAttempt(record.lastAutoAttempt)
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
function normalizeAttempt(value) {
|
|
478
|
-
if (!value || typeof value !== "object") {
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
const record = value;
|
|
482
|
-
if (typeof record.attemptedAt !== "string") {
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
return {
|
|
486
|
-
lanIps: normalizeLanIps(record.lanIps),
|
|
487
|
-
publicIpv4s: normalizeLanIps(record.publicIpv4s),
|
|
488
|
-
publicIpv6s: normalizeLanIps(record.publicIpv6s),
|
|
489
|
-
attemptedAt: record.attemptedAt,
|
|
490
|
-
success: record.success === true
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
function normalizeNetworkSnapshot(value) {
|
|
494
|
-
if (Array.isArray(value)) {
|
|
495
|
-
return {
|
|
496
|
-
lanIps: normalizeLanIps(value),
|
|
497
|
-
publicIpv4s: [],
|
|
498
|
-
publicIpv6s: []
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
const record = value && typeof value === "object" ? value : {};
|
|
502
|
-
return {
|
|
503
|
-
lanIps: normalizeLanIps(record.lanIps),
|
|
504
|
-
publicIpv4s: normalizeLanIps(record.publicIpv4s),
|
|
505
|
-
publicIpv6s: normalizeLanIps(record.publicIpv6s)
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
function readReportedSnapshot(state) {
|
|
509
|
-
return {
|
|
510
|
-
lanIps: state.lastReportedLanIps,
|
|
511
|
-
publicIpv4s: state.lastReportedPublicIpv4s,
|
|
512
|
-
publicIpv6s: state.lastReportedPublicIpv6s
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
function readAttemptSnapshot(attempt) {
|
|
516
|
-
return {
|
|
517
|
-
lanIps: attempt.lanIps,
|
|
518
|
-
publicIpv4s: attempt.publicIpv4s,
|
|
519
|
-
publicIpv6s: attempt.publicIpv6s
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
function normalizeLanIps(value) {
|
|
523
|
-
if (!Array.isArray(value)) {
|
|
524
|
-
return [];
|
|
525
|
-
}
|
|
526
|
-
return [
|
|
527
|
-
...new Set(
|
|
528
|
-
value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
|
|
529
|
-
)
|
|
530
|
-
];
|
|
531
|
-
}
|
|
532
|
-
function sameNetworkSnapshot(left, right) {
|
|
533
|
-
return sameStringList(left.lanIps, right.lanIps) && sameStringList(left.publicIpv4s, right.publicIpv4s) && sameStringList(left.publicIpv6s, right.publicIpv6s);
|
|
534
|
-
}
|
|
535
|
-
function sameStringList(left, right) {
|
|
536
|
-
if (left.length !== right.length) {
|
|
537
|
-
return false;
|
|
538
|
-
}
|
|
539
|
-
return left.every((value, index) => value === right[index]);
|
|
540
|
-
}
|
|
541
|
-
function uniqueStrings(values) {
|
|
542
|
-
return [...new Set(values)];
|
|
543
|
-
}
|
|
544
|
-
function formatUtcDay(date) {
|
|
545
|
-
return date.toISOString().slice(0, 10);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// src/link/server-report.ts
|
|
549
|
-
async function reportLinkStatusToServer(options = {}) {
|
|
550
|
-
const paths = options.paths ?? resolveRuntimePaths();
|
|
551
|
-
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
552
|
-
if (!identity?.link_id) {
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
const discoveredRoutes = options.routes ?? await discoverRouteCandidates({
|
|
556
|
-
port: config.port,
|
|
557
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
558
|
-
linkId: identity.link_id,
|
|
559
|
-
installId: identity.install_id,
|
|
560
|
-
publicKeyPem: identity.public_key_pem,
|
|
561
|
-
observePublicRoute: true,
|
|
562
|
-
configuredLanHost: config.lanHost,
|
|
563
|
-
fetchImpl: options.fetchImpl
|
|
564
|
-
});
|
|
565
|
-
const routes = await mergeLastReportedPublicRoutes(paths, discoveredRoutes);
|
|
566
|
-
const systemInfo = readLinkSystemInfo();
|
|
567
|
-
const payload = {
|
|
568
|
-
type: "hermes_link_status_report",
|
|
569
|
-
link_id: identity.link_id,
|
|
570
|
-
install_id: identity.install_id,
|
|
571
|
-
link_version: LINK_VERSION,
|
|
572
|
-
display_name: systemInfo.defaultDisplayName,
|
|
573
|
-
platform: systemInfo.platform,
|
|
574
|
-
hostname: systemInfo.hostname ?? void 0,
|
|
575
|
-
lan_ips: routes.lanIps,
|
|
576
|
-
public_ipv4s: routes.publicIpv4s,
|
|
577
|
-
public_ipv6s: routes.publicIpv6s,
|
|
578
|
-
reported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
579
|
-
};
|
|
580
|
-
const signature = signIdentityPayload(identity, canonicalJson(payload));
|
|
581
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
582
|
-
const response = await fetcher(
|
|
583
|
-
`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/links/${encodeURIComponent(identity.link_id)}/report`,
|
|
584
|
-
{
|
|
585
|
-
method: "POST",
|
|
586
|
-
headers: {
|
|
587
|
-
accept: "application/json",
|
|
588
|
-
"content-type": "application/json"
|
|
589
|
-
},
|
|
590
|
-
body: JSON.stringify({
|
|
591
|
-
...payload,
|
|
592
|
-
public_key_pem: identity.public_key_pem,
|
|
593
|
-
signature
|
|
594
|
-
})
|
|
595
|
-
}
|
|
596
|
-
);
|
|
597
|
-
const body = await response.json().catch(() => null);
|
|
598
|
-
if (!response.ok || !body) {
|
|
599
|
-
const message = readErrorMessage(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
600
|
-
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
601
|
-
}
|
|
602
|
-
await markNetworkStatusReported(paths, routes);
|
|
603
|
-
return body;
|
|
604
|
-
}
|
|
605
|
-
function canonicalJson(value) {
|
|
606
|
-
return JSON.stringify(sortJsonValue(value));
|
|
607
|
-
}
|
|
608
|
-
function sortJsonValue(value) {
|
|
609
|
-
if (Array.isArray(value)) {
|
|
610
|
-
return value.map(sortJsonValue);
|
|
611
|
-
}
|
|
612
|
-
if (value && typeof value === "object") {
|
|
613
|
-
const record = value;
|
|
614
|
-
const sorted = {};
|
|
615
|
-
for (const key of Object.keys(record).sort()) {
|
|
616
|
-
sorted[key] = sortJsonValue(record[key]);
|
|
617
|
-
}
|
|
618
|
-
return sorted;
|
|
619
|
-
}
|
|
620
|
-
return value;
|
|
621
|
-
}
|
|
622
|
-
function readErrorMessage(payload) {
|
|
623
|
-
if (typeof payload !== "object" || payload === null) {
|
|
624
|
-
return null;
|
|
625
|
-
}
|
|
626
|
-
const error = payload.error;
|
|
627
|
-
if (typeof error !== "object" || error === null) {
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
const message = error.message;
|
|
631
|
-
return typeof message === "string" ? message : null;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// src/daemon/lan-ip-monitor.ts
|
|
635
|
-
var DEFAULT_INTERVAL_MS = 5 * 6e4;
|
|
636
|
-
var DEFAULT_DAILY_REPORT_LIMIT = 20;
|
|
637
|
-
var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
|
|
638
|
-
function startLanIpMonitor(options) {
|
|
639
|
-
let running = false;
|
|
640
|
-
let closed = false;
|
|
641
|
-
let current = Promise.resolve();
|
|
642
|
-
const check = async (context = {}) => {
|
|
643
|
-
if (running || closed) {
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
running = true;
|
|
647
|
-
try {
|
|
648
|
-
await checkLanIpChange(options, context);
|
|
649
|
-
} catch (error) {
|
|
650
|
-
void options.logger.warn("lan_ip_monitor_failed", {
|
|
651
|
-
error: error instanceof Error ? error.message : String(error)
|
|
652
|
-
});
|
|
653
|
-
} finally {
|
|
654
|
-
running = false;
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
658
|
-
const timer = setInterval(() => {
|
|
659
|
-
current = check({ observePublicRoute: false });
|
|
660
|
-
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
661
|
-
timer.unref?.();
|
|
662
|
-
return {
|
|
663
|
-
async refreshPublicRoutes() {
|
|
664
|
-
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
665
|
-
await current;
|
|
666
|
-
},
|
|
667
|
-
async close() {
|
|
668
|
-
closed = true;
|
|
669
|
-
clearInterval(timer);
|
|
670
|
-
await current.catch(() => void 0);
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
async function checkLanIpChange(options, context = {}) {
|
|
675
|
-
const [identity, config] = await Promise.all([
|
|
676
|
-
loadIdentity(options.paths),
|
|
677
|
-
loadConfig(options.paths)
|
|
678
|
-
]);
|
|
679
|
-
if (!identity?.link_id) {
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
const discoveredRoutes = await discoverRouteCandidates({
|
|
683
|
-
port: config.port,
|
|
684
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
685
|
-
linkId: identity.link_id,
|
|
686
|
-
installId: identity.install_id,
|
|
687
|
-
publicKeyPem: identity.public_key_pem,
|
|
688
|
-
observePublicRoute: context.observePublicRoute === true,
|
|
689
|
-
configuredLanHost: config.lanHost,
|
|
690
|
-
fetchImpl: options.fetchImpl
|
|
691
|
-
});
|
|
692
|
-
const routes = await mergeLastReportedPublicRoutes(options.paths, discoveredRoutes);
|
|
693
|
-
if (context.publishToRelay) {
|
|
694
|
-
options.onNetworkRoutes?.(routes);
|
|
695
|
-
}
|
|
696
|
-
const reservation = await reserveAutomaticNetworkReport(options.paths, routes, {
|
|
697
|
-
dailyLimit: options.dailyReportLimit ?? DEFAULT_DAILY_REPORT_LIMIT,
|
|
698
|
-
force: context.forceReport === true,
|
|
699
|
-
unchangedMinIntervalMs: options.startupReportMinIntervalMs ?? DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS
|
|
700
|
-
});
|
|
701
|
-
if (!reservation.allowed) {
|
|
702
|
-
const logFields = {
|
|
703
|
-
lan_ips: routes.lanIps,
|
|
704
|
-
public_ipv4s: routes.publicIpv4s,
|
|
705
|
-
public_ipv6s: routes.publicIpv6s,
|
|
706
|
-
reason: reservation.reason
|
|
707
|
-
};
|
|
708
|
-
void options.logger.debug("lan_ip_report_skipped", logFields);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
try {
|
|
712
|
-
const result = await reportLinkStatusToServer({
|
|
713
|
-
paths: options.paths,
|
|
714
|
-
fetchImpl: options.fetchImpl,
|
|
715
|
-
routes
|
|
716
|
-
});
|
|
717
|
-
if (result) {
|
|
718
|
-
options.onNetworkRoutes?.(routes);
|
|
719
|
-
void options.logger.info("lan_ip_change_reported", {
|
|
720
|
-
link_id: result.linkId,
|
|
721
|
-
lan_ips: routes.lanIps,
|
|
722
|
-
public_ipv4s: routes.publicIpv4s,
|
|
723
|
-
public_ipv6s: routes.publicIpv6s
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
} catch (error) {
|
|
727
|
-
void options.logger.warn("lan_ip_change_report_failed", {
|
|
728
|
-
lan_ips: routes.lanIps,
|
|
729
|
-
error: error instanceof Error ? error.message : String(error)
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// src/daemon/scheduler.ts
|
|
735
|
-
function startCronDeliveryScheduler(options) {
|
|
736
|
-
let running = false;
|
|
737
|
-
let current = Promise.resolve();
|
|
738
|
-
const syncCronDeliveries = async () => {
|
|
739
|
-
if (running) {
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
running = true;
|
|
743
|
-
try {
|
|
744
|
-
await syncHermesLinkCronDeliveries(
|
|
745
|
-
options.paths,
|
|
746
|
-
options.conversations,
|
|
747
|
-
options.logger
|
|
748
|
-
);
|
|
749
|
-
} catch (error) {
|
|
750
|
-
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
751
|
-
source: "daemon_scheduler",
|
|
752
|
-
error: error instanceof Error ? error.message : String(error)
|
|
753
|
-
});
|
|
754
|
-
} finally {
|
|
755
|
-
running = false;
|
|
756
|
-
}
|
|
757
|
-
};
|
|
758
|
-
const timer = setInterval(() => {
|
|
759
|
-
current = syncCronDeliveries();
|
|
760
|
-
}, options.intervalMs ?? 3e4);
|
|
761
|
-
timer.unref?.();
|
|
762
|
-
return {
|
|
763
|
-
async close() {
|
|
764
|
-
clearInterval(timer);
|
|
765
|
-
await current.catch(() => void 0);
|
|
766
|
-
}
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
function startHermesSessionSyncScheduler(options) {
|
|
770
|
-
let running = false;
|
|
771
|
-
let current = Promise.resolve();
|
|
772
|
-
const syncSessions = async () => {
|
|
773
|
-
if (running) {
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
running = true;
|
|
777
|
-
try {
|
|
778
|
-
await options.conversations.syncHermesSessions();
|
|
779
|
-
} catch (error) {
|
|
780
|
-
void options.logger.warn("hermes_session_sync_failed", {
|
|
781
|
-
source: "daemon_scheduler",
|
|
782
|
-
error: error instanceof Error ? error.message : String(error)
|
|
783
|
-
});
|
|
784
|
-
} finally {
|
|
785
|
-
running = false;
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
const timer = setInterval(() => {
|
|
789
|
-
current = syncSessions();
|
|
790
|
-
}, options.intervalMs ?? 10 * 60 * 1e3);
|
|
791
|
-
timer.unref?.();
|
|
792
|
-
return {
|
|
793
|
-
async close() {
|
|
794
|
-
clearInterval(timer);
|
|
795
|
-
await current.catch(() => void 0);
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// src/daemon/service.ts
|
|
801
|
-
var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
|
|
802
|
-
var RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS = 15 * 6e4;
|
|
803
|
-
async function startLinkService(options = {}) {
|
|
804
|
-
const paths = options.paths ?? resolveRuntimePaths();
|
|
805
|
-
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
806
|
-
const logger = createFileLogger({ paths, minLevel: config.logLevel });
|
|
807
|
-
await logger.info("service_starting", {
|
|
808
|
-
port: config.port,
|
|
809
|
-
mode: identity?.link_id ? "paired" : "local-only"
|
|
810
|
-
});
|
|
811
|
-
const migration = await migrateLinkDatabase(paths);
|
|
812
|
-
if (migration.appliedVersions.length > 0) {
|
|
813
|
-
await logger.info("database_migrated", {
|
|
814
|
-
database_file: migration.databaseFile,
|
|
815
|
-
applied_versions: migration.appliedVersions,
|
|
816
|
-
current_version: migration.currentVersion
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
const conversations = new ConversationService(paths, logger);
|
|
820
|
-
await conversations.rebuildStatisticsIndex();
|
|
821
|
-
let relay = null;
|
|
822
|
-
let lanIpMonitor = null;
|
|
823
|
-
const loadRelayStreamBatchPolicy = async (source) => {
|
|
824
|
-
const streamBatchPolicy = await fetchRelayStreamBatchPolicy(config.serverBaseUrl);
|
|
825
|
-
if (!streamBatchPolicy) {
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
relay?.updateStreamBatchPolicy(streamBatchPolicy);
|
|
829
|
-
void logger.info("relay_stream_policy_loaded", {
|
|
830
|
-
source,
|
|
831
|
-
flushIntervalMs: streamBatchPolicy.flushIntervalMs,
|
|
832
|
-
flushBytes: streamBatchPolicy.flushBytes
|
|
833
|
-
});
|
|
834
|
-
return streamBatchPolicy;
|
|
835
|
-
};
|
|
836
|
-
let hermesSessionSync = Promise.resolve();
|
|
837
|
-
const triggerHermesSessionSync = () => {
|
|
838
|
-
hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
|
|
839
|
-
void logger.warn("hermes_session_sync_failed", {
|
|
840
|
-
source: "service_startup",
|
|
841
|
-
error: error instanceof Error ? error.message : String(error)
|
|
842
|
-
});
|
|
843
|
-
});
|
|
844
|
-
};
|
|
845
|
-
const app = await createApp({
|
|
846
|
-
paths,
|
|
847
|
-
logger,
|
|
848
|
-
conversations,
|
|
849
|
-
onPairingClaimed: async () => {
|
|
850
|
-
triggerHermesSessionSync();
|
|
851
|
-
void loadRelayStreamBatchPolicy("pairing_claimed");
|
|
852
|
-
await options.onPairingClaimed?.();
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
|
-
const server = createServer(app.callback());
|
|
856
|
-
try {
|
|
857
|
-
await listenServer(server, config.port);
|
|
858
|
-
} catch (error) {
|
|
859
|
-
await logger.error("service_start_failed", {
|
|
860
|
-
port: config.port,
|
|
861
|
-
link_id: identity?.link_id ?? null,
|
|
862
|
-
error: error instanceof Error ? error.message : String(error)
|
|
863
|
-
});
|
|
864
|
-
await logger.flush();
|
|
865
|
-
throw error;
|
|
866
|
-
}
|
|
867
|
-
server.on("error", (error) => {
|
|
868
|
-
void logger.error("service_error", {
|
|
869
|
-
port: config.port,
|
|
870
|
-
link_id: identity?.link_id ?? null,
|
|
871
|
-
error: error.message
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
void logger.info("service_started", {
|
|
875
|
-
port: config.port,
|
|
876
|
-
link_id: identity?.link_id ?? null
|
|
877
|
-
});
|
|
878
|
-
triggerHermesSessionSync();
|
|
879
|
-
const scheduler = startCronDeliveryScheduler({
|
|
880
|
-
paths,
|
|
881
|
-
conversations,
|
|
882
|
-
logger
|
|
883
|
-
});
|
|
884
|
-
const hermesSessionSyncScheduler = startHermesSessionSyncScheduler({
|
|
885
|
-
conversations,
|
|
886
|
-
logger
|
|
887
|
-
});
|
|
888
|
-
let hasSeenRelayConnected = false;
|
|
889
|
-
let lastRelayReconnectPublicRouteRefreshAt = 0;
|
|
890
|
-
if (identity?.link_id) {
|
|
891
|
-
let resolveRelayReady = null;
|
|
892
|
-
const relayReady = new Promise((resolve) => {
|
|
893
|
-
resolveRelayReady = resolve;
|
|
894
|
-
});
|
|
895
|
-
relay = connectRelayControl({
|
|
896
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
897
|
-
linkId: identity.link_id,
|
|
898
|
-
localPort: config.port,
|
|
899
|
-
maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
|
|
900
|
-
backoffBaseMs: 1e3,
|
|
901
|
-
backoffMaxMs: 3e4,
|
|
902
|
-
onStreamBatchPolicy: (policy) => {
|
|
903
|
-
void logger.info("relay_stream_policy_updated", {
|
|
904
|
-
flushIntervalMs: policy.flushIntervalMs,
|
|
905
|
-
flushBytes: policy.flushBytes
|
|
906
|
-
});
|
|
907
|
-
},
|
|
908
|
-
onStatus: (status) => {
|
|
909
|
-
void logger.info("relay_status", status);
|
|
910
|
-
if (status.state === "connected") {
|
|
911
|
-
const now = Date.now();
|
|
912
|
-
if (hasSeenRelayConnected && lanIpMonitor && now - lastRelayReconnectPublicRouteRefreshAt >= RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS) {
|
|
913
|
-
lastRelayReconnectPublicRouteRefreshAt = now;
|
|
914
|
-
void lanIpMonitor.refreshPublicRoutes();
|
|
915
|
-
}
|
|
916
|
-
hasSeenRelayConnected = true;
|
|
917
|
-
resolveRelayReady?.(true);
|
|
918
|
-
resolveRelayReady = null;
|
|
919
|
-
} else if (status.state === "failed") {
|
|
920
|
-
resolveRelayReady?.(false);
|
|
921
|
-
resolveRelayReady = null;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
void loadRelayStreamBatchPolicy("service_startup");
|
|
926
|
-
if (options.waitForRelayReady) {
|
|
927
|
-
await Promise.race([
|
|
928
|
-
relayReady,
|
|
929
|
-
waitForRelayReadyTimeout(options.relayReadyTimeoutMs)
|
|
930
|
-
]);
|
|
931
|
-
resolveRelayReady = null;
|
|
932
|
-
}
|
|
933
|
-
} else {
|
|
934
|
-
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
935
|
-
}
|
|
936
|
-
lanIpMonitor = startLanIpMonitor({
|
|
937
|
-
paths,
|
|
938
|
-
logger,
|
|
939
|
-
intervalMs: options.lanIpMonitorIntervalMs,
|
|
940
|
-
dailyReportLimit: options.lanIpMonitorDailyReportLimit,
|
|
941
|
-
fetchImpl: options.lanIpMonitorFetchImpl,
|
|
942
|
-
onNetworkRoutes: (routes) => {
|
|
943
|
-
relay?.publishNetworkRoutes(routes);
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
if (options.writePidFile) {
|
|
947
|
-
await writePidFile(paths);
|
|
948
|
-
}
|
|
949
|
-
return {
|
|
950
|
-
async close() {
|
|
951
|
-
relay?.close();
|
|
952
|
-
await closeServer(server);
|
|
953
|
-
await Promise.all([
|
|
954
|
-
scheduler.close(),
|
|
955
|
-
hermesSessionSyncScheduler.close(),
|
|
956
|
-
lanIpMonitor?.close(),
|
|
957
|
-
hermesSessionSync.catch(() => void 0)
|
|
958
|
-
]);
|
|
959
|
-
await logger.info("service_stopped");
|
|
960
|
-
await logger.flush();
|
|
961
|
-
if (options.writePidFile) {
|
|
962
|
-
await rm(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
function waitForRelayReadyTimeout(timeoutMs) {
|
|
968
|
-
return new Promise((resolve) => {
|
|
969
|
-
const timer = setTimeout(
|
|
970
|
-
() => resolve(false),
|
|
971
|
-
timeoutMs ?? DEFAULT_RELAY_READY_TIMEOUT_MS
|
|
972
|
-
);
|
|
973
|
-
timer.unref?.();
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
function pidFilePath(paths = resolveRuntimePaths()) {
|
|
977
|
-
return `${paths.runDir}/hermeslink.pid`;
|
|
978
|
-
}
|
|
979
|
-
async function writePidFile(paths) {
|
|
980
|
-
await mkdir(paths.runDir, { recursive: true, mode: 448 });
|
|
981
|
-
await writeFile(pidFilePath(paths), `${process.pid}
|
|
982
|
-
`, { mode: 384 });
|
|
983
|
-
}
|
|
984
|
-
async function closeServer(server) {
|
|
985
|
-
await new Promise((resolve, reject) => {
|
|
986
|
-
let settled = false;
|
|
987
|
-
let forceCloseTimer;
|
|
988
|
-
let timeoutTimer;
|
|
989
|
-
const settle = (error) => {
|
|
990
|
-
if (settled) {
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
settled = true;
|
|
994
|
-
clearTimeout(forceCloseTimer);
|
|
995
|
-
clearTimeout(timeoutTimer);
|
|
996
|
-
if (error) {
|
|
997
|
-
reject(error);
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
resolve();
|
|
1001
|
-
};
|
|
1002
|
-
forceCloseTimer = setTimeout(() => {
|
|
1003
|
-
server.closeIdleConnections?.();
|
|
1004
|
-
server.closeAllConnections?.();
|
|
1005
|
-
}, 250);
|
|
1006
|
-
timeoutTimer = setTimeout(() => {
|
|
1007
|
-
server.closeAllConnections?.();
|
|
1008
|
-
settle();
|
|
1009
|
-
}, 5e3);
|
|
1010
|
-
server.close((error) => {
|
|
1011
|
-
if (error) {
|
|
1012
|
-
settle(error);
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
settle();
|
|
1016
|
-
});
|
|
1017
|
-
server.closeIdleConnections?.();
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
async function listenServer(server, port) {
|
|
1021
|
-
await new Promise((resolve, reject) => {
|
|
1022
|
-
const cleanup = () => {
|
|
1023
|
-
server.off("error", onError);
|
|
1024
|
-
server.off("listening", onListening);
|
|
1025
|
-
};
|
|
1026
|
-
const onError = (error) => {
|
|
1027
|
-
cleanup();
|
|
1028
|
-
reject(error);
|
|
1029
|
-
};
|
|
1030
|
-
const onListening = () => {
|
|
1031
|
-
cleanup();
|
|
1032
|
-
resolve();
|
|
1033
|
-
};
|
|
1034
|
-
server.once("error", onError);
|
|
1035
|
-
server.once("listening", onListening);
|
|
1036
|
-
server.listen(port);
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// src/daemon/process.ts
|
|
1041
|
-
async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
1042
|
-
const config = await loadConfig(paths);
|
|
1043
|
-
let status = await getDaemonStatus(paths);
|
|
1044
|
-
if (status.running) {
|
|
1045
|
-
const probe = await probeLocalLinkService({ port: config.port, timeoutMs: 500 });
|
|
1046
|
-
if (probe.reachable) {
|
|
1047
|
-
return status;
|
|
1048
|
-
}
|
|
1049
|
-
await stopDaemonProcess(paths);
|
|
1050
|
-
status = await getDaemonStatus(paths);
|
|
1051
|
-
if (status.running) {
|
|
1052
|
-
return status;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
await mkdir2(paths.logsDir, { recursive: true, mode: 448 });
|
|
1056
|
-
await mkdir2(paths.runDir, { recursive: true, mode: 448 });
|
|
1057
|
-
const scriptPath = currentCliScriptPath();
|
|
1058
|
-
const child = spawn(process.execPath, [scriptPath, "daemon-supervisor"], {
|
|
1059
|
-
detached: true,
|
|
1060
|
-
stdio: "ignore",
|
|
1061
|
-
env: process.env
|
|
1062
|
-
});
|
|
1063
|
-
child.unref();
|
|
1064
|
-
for (let index = 0; index < 12; index += 1) {
|
|
1065
|
-
await wait(250);
|
|
1066
|
-
const next = await getDaemonStatus(paths);
|
|
1067
|
-
if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
|
|
1068
|
-
return next;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
return await getDaemonStatus(paths);
|
|
1072
|
-
}
|
|
1073
|
-
async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
|
|
1074
|
-
await mkdir2(paths.logsDir, { recursive: true, mode: 448 });
|
|
1075
|
-
const log = createRotatingTextLogWriter({
|
|
1076
|
-
paths,
|
|
1077
|
-
fileName: path.basename(daemonLogFile(paths))
|
|
1078
|
-
});
|
|
1079
|
-
const scriptPath = currentCliScriptPath();
|
|
1080
|
-
const child = spawn(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
1081
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1082
|
-
env: process.env
|
|
1083
|
-
});
|
|
1084
|
-
const write = (chunk) => {
|
|
1085
|
-
void log.write(chunk);
|
|
1086
|
-
};
|
|
1087
|
-
write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor started
|
|
1088
|
-
`);
|
|
1089
|
-
child.stdout?.on("data", write);
|
|
1090
|
-
child.stderr?.on("data", write);
|
|
1091
|
-
const forwardStop = () => {
|
|
1092
|
-
if (child.pid && isProcessAlive(child.pid)) {
|
|
1093
|
-
child.kill("SIGTERM");
|
|
1094
|
-
}
|
|
1095
|
-
};
|
|
1096
|
-
process.once("SIGINT", forwardStop);
|
|
1097
|
-
process.once("SIGTERM", forwardStop);
|
|
1098
|
-
const result = await new Promise((resolve, reject) => {
|
|
1099
|
-
child.once("error", reject);
|
|
1100
|
-
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
1101
|
-
}).catch((error) => {
|
|
1102
|
-
write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor failed: ${error instanceof Error ? error.message : String(error)}
|
|
1103
|
-
`);
|
|
1104
|
-
return { code: 1, signal: null };
|
|
1105
|
-
});
|
|
1106
|
-
process.off("SIGINT", forwardStop);
|
|
1107
|
-
process.off("SIGTERM", forwardStop);
|
|
1108
|
-
write(
|
|
1109
|
-
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${result.code ?? "null"} signal=${result.signal ?? "null"}
|
|
1110
|
-
`
|
|
1111
|
-
);
|
|
1112
|
-
await log.flush();
|
|
1113
|
-
return result.code ?? (result.signal ? 0 : 1);
|
|
1114
|
-
}
|
|
1115
|
-
async function probeLocalLinkService(options) {
|
|
1116
|
-
const unreachable = {
|
|
1117
|
-
reachable: false,
|
|
1118
|
-
reusable: false,
|
|
1119
|
-
linkId: null,
|
|
1120
|
-
version: null
|
|
1121
|
-
};
|
|
1122
|
-
let response;
|
|
1123
|
-
try {
|
|
1124
|
-
response = await fetch(`http://127.0.0.1:${options.port}/api/v1/bootstrap`, {
|
|
1125
|
-
headers: { accept: "application/json" },
|
|
1126
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? 1e3)
|
|
1127
|
-
});
|
|
1128
|
-
} catch {
|
|
1129
|
-
return unreachable;
|
|
1130
|
-
}
|
|
1131
|
-
if (!response.ok) {
|
|
1132
|
-
return unreachable;
|
|
1133
|
-
}
|
|
1134
|
-
const payload = await response.json().catch(() => null);
|
|
1135
|
-
if (!payload || payload.api_version !== 1) {
|
|
1136
|
-
return unreachable;
|
|
1137
|
-
}
|
|
1138
|
-
const linkId = typeof payload.link_id === "string" ? payload.link_id : null;
|
|
1139
|
-
return {
|
|
1140
|
-
reachable: true,
|
|
1141
|
-
reusable: options.linkId ? linkId === options.linkId : true,
|
|
1142
|
-
linkId,
|
|
1143
|
-
version: typeof payload.version === "string" ? payload.version : null
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
1147
|
-
const status = await getDaemonStatus(paths);
|
|
1148
|
-
if (!status.running || !status.pid) {
|
|
1149
|
-
return status;
|
|
1150
|
-
}
|
|
1151
|
-
try {
|
|
1152
|
-
process.kill(status.pid, "SIGTERM");
|
|
1153
|
-
} catch {
|
|
1154
|
-
await rm2(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
1155
|
-
return await getDaemonStatus(paths);
|
|
1156
|
-
}
|
|
1157
|
-
for (let index = 0; index < 20; index += 1) {
|
|
1158
|
-
await wait(250);
|
|
1159
|
-
if (!isProcessAlive(status.pid)) {
|
|
1160
|
-
break;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
if (isProcessAlive(status.pid)) {
|
|
1164
|
-
try {
|
|
1165
|
-
process.kill(status.pid, "SIGKILL");
|
|
1166
|
-
} catch {
|
|
1167
|
-
}
|
|
1168
|
-
for (let index = 0; index < 10; index += 1) {
|
|
1169
|
-
await wait(250);
|
|
1170
|
-
if (!isProcessAlive(status.pid)) {
|
|
1171
|
-
break;
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
if (!isProcessAlive(status.pid) || !await pidBackedServiceIsReachable(paths)) {
|
|
1176
|
-
await rm2(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
1177
|
-
}
|
|
1178
|
-
return await getDaemonStatus(paths);
|
|
1179
|
-
}
|
|
1180
|
-
async function getDaemonStatus(paths = resolveRuntimePaths()) {
|
|
1181
|
-
const pidFile = pidFilePath(paths);
|
|
1182
|
-
const pid = await readPid(pidFile);
|
|
1183
|
-
if (pid && !isProcessAlive(pid)) {
|
|
1184
|
-
await rm2(pidFile, { force: true }).catch(() => void 0);
|
|
1185
|
-
return {
|
|
1186
|
-
running: false,
|
|
1187
|
-
pid: null,
|
|
1188
|
-
pidFile,
|
|
1189
|
-
logFile: daemonLogFile(paths)
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
return {
|
|
1193
|
-
running: Boolean(pid),
|
|
1194
|
-
pid,
|
|
1195
|
-
pidFile,
|
|
1196
|
-
logFile: daemonLogFile(paths)
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
function daemonLogFile(paths = resolveRuntimePaths()) {
|
|
1200
|
-
return getDaemonLogFile(paths);
|
|
1201
|
-
}
|
|
1202
|
-
function currentCliScriptPath() {
|
|
1203
|
-
return process.argv[1];
|
|
1204
|
-
}
|
|
1205
|
-
async function readPid(filePath) {
|
|
1206
|
-
const raw = await readFile(filePath, "utf8").catch(() => null);
|
|
1207
|
-
if (!raw) {
|
|
1208
|
-
return null;
|
|
1209
|
-
}
|
|
1210
|
-
const pid = Number.parseInt(raw.trim(), 10);
|
|
1211
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
1212
|
-
}
|
|
1213
|
-
function isProcessAlive(pid) {
|
|
1214
|
-
try {
|
|
1215
|
-
process.kill(pid, 0);
|
|
1216
|
-
return true;
|
|
1217
|
-
} catch {
|
|
1218
|
-
return false;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
async function pidBackedServiceIsReachable(paths) {
|
|
1222
|
-
const config = await loadConfig(paths).catch(() => null);
|
|
1223
|
-
if (!config) {
|
|
1224
|
-
return false;
|
|
1225
|
-
}
|
|
1226
|
-
return (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable;
|
|
1227
|
-
}
|
|
1228
|
-
function wait(ms) {
|
|
1229
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// src/autostart/autostart.ts
|
|
55
|
+
import { promisify } from "util";
|
|
1233
56
|
var execFileAsync = promisify(execFile);
|
|
1234
57
|
var MACOS_LABEL = "com.hermespilot.link";
|
|
1235
58
|
async function enableAutostart() {
|
|
@@ -1237,14 +60,14 @@ async function enableAutostart() {
|
|
|
1237
60
|
if (!definition) {
|
|
1238
61
|
return unsupportedStatus();
|
|
1239
62
|
}
|
|
1240
|
-
await
|
|
1241
|
-
await
|
|
63
|
+
await mkdir(path.dirname(definition.filePath), { recursive: true, mode: 448 });
|
|
64
|
+
await writeFile(definition.filePath, definition.content, { mode: 384 });
|
|
1242
65
|
if (definition.method === "systemd-user") {
|
|
1243
|
-
await execFileAsync("systemctl", ["--user", "enable",
|
|
1244
|
-
await
|
|
66
|
+
await execFileAsync("systemctl", ["--user", "enable", path.basename(definition.filePath)]).catch(async () => {
|
|
67
|
+
await rm(definition.filePath, { force: true }).catch(() => void 0);
|
|
1245
68
|
const fallback = xdgAutostartDefinition();
|
|
1246
|
-
await
|
|
1247
|
-
await
|
|
69
|
+
await mkdir(path.dirname(fallback.filePath), { recursive: true, mode: 448 });
|
|
70
|
+
await writeFile(fallback.filePath, fallback.content, { mode: 384 });
|
|
1248
71
|
});
|
|
1249
72
|
}
|
|
1250
73
|
return await getAutostartStatus();
|
|
@@ -1253,9 +76,9 @@ async function disableAutostart() {
|
|
|
1253
76
|
const definitions = await allAutostartDefinitions();
|
|
1254
77
|
for (const definition of definitions) {
|
|
1255
78
|
if (definition.method === "systemd-user") {
|
|
1256
|
-
await execFileAsync("systemctl", ["--user", "disable",
|
|
79
|
+
await execFileAsync("systemctl", ["--user", "disable", path.basename(definition.filePath)]).catch(() => void 0);
|
|
1257
80
|
}
|
|
1258
|
-
await
|
|
81
|
+
await rm(definition.filePath, { force: true }).catch(() => void 0);
|
|
1259
82
|
}
|
|
1260
83
|
return await getAutostartStatus();
|
|
1261
84
|
}
|
|
@@ -1265,7 +88,7 @@ async function getAutostartStatus() {
|
|
|
1265
88
|
return unsupportedStatus();
|
|
1266
89
|
}
|
|
1267
90
|
for (const definition of definitions) {
|
|
1268
|
-
const content = await
|
|
91
|
+
const content = await readFile(definition.filePath, "utf8").catch(() => null);
|
|
1269
92
|
if (content !== null) {
|
|
1270
93
|
return {
|
|
1271
94
|
supported: true,
|
|
@@ -1316,7 +139,7 @@ async function hasSystemctlUser() {
|
|
|
1316
139
|
}
|
|
1317
140
|
}
|
|
1318
141
|
function launchdDefinition() {
|
|
1319
|
-
const filePath =
|
|
142
|
+
const filePath = path.join(os.homedir(), "Library", "LaunchAgents", `${MACOS_LABEL}.plist`);
|
|
1320
143
|
const environment = autostartEnvironment();
|
|
1321
144
|
return {
|
|
1322
145
|
method: "launchd",
|
|
@@ -1347,7 +170,7 @@ ${plistEnvironmentEntries(environment)}
|
|
|
1347
170
|
};
|
|
1348
171
|
}
|
|
1349
172
|
function systemdUserDefinition() {
|
|
1350
|
-
const filePath =
|
|
173
|
+
const filePath = path.join(os.homedir(), ".config", "systemd", "user", "hermeslink.service");
|
|
1351
174
|
const environment = autostartEnvironment();
|
|
1352
175
|
return {
|
|
1353
176
|
method: "systemd-user",
|
|
@@ -1368,7 +191,7 @@ WantedBy=default.target
|
|
|
1368
191
|
};
|
|
1369
192
|
}
|
|
1370
193
|
function xdgAutostartDefinition() {
|
|
1371
|
-
const filePath =
|
|
194
|
+
const filePath = path.join(os.homedir(), ".config", "autostart", "hermeslink.desktop");
|
|
1372
195
|
const environment = autostartEnvironment();
|
|
1373
196
|
return {
|
|
1374
197
|
method: "xdg-autostart",
|
|
@@ -1383,8 +206,8 @@ X-GNOME-Autostart-enabled=true
|
|
|
1383
206
|
};
|
|
1384
207
|
}
|
|
1385
208
|
function windowsStartupDefinition() {
|
|
1386
|
-
const appData = process.env.APPDATA ??
|
|
1387
|
-
const filePath =
|
|
209
|
+
const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
|
|
210
|
+
const filePath = path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "HermesLink.cmd");
|
|
1388
211
|
const environment = autostartEnvironment();
|
|
1389
212
|
return {
|
|
1390
213
|
method: "windows-startup",
|
|
@@ -1429,9 +252,9 @@ function autostartEnvironment() {
|
|
|
1429
252
|
function buildAutostartPath() {
|
|
1430
253
|
const separator = process.platform === "win32" ? ";" : ":";
|
|
1431
254
|
const candidates = [
|
|
1432
|
-
|
|
255
|
+
path.dirname(process.execPath),
|
|
1433
256
|
...(process.env.PATH ?? "").split(separator),
|
|
1434
|
-
|
|
257
|
+
path.join(os.homedir(), ".local", "bin"),
|
|
1435
258
|
...process.platform === "win32" ? [] : ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]
|
|
1436
259
|
];
|
|
1437
260
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1794,12 +617,12 @@ function parseLanguage(value) {
|
|
|
1794
617
|
|
|
1795
618
|
// src/pairing/preflight.ts
|
|
1796
619
|
import { access, stat } from "fs/promises";
|
|
1797
|
-
import
|
|
620
|
+
import path2 from "path";
|
|
1798
621
|
async function assertPairingPreflightReady(options = {}) {
|
|
1799
622
|
const profileName = normalizeProfileName(options.profileName);
|
|
1800
623
|
const hermesHome = resolveHermesProfileDir(profileName);
|
|
1801
624
|
const configPath = resolveHermesConfigPath(profileName);
|
|
1802
|
-
const envPath =
|
|
625
|
+
const envPath = path2.join(hermesHome, ".env");
|
|
1803
626
|
const failures = [];
|
|
1804
627
|
options.onProgress?.("hermes_files");
|
|
1805
628
|
if (!await isDirectory(hermesHome)) {
|
|
@@ -1943,7 +766,7 @@ function normalizeProfileName(profileName) {
|
|
|
1943
766
|
}
|
|
1944
767
|
|
|
1945
768
|
// src/runtime/browser.ts
|
|
1946
|
-
import { spawn
|
|
769
|
+
import { spawn } from "child_process";
|
|
1947
770
|
async function openSystemBrowser(url) {
|
|
1948
771
|
const platform = process.platform;
|
|
1949
772
|
if (platform === "win32") {
|
|
@@ -1965,7 +788,7 @@ async function spawnDetached(command, args) {
|
|
|
1965
788
|
resolve(ok);
|
|
1966
789
|
};
|
|
1967
790
|
try {
|
|
1968
|
-
const child =
|
|
791
|
+
const child = spawn(command, args, {
|
|
1969
792
|
detached: true,
|
|
1970
793
|
stdio: "ignore"
|
|
1971
794
|
});
|
|
@@ -1983,7 +806,7 @@ async function spawnDetached(command, args) {
|
|
|
1983
806
|
// src/runtime/install-info.ts
|
|
1984
807
|
import { execFileSync } from "child_process";
|
|
1985
808
|
import { existsSync, realpathSync } from "fs";
|
|
1986
|
-
import
|
|
809
|
+
import path3 from "path";
|
|
1987
810
|
function readInstallPathInfo() {
|
|
1988
811
|
return inspectInstallPath({
|
|
1989
812
|
npmPrefix: resolveGlobalPrefix()
|
|
@@ -2097,7 +920,7 @@ function normalizeComparablePath(value, platform) {
|
|
|
2097
920
|
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
2098
921
|
}
|
|
2099
922
|
function pathForPlatform(platform) {
|
|
2100
|
-
return platform === "win32" ?
|
|
923
|
+
return platform === "win32" ? path3.win32 : path3.posix;
|
|
2101
924
|
}
|
|
2102
925
|
function quoteShellToken(value, platform) {
|
|
2103
926
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) {
|
|
@@ -2660,9 +1483,9 @@ function isCliEntrypoint(entry = process.argv[1], moduleUrl = import.meta.url) {
|
|
|
2660
1483
|
return false;
|
|
2661
1484
|
}
|
|
2662
1485
|
try {
|
|
2663
|
-
return moduleUrl === pathToFileURL(realpathSync2(
|
|
1486
|
+
return moduleUrl === pathToFileURL(realpathSync2(path4.resolve(entry))).href;
|
|
2664
1487
|
} catch {
|
|
2665
|
-
return moduleUrl === pathToFileURL(
|
|
1488
|
+
return moduleUrl === pathToFileURL(path4.resolve(entry)).href;
|
|
2666
1489
|
}
|
|
2667
1490
|
}
|
|
2668
1491
|
export {
|