@hermespilot/link 0.5.2 → 0.5.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/dist/cli/index.js CHANGED
@@ -5,1231 +5,54 @@ import {
5
5
  LINK_VERSION,
6
6
  LinkHttpError,
7
7
  clearPairingClaim,
8
- createApp,
8
+ connectRelayControl,
9
9
  createFileLogger,
10
- createRotatingTextLogWriter,
10
+ currentCliScriptPath,
11
+ daemonLogFile,
11
12
  defaultLinkConfig,
12
13
  detectRuntimeEnvironment,
13
- discoverRouteCandidates,
14
14
  ensureHermesApiServerAvailable,
15
15
  ensureHermesApiServerConfig,
16
16
  ensureIdentity,
17
- getDaemonLogFile,
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
- signIdentityPayload,
37
- syncHermesLinkCronDeliveries,
38
- writeJsonFile
39
- } from "../chunk-DZMN5RIV.js";
37
+ startDaemonProcess,
38
+ startLinkService,
39
+ stopDaemonProcess
40
+ } from "../chunk-5JBXQ3VC.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 path5 from "path";
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 as mkdir3, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
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 mkdir3(path2.dirname(definition.filePath), { recursive: true, mode: 448 });
1241
- await writeFile2(definition.filePath, definition.content, { mode: 384 });
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", path2.basename(definition.filePath)]).catch(async () => {
1244
- await rm3(definition.filePath, { force: true }).catch(() => void 0);
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 mkdir3(path2.dirname(fallback.filePath), { recursive: true, mode: 448 });
1247
- await writeFile2(fallback.filePath, fallback.content, { mode: 384 });
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", path2.basename(definition.filePath)]).catch(() => void 0);
79
+ await execFileAsync("systemctl", ["--user", "disable", path.basename(definition.filePath)]).catch(() => void 0);
1257
80
  }
1258
- await rm3(definition.filePath, { force: true }).catch(() => void 0);
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 readFile2(definition.filePath, "utf8").catch(() => null);
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 = path2.join(os.homedir(), "Library", "LaunchAgents", `${MACOS_LABEL}.plist`);
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 = path2.join(os.homedir(), ".config", "systemd", "user", "hermeslink.service");
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 = path2.join(os.homedir(), ".config", "autostart", "hermeslink.desktop");
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 ?? path2.join(os.homedir(), "AppData", "Roaming");
1387
- const filePath = path2.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "HermesLink.cmd");
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
- path2.dirname(process.execPath),
255
+ path.dirname(process.execPath),
1433
256
  ...(process.env.PATH ?? "").split(separator),
1434
- path2.join(os.homedir(), ".local", "bin"),
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 path3 from "path";
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 = path3.join(hermesHome, ".env");
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 as spawn2 } from "child_process";
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 = spawn2(command, args, {
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 path4 from "path";
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" ? path4.win32 : path4.posix;
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(path5.resolve(entry))).href;
1486
+ return moduleUrl === pathToFileURL(realpathSync2(path4.resolve(entry))).href;
2664
1487
  } catch {
2665
- return moduleUrl === pathToFileURL(path5.resolve(entry)).href;
1488
+ return moduleUrl === pathToFileURL(path4.resolve(entry)).href;
2666
1489
  }
2667
1490
  }
2668
1491
  export {