@hermespilot/link 0.5.9 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/{chunk-52SUJB7K.js → chunk-RBMFF32Z.js} +2268 -478
- package/dist/cli/index.js +615 -25
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -13,9 +13,12 @@ import {
|
|
|
13
13
|
detectRuntimeEnvironment,
|
|
14
14
|
ensureHermesApiServerAvailable,
|
|
15
15
|
ensureHermesApiServerConfig,
|
|
16
|
+
ensureHermesLinkSkillInstalledBestEffort,
|
|
16
17
|
ensureIdentity,
|
|
17
18
|
fetchRelayStreamBatchPolicy,
|
|
19
|
+
flushLogFiles,
|
|
18
20
|
getDaemonStatus,
|
|
21
|
+
getGatewayLogFiles,
|
|
19
22
|
getIdentityStatus,
|
|
20
23
|
getLinkLogFile,
|
|
21
24
|
hasActiveDevices,
|
|
@@ -28,6 +31,10 @@ import {
|
|
|
28
31
|
readHermesApiServerConfig,
|
|
29
32
|
readHermesVersion,
|
|
30
33
|
readPairingClaim,
|
|
34
|
+
readRecentGatewayLogEntries,
|
|
35
|
+
readRecentLogEntries,
|
|
36
|
+
readRecentTextLogEntries,
|
|
37
|
+
readRelayStatusSnapshot,
|
|
31
38
|
reportLinkStatusToServer,
|
|
32
39
|
resolveHermesConfigPath,
|
|
33
40
|
resolveHermesProfileDir,
|
|
@@ -37,11 +44,12 @@ import {
|
|
|
37
44
|
startDaemonProcess,
|
|
38
45
|
startLinkService,
|
|
39
46
|
stopDaemonProcess
|
|
40
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-RBMFF32Z.js";
|
|
41
48
|
|
|
42
49
|
// src/cli/index.ts
|
|
43
50
|
import { Command } from "commander";
|
|
44
|
-
import { realpathSync as realpathSync2 } from "fs";
|
|
51
|
+
import { realpathSync as realpathSync2, unwatchFile, watchFile } from "fs";
|
|
52
|
+
import { open, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
45
53
|
import path4 from "path";
|
|
46
54
|
import { createInterface } from "readline/promises";
|
|
47
55
|
import { pathToFileURL } from "url";
|
|
@@ -298,12 +306,41 @@ var messages = {
|
|
|
298
306
|
"status.json": "print machine-readable status",
|
|
299
307
|
"status.runtime": "Runtime: {value}",
|
|
300
308
|
"status.mode": "Mode: {value}",
|
|
309
|
+
"status.daemon": "Service: {value}",
|
|
310
|
+
"status.daemon.runningReachable": "running (PID {pid}, local API reachable)",
|
|
311
|
+
"status.daemon.runningUnreachable": "running (PID {pid}, local API is not responding)",
|
|
312
|
+
"status.daemon.notRunning": "not running",
|
|
301
313
|
"status.port": "Local port: {value}",
|
|
302
|
-
"status.lanHost": "
|
|
314
|
+
"status.lanHost": "LAN access address (manual override): {value}",
|
|
303
315
|
"status.notSet": "not set",
|
|
316
|
+
"status.unknown": "unknown",
|
|
304
317
|
"status.environmentWarning": "Network note: {message}",
|
|
305
318
|
"status.linkId": "Link ID: {value}",
|
|
306
319
|
"status.notPaired": "not paired",
|
|
320
|
+
"status.relay": "Relay: {value}",
|
|
321
|
+
"status.relay.notPaired": "not connected (not paired yet; Relay is disabled until pairing)",
|
|
322
|
+
"status.relay.notConfigured": "not configured",
|
|
323
|
+
"status.relay.serviceStopped": "not connected (background service is not running)",
|
|
324
|
+
"status.relay.serviceUnreachable": "unknown (background service is running but local API is not responding)",
|
|
325
|
+
"status.relay.unknown": "unknown (background service has not reported Relay state yet)",
|
|
326
|
+
"status.relay.state.connecting": "connecting",
|
|
327
|
+
"status.relay.state.connected": "connected",
|
|
328
|
+
"status.relay.state.disconnected": "disconnected",
|
|
329
|
+
"status.relay.state.retrying": "retrying",
|
|
330
|
+
"status.relay.state.cooldown": "cooling down",
|
|
331
|
+
"status.relay.state.failed": "failed",
|
|
332
|
+
"status.relay.stateWithAttempt": "{state}, attempt {attempt}",
|
|
333
|
+
"status.relay.updatedAt": "updated {time}",
|
|
334
|
+
"status.relay.withMessage": "{state} - {message}",
|
|
335
|
+
"status.relay.withUpdatedAt": "{state}; {updatedAt}",
|
|
336
|
+
"status.relay.withMessageAndUpdatedAt": "{state} - {message}; {updatedAt}",
|
|
337
|
+
"status.recentError": "Latest error: {value}",
|
|
338
|
+
"status.recentError.none": "none",
|
|
339
|
+
"status.recentError.format": "{time} [{source}] {message}",
|
|
340
|
+
"status.logSource.service": "service",
|
|
341
|
+
"status.logSource.daemon": "daemon",
|
|
342
|
+
"status.logSource.gateway": "gateway",
|
|
343
|
+
"status.timeUnknown": "time unknown",
|
|
307
344
|
"start.description": "Start Hermes Link daemon",
|
|
308
345
|
"start.backgroundStarted": "Hermes Link is running in the background. PID: {pid}",
|
|
309
346
|
"start.alreadyRunning": "Hermes Link is already running. PID: {pid}",
|
|
@@ -323,18 +360,36 @@ var messages = {
|
|
|
323
360
|
"config.unset.description": "Unset a configuration value",
|
|
324
361
|
"config.unknownKey": "Unknown config key: {key}",
|
|
325
362
|
"config.lanHostInvalid": "lan-host must be a private LAN IPv4 address, such as 192.168.1.23.",
|
|
326
|
-
"config.lanHostSet": "
|
|
327
|
-
"config.lanHostUnset": "
|
|
363
|
+
"config.lanHostSet": "LAN access address override set: {value}",
|
|
364
|
+
"config.lanHostUnset": "LAN access address override cleared.",
|
|
328
365
|
"config.logLevelInvalid": "log-level must be one of: debug, info, warn, error.",
|
|
329
366
|
"config.logLevelSet": "Configured log level: {value}",
|
|
367
|
+
"config.logLevelRestartRequired": "The running daemon keeps its current logger until restart. Run `hermeslink restart` to apply this write level immediately.",
|
|
330
368
|
"config.logLevelUnset": "Configured log level reset to the default: {value}.",
|
|
331
369
|
"config.reported": "Updated HermesPilot Server with the latest LAN address.",
|
|
332
370
|
"config.reportSkippedUnpaired": "Hermes Link is not paired yet. The LAN address will be reported after pairing.",
|
|
333
371
|
"daemon.description": "Run Hermes Link in the foreground",
|
|
334
372
|
"daemon.foreground": "Hermes Link foreground daemon is running. Press Ctrl+C to stop.",
|
|
335
|
-
"logs.description": "Show Hermes Link
|
|
373
|
+
"logs.description": "Show or follow Hermes Link logs",
|
|
374
|
+
"logs.follow": "follow log output",
|
|
375
|
+
"logs.tail": "print the last N lines before exiting or following",
|
|
376
|
+
"logs.serviceOnly": "only read the service JSONL log",
|
|
377
|
+
"logs.daemonOnly": "only read the daemon stdout/stderr log",
|
|
378
|
+
"logs.gatewayOnly": "only read Hermes Gateway logs",
|
|
379
|
+
"logs.all": "read service, daemon, and Gateway logs",
|
|
380
|
+
"logs.level": "minimum level to display: debug, info, warn, or error",
|
|
381
|
+
"logs.debugLevel": "show debug and above",
|
|
382
|
+
"logs.infoLevel": "show info and above",
|
|
383
|
+
"logs.warnLevel": "show warn and above",
|
|
384
|
+
"logs.errorLevel": "show errors only",
|
|
336
385
|
"logs.servicePath": "Service log: {path}",
|
|
337
386
|
"logs.daemonPath": "Daemon stdout/stderr log: {path}",
|
|
387
|
+
"logs.gatewayPath": "Gateway log: {path}",
|
|
388
|
+
"logs.followHint": "Use `hermeslink logs -f` to follow logs, or `hermeslink logs -n 100` to print recent lines.",
|
|
389
|
+
"logs.following": "Following logs. Press Ctrl+C to stop.",
|
|
390
|
+
"logs.flush.description": "Clear Hermes Link log files",
|
|
391
|
+
"logs.flush.all": "clear service, daemon, and Gateway logs",
|
|
392
|
+
"logs.flush.flushed": "Cleared {truncated} active log file(s) and removed {removed} rotated log file(s).",
|
|
338
393
|
"autostart.description": "Manage boot autostart",
|
|
339
394
|
"autostart.on.description": "Enable boot autostart",
|
|
340
395
|
"autostart.off.description": "Disable boot autostart",
|
|
@@ -390,7 +445,7 @@ var messages = {
|
|
|
390
445
|
"doctor.installId": "Install ID: {value}",
|
|
391
446
|
"doctor.linkId": "Link ID: {value}",
|
|
392
447
|
"doctor.notAssigned": "not assigned",
|
|
393
|
-
"doctor.lanHost": "
|
|
448
|
+
"doctor.lanHost": "LAN access address (manual override): {value}",
|
|
394
449
|
"doctor.networkWarning": "Network note: {message}",
|
|
395
450
|
"doctor.hermesCli": "Hermes CLI: {value}",
|
|
396
451
|
"doctor.hermesCliUnavailable": "Hermes CLI is unavailable. Please make sure the `hermes` command can run in this system.",
|
|
@@ -415,12 +470,41 @@ var messages = {
|
|
|
415
470
|
"status.json": "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB\u7684\u72B6\u6001 JSON",
|
|
416
471
|
"status.runtime": "\u8FD0\u884C\u76EE\u5F55\uFF1A{value}",
|
|
417
472
|
"status.mode": "\u6A21\u5F0F\uFF1A{value}",
|
|
473
|
+
"status.daemon": "\u540E\u53F0\u670D\u52A1\uFF1A{value}",
|
|
474
|
+
"status.daemon.runningReachable": "\u8FD0\u884C\u4E2D\uFF08PID\uFF1A{pid}\uFF0C\u672C\u5730 API \u53EF\u8BBF\u95EE\uFF09",
|
|
475
|
+
"status.daemon.runningUnreachable": "\u8FD0\u884C\u4E2D\uFF08PID\uFF1A{pid}\uFF0C\u4F46\u672C\u5730 API \u6682\u4E0D\u53EF\u8BBF\u95EE\uFF09",
|
|
476
|
+
"status.daemon.notRunning": "\u672A\u8FD0\u884C",
|
|
418
477
|
"status.port": "\u672C\u5730\u7AEF\u53E3\uFF1A{value}",
|
|
419
|
-
"status.lanHost": "\
|
|
478
|
+
"status.lanHost": "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\uFF08\u624B\u52A8\u6307\u5B9A\uFF09\uFF1A{value}",
|
|
420
479
|
"status.notSet": "\u672A\u8BBE\u7F6E",
|
|
480
|
+
"status.unknown": "\u672A\u77E5",
|
|
421
481
|
"status.environmentWarning": "\u7F51\u7EDC\u63D0\u793A\uFF1A{message}",
|
|
422
482
|
"status.linkId": "Link ID\uFF1A{value}",
|
|
423
483
|
"status.notPaired": "\u5C1A\u672A\u914D\u5BF9",
|
|
484
|
+
"status.relay": "Relay\uFF1A{value}",
|
|
485
|
+
"status.relay.notPaired": "\u672A\u8FDE\u63A5\uFF08\u5C1A\u672A\u914D\u5BF9\uFF0CRelay \u4F1A\u4FDD\u6301\u5173\u95ED\uFF09",
|
|
486
|
+
"status.relay.notConfigured": "\u672A\u914D\u7F6E",
|
|
487
|
+
"status.relay.serviceStopped": "\u672A\u8FDE\u63A5\uFF08\u540E\u53F0\u670D\u52A1\u672A\u8FD0\u884C\uFF09",
|
|
488
|
+
"status.relay.serviceUnreachable": "\u672A\u77E5\uFF08\u540E\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u4F46\u672C\u5730 API \u6682\u4E0D\u53EF\u8BBF\u95EE\uFF09",
|
|
489
|
+
"status.relay.unknown": "\u672A\u77E5\uFF08\u540E\u53F0\u670D\u52A1\u8FD8\u6CA1\u6709\u4E0A\u62A5 Relay \u72B6\u6001\uFF09",
|
|
490
|
+
"status.relay.state.connecting": "\u8FDE\u63A5\u4E2D",
|
|
491
|
+
"status.relay.state.connected": "\u5DF2\u8FDE\u63A5",
|
|
492
|
+
"status.relay.state.disconnected": "\u672A\u8FDE\u63A5",
|
|
493
|
+
"status.relay.state.retrying": "\u91CD\u8BD5\u4E2D",
|
|
494
|
+
"status.relay.state.cooldown": "\u51B7\u5374\u4E2D",
|
|
495
|
+
"status.relay.state.failed": "\u8FDE\u63A5\u5931\u8D25",
|
|
496
|
+
"status.relay.stateWithAttempt": "{state}\uFF08\u7B2C {attempt} \u6B21\uFF09",
|
|
497
|
+
"status.relay.updatedAt": "\u66F4\u65B0\u4E8E {time}",
|
|
498
|
+
"status.relay.withMessage": "{state} - {message}",
|
|
499
|
+
"status.relay.withUpdatedAt": "{state}\uFF1B{updatedAt}",
|
|
500
|
+
"status.relay.withMessageAndUpdatedAt": "{state} - {message}\uFF1B{updatedAt}",
|
|
501
|
+
"status.recentError": "\u6700\u8FD1\u9519\u8BEF\uFF1A{value}",
|
|
502
|
+
"status.recentError.none": "\u65E0",
|
|
503
|
+
"status.recentError.format": "{time} [{source}] {message}",
|
|
504
|
+
"status.logSource.service": "\u670D\u52A1",
|
|
505
|
+
"status.logSource.daemon": "\u5B88\u62A4\u8FDB\u7A0B",
|
|
506
|
+
"status.logSource.gateway": "Gateway",
|
|
507
|
+
"status.timeUnknown": "\u65F6\u95F4\u672A\u77E5",
|
|
424
508
|
"start.description": "\u542F\u52A8 Hermes Link \u670D\u52A1",
|
|
425
509
|
"start.backgroundStarted": "Hermes Link \u5DF2\u5728\u540E\u53F0\u8FD0\u884C\u3002PID\uFF1A{pid}",
|
|
426
510
|
"start.alreadyRunning": "Hermes Link \u5DF2\u7ECF\u5728\u8FD0\u884C\u3002PID\uFF1A{pid}",
|
|
@@ -440,18 +524,36 @@ var messages = {
|
|
|
440
524
|
"config.unset.description": "\u6E05\u9664\u914D\u7F6E\u9879",
|
|
441
525
|
"config.unknownKey": "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}",
|
|
442
526
|
"config.lanHostInvalid": "lan-host \u5FC5\u987B\u662F\u5C40\u57DF\u7F51 IPv4 \u5730\u5740\uFF0C\u4F8B\u5982 192.168.1.23\u3002",
|
|
443
|
-
"config.lanHostSet": "\u5DF2\
|
|
444
|
-
"config.lanHostUnset": "\u5DF2\u6E05\u9664\u5C40\u57DF\u7F51\
|
|
527
|
+
"config.lanHostSet": "\u5DF2\u8BBE\u7F6E\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\uFF08\u624B\u52A8\u6307\u5B9A\uFF09\uFF1A{value}",
|
|
528
|
+
"config.lanHostUnset": "\u5DF2\u6E05\u9664\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\u624B\u52A8\u914D\u7F6E\u3002",
|
|
445
529
|
"config.logLevelInvalid": "log-level \u53EA\u80FD\u662F\u4EE5\u4E0B\u503C\u4E4B\u4E00\uFF1Adebug\u3001info\u3001warn\u3001error\u3002",
|
|
446
530
|
"config.logLevelSet": "\u5DF2\u914D\u7F6E\u65E5\u5FD7\u7EA7\u522B\uFF1A{value}",
|
|
531
|
+
"config.logLevelRestartRequired": "\u6B63\u5728\u8FD0\u884C\u7684 daemon \u4F1A\u7EE7\u7EED\u4F7F\u7528\u542F\u52A8\u65F6\u7684\u65E5\u5FD7\u5199\u5165\u7EA7\u522B\u3002\u8FD0\u884C `hermeslink restart` \u53EF\u7ACB\u5373\u5E94\u7528\u65B0\u7684\u5199\u5165\u7EA7\u522B\u3002",
|
|
447
532
|
"config.logLevelUnset": "\u5DF2\u5C06\u65E5\u5FD7\u7EA7\u522B\u6062\u590D\u4E3A\u9ED8\u8BA4\u503C\uFF1A{value}\u3002",
|
|
448
533
|
"config.reported": "\u5DF2\u628A\u6700\u65B0\u5C40\u57DF\u7F51\u5730\u5740\u66F4\u65B0\u5230 HermesPilot Server\u3002",
|
|
449
534
|
"config.reportSkippedUnpaired": "Hermes Link \u8FD8\u6CA1\u6709\u914D\u5BF9\uFF0C\u5C40\u57DF\u7F51\u5730\u5740\u4F1A\u5728\u914D\u5BF9\u540E\u4E0A\u62A5\u3002",
|
|
450
535
|
"daemon.description": "\u4EE5\u524D\u53F0\u65B9\u5F0F\u8FD0\u884C Hermes Link",
|
|
451
536
|
"daemon.foreground": "Hermes Link \u524D\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
|
|
452
|
-
"logs.description": "\u663E\u793A Hermes Link \u65E5\u5FD7
|
|
537
|
+
"logs.description": "\u663E\u793A\u6216\u8DDF\u8E2A Hermes Link \u65E5\u5FD7",
|
|
538
|
+
"logs.follow": "\u6301\u7EED\u8DDF\u8E2A\u65E5\u5FD7\u8F93\u51FA",
|
|
539
|
+
"logs.tail": "\u9000\u51FA\u6216\u6301\u7EED\u8DDF\u8E2A\u524D\u5148\u8F93\u51FA\u6700\u8FD1 N \u884C",
|
|
540
|
+
"logs.serviceOnly": "\u53EA\u8BFB\u53D6\u670D\u52A1 JSONL \u65E5\u5FD7",
|
|
541
|
+
"logs.daemonOnly": "\u53EA\u8BFB\u53D6 Daemon \u6807\u51C6\u8F93\u51FA/\u9519\u8BEF\u65E5\u5FD7",
|
|
542
|
+
"logs.gatewayOnly": "\u53EA\u8BFB\u53D6 Hermes Gateway \u65E5\u5FD7",
|
|
543
|
+
"logs.all": "\u8BFB\u53D6\u670D\u52A1\u3001Daemon \u4E0E Gateway \u65E5\u5FD7",
|
|
544
|
+
"logs.level": "\u6700\u4F4E\u663E\u793A\u7EA7\u522B\uFF1Adebug\u3001info\u3001warn \u6216 error",
|
|
545
|
+
"logs.debugLevel": "\u663E\u793A debug \u53CA\u4EE5\u4E0A\u7EA7\u522B",
|
|
546
|
+
"logs.infoLevel": "\u663E\u793A info \u53CA\u4EE5\u4E0A\u7EA7\u522B",
|
|
547
|
+
"logs.warnLevel": "\u663E\u793A warn \u53CA\u4EE5\u4E0A\u7EA7\u522B",
|
|
548
|
+
"logs.errorLevel": "\u53EA\u663E\u793A error \u7EA7\u522B",
|
|
453
549
|
"logs.servicePath": "\u670D\u52A1\u65E5\u5FD7\uFF1A{path}",
|
|
454
550
|
"logs.daemonPath": "Daemon \u6807\u51C6\u8F93\u51FA/\u9519\u8BEF\u65E5\u5FD7\uFF1A{path}",
|
|
551
|
+
"logs.gatewayPath": "Gateway \u65E5\u5FD7\uFF1A{path}",
|
|
552
|
+
"logs.followHint": "\u4F7F\u7528 `hermeslink logs -f` \u6301\u7EED\u8DDF\u8E2A\u65E5\u5FD7\uFF0C\u6216 `hermeslink logs -n 100` \u8F93\u51FA\u6700\u8FD1\u65E5\u5FD7\u3002",
|
|
553
|
+
"logs.following": "\u6B63\u5728\u6301\u7EED\u8DDF\u8E2A\u65E5\u5FD7\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
|
|
554
|
+
"logs.flush.description": "\u6E05\u7A7A Hermes Link \u65E5\u5FD7\u6587\u4EF6",
|
|
555
|
+
"logs.flush.all": "\u6E05\u7A7A\u670D\u52A1\u3001Daemon \u4E0E Gateway \u65E5\u5FD7",
|
|
556
|
+
"logs.flush.flushed": "\u5DF2\u6E05\u7A7A {truncated} \u4E2A\u5F53\u524D\u65E5\u5FD7\u6587\u4EF6\uFF0C\u5E76\u5220\u9664 {removed} \u4E2A\u8F6E\u8F6C\u65E5\u5FD7\u6587\u4EF6\u3002",
|
|
455
557
|
"autostart.description": "\u7BA1\u7406\u5F00\u673A\u81EA\u542F",
|
|
456
558
|
"autostart.on.description": "\u542F\u7528\u5F00\u673A\u81EA\u542F",
|
|
457
559
|
"autostart.off.description": "\u5173\u95ED\u5F00\u673A\u81EA\u542F",
|
|
@@ -507,7 +609,7 @@ var messages = {
|
|
|
507
609
|
"doctor.installId": "Install ID\uFF1A{value}",
|
|
508
610
|
"doctor.linkId": "Link ID\uFF1A{value}",
|
|
509
611
|
"doctor.notAssigned": "\u5C1A\u672A\u5206\u914D",
|
|
510
|
-
"doctor.lanHost": "\
|
|
612
|
+
"doctor.lanHost": "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\uFF08\u624B\u52A8\u6307\u5B9A\uFF09\uFF1A{value}",
|
|
511
613
|
"doctor.networkWarning": "\u7F51\u7EDC\u63D0\u793A\uFF1A{message}",
|
|
512
614
|
"doctor.hermesCli": "Hermes CLI\uFF1A{value}",
|
|
513
615
|
"doctor.hermesCliUnavailable": "Hermes CLI\uFF1A\u4E0D\u53EF\u7528\u3002\u8BF7\u786E\u8BA4\u5F53\u524D\u7CFB\u7EDF\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C `hermes` \u547D\u4EE4\u3002",
|
|
@@ -942,6 +1044,23 @@ program.command("status").option("--json", helpText("status.json")).description(
|
|
|
942
1044
|
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
943
1045
|
const language = resolveLanguage(config.language);
|
|
944
1046
|
const t = translate.bind(null, language);
|
|
1047
|
+
const [daemonStatus, relayStatus, recentError] = await Promise.all([
|
|
1048
|
+
getDaemonStatus(paths),
|
|
1049
|
+
readRelayStatusSnapshot(paths).catch(() => null),
|
|
1050
|
+
readMostRecentError(paths).catch(() => null)
|
|
1051
|
+
]);
|
|
1052
|
+
const serviceProbe = daemonStatus.running ? await probeLocalLinkService({
|
|
1053
|
+
port: config.port,
|
|
1054
|
+
linkId: identity?.link_id ?? void 0,
|
|
1055
|
+
timeoutMs: 500
|
|
1056
|
+
}).catch(() => null) : null;
|
|
1057
|
+
const relay = buildRelayStatusPayload({
|
|
1058
|
+
paired: Boolean(identity?.link_id),
|
|
1059
|
+
relayConfigured: Boolean(config.relayBaseUrl),
|
|
1060
|
+
daemonRunning: daemonStatus.running,
|
|
1061
|
+
localApiReachable: serviceProbe?.reachable ?? false,
|
|
1062
|
+
relayStatus
|
|
1063
|
+
});
|
|
945
1064
|
const payload = {
|
|
946
1065
|
version: LINK_VERSION,
|
|
947
1066
|
runtimeHome: paths.homeDir,
|
|
@@ -949,12 +1068,18 @@ program.command("status").option("--json", helpText("status.json")).description(
|
|
|
949
1068
|
mode: identity?.link_id ? "paired" : "local-only",
|
|
950
1069
|
port: config.port,
|
|
951
1070
|
lanHost: config.lanHost,
|
|
1071
|
+
lanHostMeaning: config.lanHost ? "Manual LAN address override. It is only needed when the phone cannot reach the automatically discovered LAN address." : null,
|
|
1072
|
+
daemon: {
|
|
1073
|
+
running: daemonStatus.running,
|
|
1074
|
+
pid: daemonStatus.pid,
|
|
1075
|
+
localApiReachable: serviceProbe?.reachable ?? false,
|
|
1076
|
+
localApiVersion: serviceProbe?.version ?? null,
|
|
1077
|
+
logFile: daemonStatus.logFile
|
|
1078
|
+
},
|
|
952
1079
|
environment: detectRuntimeEnvironment(),
|
|
953
1080
|
identity: identity ? getIdentityStatus(identity) : null,
|
|
954
|
-
relay
|
|
955
|
-
|
|
956
|
-
connected: false
|
|
957
|
-
}
|
|
1081
|
+
relay,
|
|
1082
|
+
recentError
|
|
958
1083
|
};
|
|
959
1084
|
if (options.json) {
|
|
960
1085
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -963,12 +1088,17 @@ program.command("status").option("--json", helpText("status.json")).description(
|
|
|
963
1088
|
console.log(`Hermes Link ${payload.version}`);
|
|
964
1089
|
console.log(t("status.runtime", { value: payload.runtimeHome }));
|
|
965
1090
|
console.log(t("status.mode", { value: payload.mode }));
|
|
1091
|
+
console.log(t("status.daemon", { value: formatDaemonStatus(payload.daemon, t) }));
|
|
966
1092
|
console.log(t("status.port", { value: payload.port }));
|
|
967
|
-
|
|
1093
|
+
if (payload.lanHost) {
|
|
1094
|
+
console.log(t("status.lanHost", { value: payload.lanHost }));
|
|
1095
|
+
}
|
|
968
1096
|
if (payload.environment.warning) {
|
|
969
1097
|
console.log(t("status.environmentWarning", { message: payload.environment.warning }));
|
|
970
1098
|
}
|
|
971
1099
|
console.log(t("status.linkId", { value: payload.identity?.linkId ?? t("status.notPaired") }));
|
|
1100
|
+
console.log(t("status.relay", { value: formatRelayStatus(payload.relay, t, language) }));
|
|
1101
|
+
console.log(t("status.recentError", { value: formatRecentError(payload.recentError, t, language) }));
|
|
972
1102
|
});
|
|
973
1103
|
var configCommand = program.command("config").description(helpText("config.description"));
|
|
974
1104
|
configCommand.command("set").argument("<key>").argument("<value>").description(helpText("config.set.description")).action(async (key, value) => {
|
|
@@ -994,6 +1124,10 @@ configCommand.command("set").argument("<key>").argument("<value>").description(h
|
|
|
994
1124
|
}
|
|
995
1125
|
const next = await saveConfig({ logLevel }, paths);
|
|
996
1126
|
console.log(t("config.logLevelSet", { value: next.logLevel }));
|
|
1127
|
+
const status = await getDaemonStatus(paths).catch(() => null);
|
|
1128
|
+
if (status?.running) {
|
|
1129
|
+
console.log(t("config.logLevelRestartRequired"));
|
|
1130
|
+
}
|
|
997
1131
|
return;
|
|
998
1132
|
}
|
|
999
1133
|
{
|
|
@@ -1024,15 +1158,24 @@ configCommand.command("unset").argument("<key>").description(helpText("config.un
|
|
|
1024
1158
|
}
|
|
1025
1159
|
});
|
|
1026
1160
|
program.command("start").description(helpText("start.description")).action(async () => {
|
|
1027
|
-
const
|
|
1161
|
+
const paths = resolveRuntimePaths();
|
|
1162
|
+
const [config, status] = await Promise.all([loadConfig(paths), getDaemonStatus(paths)]);
|
|
1028
1163
|
const language = resolveLanguage(config.language);
|
|
1029
1164
|
const t = translate.bind(null, language);
|
|
1030
1165
|
if (status.running && status.pid) {
|
|
1031
1166
|
console.log(t("start.alreadyRunning", { pid: status.pid }));
|
|
1167
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
1168
|
+
paths,
|
|
1169
|
+
source: "cli_start_already_running"
|
|
1170
|
+
});
|
|
1032
1171
|
return;
|
|
1033
1172
|
}
|
|
1034
|
-
const nextStatus = await startDaemonProcess();
|
|
1173
|
+
const nextStatus = await startDaemonProcess(paths);
|
|
1035
1174
|
console.log(t("start.backgroundStarted", { pid: nextStatus.pid ?? "unknown" }));
|
|
1175
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
1176
|
+
paths,
|
|
1177
|
+
source: "cli_start"
|
|
1178
|
+
});
|
|
1036
1179
|
});
|
|
1037
1180
|
program.command("stop").description(helpText("stop.description")).action(async () => {
|
|
1038
1181
|
const config = await loadConfig();
|
|
@@ -1047,12 +1190,17 @@ program.command("stop").description(helpText("stop.description")).action(async (
|
|
|
1047
1190
|
console.log(t("stop.stopped"));
|
|
1048
1191
|
});
|
|
1049
1192
|
program.command("restart").description(helpText("restart.description")).action(async () => {
|
|
1050
|
-
const
|
|
1193
|
+
const paths = resolveRuntimePaths();
|
|
1194
|
+
const config = await loadConfig(paths);
|
|
1051
1195
|
const language = resolveLanguage(config.language);
|
|
1052
1196
|
const t = translate.bind(null, language);
|
|
1053
|
-
await stopDaemonProcess();
|
|
1054
|
-
const status = await startDaemonProcess();
|
|
1197
|
+
await stopDaemonProcess(paths);
|
|
1198
|
+
const status = await startDaemonProcess(paths);
|
|
1055
1199
|
console.log(t("start.backgroundStarted", { pid: status.pid ?? "unknown" }));
|
|
1200
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
1201
|
+
paths,
|
|
1202
|
+
source: "cli_restart"
|
|
1203
|
+
});
|
|
1056
1204
|
});
|
|
1057
1205
|
program.command("deliver").argument("<staging-dir>").description(helpText("deliver.description")).action(async (stagingDir) => {
|
|
1058
1206
|
const paths = resolveRuntimePaths();
|
|
@@ -1125,6 +1273,7 @@ program.command("pair").description(helpText("pair.description")).action(async (
|
|
|
1125
1273
|
relayBaseUrl: prepared.relayBaseUrl,
|
|
1126
1274
|
linkId: prepared.linkId,
|
|
1127
1275
|
localPort: config.port,
|
|
1276
|
+
paths,
|
|
1128
1277
|
initialStreamBatchPolicy: streamBatchPolicy
|
|
1129
1278
|
});
|
|
1130
1279
|
pairingRelayBridge.publishNetworkRoutes(prepared.routes);
|
|
@@ -1151,6 +1300,10 @@ program.command("pair").description(helpText("pair.description")).action(async (
|
|
|
1151
1300
|
await clearPairingClaim(prepared.sessionId, paths);
|
|
1152
1301
|
console.log(t(reusedRunningService ? "pair.claimedRunning" : "pair.claimed"));
|
|
1153
1302
|
printPostPairingNetworkNotice(prepared.routes.environment, config, t);
|
|
1303
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
1304
|
+
paths,
|
|
1305
|
+
source: "cli_pair_claimed"
|
|
1306
|
+
});
|
|
1154
1307
|
try {
|
|
1155
1308
|
if (hadActiveDevices) {
|
|
1156
1309
|
console.log(t("pair.autostartUnchanged"));
|
|
@@ -1224,13 +1377,44 @@ autostart.command("status").description(helpText("autostart.status.description")
|
|
|
1224
1377
|
})
|
|
1225
1378
|
);
|
|
1226
1379
|
});
|
|
1227
|
-
program.command("logs").description(helpText("logs.description")).action(async () => {
|
|
1380
|
+
var logsCommand = program.command("logs").option("-f, --follow", helpText("logs.follow")).option("-n, --tail <lines>", helpText("logs.tail")).option("--service", helpText("logs.serviceOnly")).option("--daemon", helpText("logs.daemonOnly")).option("--gateway", helpText("logs.gatewayOnly")).option("--all", helpText("logs.all")).option("--level <level>", helpText("logs.level")).option("--debug", helpText("logs.debugLevel")).option("-i, --info", helpText("logs.infoLevel")).option("-w, --warn", helpText("logs.warnLevel")).option("-e, --error", helpText("logs.errorLevel")).description(helpText("logs.description")).action(async (options) => {
|
|
1228
1381
|
const paths = resolveRuntimePaths();
|
|
1229
1382
|
const config = await loadConfig(paths);
|
|
1230
1383
|
const language = resolveLanguage(config.language);
|
|
1231
1384
|
const t = translate.bind(null, language);
|
|
1232
|
-
|
|
1233
|
-
|
|
1385
|
+
const sources = resolveLogSources(paths, options);
|
|
1386
|
+
const tailLines = parseLogTailLines(options.tail, options.follow ? 80 : null);
|
|
1387
|
+
const displayLevel = parseLogDisplayLevel(options, config.logLevel);
|
|
1388
|
+
if (!options.follow && tailLines === null) {
|
|
1389
|
+
console.log(t("logs.servicePath", { path: getLinkLogFile(paths) }));
|
|
1390
|
+
console.log(t("logs.daemonPath", { path: daemonLogFile(paths) }));
|
|
1391
|
+
for (const filePath of getGatewayLogFiles(paths)) {
|
|
1392
|
+
console.log(t("logs.gatewayPath", { path: filePath }));
|
|
1393
|
+
}
|
|
1394
|
+
console.log(t("logs.followHint"));
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
await printRecentLogLines(sources, tailLines ?? 80, displayLevel);
|
|
1398
|
+
if (options.follow) {
|
|
1399
|
+
console.log(t("logs.following"));
|
|
1400
|
+
await followLogSources(sources, displayLevel);
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
logsCommand.command("flush").description(helpText("logs.flush.description")).option("--service", helpText("logs.serviceOnly")).option("--daemon", helpText("logs.daemonOnly")).option("--gateway", helpText("logs.gatewayOnly")).option("--all", helpText("logs.flush.all")).action(async (options) => {
|
|
1404
|
+
const paths = resolveRuntimePaths();
|
|
1405
|
+
const config = await loadConfig(paths);
|
|
1406
|
+
const language = resolveLanguage(config.language);
|
|
1407
|
+
const t = translate.bind(null, language);
|
|
1408
|
+
const sources = resolveLogSources(paths, options);
|
|
1409
|
+
const result = await flushLogFiles({
|
|
1410
|
+
filePaths: sources.map((source) => source.filePath)
|
|
1411
|
+
});
|
|
1412
|
+
console.log(
|
|
1413
|
+
t("logs.flush.flushed", {
|
|
1414
|
+
truncated: result.truncated.length,
|
|
1415
|
+
removed: result.removed.length
|
|
1416
|
+
})
|
|
1417
|
+
);
|
|
1234
1418
|
});
|
|
1235
1419
|
program.command("doctor").option("--install", helpText("doctor.installOnly")).description(helpText("doctor.description")).action(async (options) => {
|
|
1236
1420
|
const installInfo = readInstallPathInfo();
|
|
@@ -1285,6 +1469,193 @@ async function loadCliLanguage() {
|
|
|
1285
1469
|
const config = await loadConfig();
|
|
1286
1470
|
return resolveLanguage(config.language);
|
|
1287
1471
|
}
|
|
1472
|
+
function buildRelayStatusPayload(input) {
|
|
1473
|
+
if (!input.paired) {
|
|
1474
|
+
return emptyRelayStatus(input.relayConfigured, "not_paired");
|
|
1475
|
+
}
|
|
1476
|
+
if (!input.relayConfigured) {
|
|
1477
|
+
return emptyRelayStatus(false, "not_configured");
|
|
1478
|
+
}
|
|
1479
|
+
if (!input.daemonRunning) {
|
|
1480
|
+
return emptyRelayStatus(true, "service_stopped");
|
|
1481
|
+
}
|
|
1482
|
+
if (!input.localApiReachable) {
|
|
1483
|
+
return emptyRelayStatus(true, "service_unreachable");
|
|
1484
|
+
}
|
|
1485
|
+
if (!input.relayStatus) {
|
|
1486
|
+
return emptyRelayStatus(true, "unknown");
|
|
1487
|
+
}
|
|
1488
|
+
return {
|
|
1489
|
+
configured: true,
|
|
1490
|
+
connected: input.relayStatus.state === "connected",
|
|
1491
|
+
state: input.relayStatus.state,
|
|
1492
|
+
attempt: input.relayStatus.attempt,
|
|
1493
|
+
message: input.relayStatus.message,
|
|
1494
|
+
updatedAt: input.relayStatus.updatedAt
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function emptyRelayStatus(configured, state) {
|
|
1498
|
+
return {
|
|
1499
|
+
configured,
|
|
1500
|
+
connected: false,
|
|
1501
|
+
state,
|
|
1502
|
+
attempt: null,
|
|
1503
|
+
message: null,
|
|
1504
|
+
updatedAt: null
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
function formatDaemonStatus(daemon, t) {
|
|
1508
|
+
if (!daemon.running) {
|
|
1509
|
+
return t("status.daemon.notRunning");
|
|
1510
|
+
}
|
|
1511
|
+
const pid = daemon.pid ?? t("status.unknown");
|
|
1512
|
+
return t(
|
|
1513
|
+
daemon.localApiReachable ? "status.daemon.runningReachable" : "status.daemon.runningUnreachable",
|
|
1514
|
+
{ pid }
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
function formatRelayStatus(relay, t, language) {
|
|
1518
|
+
if (relay.state === "not_paired") {
|
|
1519
|
+
return t("status.relay.notPaired");
|
|
1520
|
+
}
|
|
1521
|
+
if (relay.state === "not_configured") {
|
|
1522
|
+
return t("status.relay.notConfigured");
|
|
1523
|
+
}
|
|
1524
|
+
if (relay.state === "service_stopped") {
|
|
1525
|
+
return t("status.relay.serviceStopped");
|
|
1526
|
+
}
|
|
1527
|
+
if (relay.state === "service_unreachable") {
|
|
1528
|
+
return t("status.relay.serviceUnreachable");
|
|
1529
|
+
}
|
|
1530
|
+
if (relay.state === "unknown") {
|
|
1531
|
+
return t("status.relay.unknown");
|
|
1532
|
+
}
|
|
1533
|
+
let state = formatRelayState(relay.state, t);
|
|
1534
|
+
if (relay.attempt !== null && relay.attempt > 0 && relay.state !== "connected") {
|
|
1535
|
+
state = t("status.relay.stateWithAttempt", {
|
|
1536
|
+
state,
|
|
1537
|
+
attempt: relay.attempt
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
const updatedAt = relay.updatedAt ? t("status.relay.updatedAt", {
|
|
1541
|
+
time: formatTimestamp(relay.updatedAt, language)
|
|
1542
|
+
}) : null;
|
|
1543
|
+
if (relay.message && updatedAt) {
|
|
1544
|
+
return t("status.relay.withMessageAndUpdatedAt", {
|
|
1545
|
+
state,
|
|
1546
|
+
message: relay.message,
|
|
1547
|
+
updatedAt
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
if (relay.message) {
|
|
1551
|
+
return t("status.relay.withMessage", {
|
|
1552
|
+
state,
|
|
1553
|
+
message: relay.message
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
if (updatedAt) {
|
|
1557
|
+
return t("status.relay.withUpdatedAt", {
|
|
1558
|
+
state,
|
|
1559
|
+
updatedAt
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
return state;
|
|
1563
|
+
}
|
|
1564
|
+
function formatRelayState(state, t) {
|
|
1565
|
+
switch (state) {
|
|
1566
|
+
case "connecting":
|
|
1567
|
+
return t("status.relay.state.connecting");
|
|
1568
|
+
case "connected":
|
|
1569
|
+
return t("status.relay.state.connected");
|
|
1570
|
+
case "disconnected":
|
|
1571
|
+
return t("status.relay.state.disconnected");
|
|
1572
|
+
case "retrying":
|
|
1573
|
+
return t("status.relay.state.retrying");
|
|
1574
|
+
case "cooldown":
|
|
1575
|
+
return t("status.relay.state.cooldown");
|
|
1576
|
+
case "failed":
|
|
1577
|
+
return t("status.relay.state.failed");
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async function readMostRecentError(paths) {
|
|
1581
|
+
const [service, daemon, gateway] = await Promise.all([
|
|
1582
|
+
readRecentLogEntries({ paths, limit: 120 }).catch(() => []),
|
|
1583
|
+
readRecentTextLogEntries({
|
|
1584
|
+
paths,
|
|
1585
|
+
filePaths: [daemonLogFile(paths)],
|
|
1586
|
+
limit: 120
|
|
1587
|
+
}).catch(() => []),
|
|
1588
|
+
readRecentGatewayLogEntries({ paths, limit: 120 }).catch(() => [])
|
|
1589
|
+
]);
|
|
1590
|
+
const errors = [
|
|
1591
|
+
...service.map((entry) => toStatusRecentError("service", entry)),
|
|
1592
|
+
...daemon.map((entry) => toStatusRecentError("daemon", entry)),
|
|
1593
|
+
...gateway.map((entry) => toStatusRecentError("gateway", entry))
|
|
1594
|
+
].filter((item) => item !== null);
|
|
1595
|
+
errors.sort((left, right) => readTimestampMs(right.ts) - readTimestampMs(left.ts));
|
|
1596
|
+
return errors[0] ?? null;
|
|
1597
|
+
}
|
|
1598
|
+
function toStatusRecentError(source, entry) {
|
|
1599
|
+
if (!isActionableStatusError(source, entry)) {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
const errorDetail = typeof entry.fields?.error === "string" ? entry.fields.error : null;
|
|
1603
|
+
const message = errorDetail && errorDetail !== entry.message ? `${entry.message}: ${errorDetail}` : entry.message;
|
|
1604
|
+
return {
|
|
1605
|
+
source,
|
|
1606
|
+
ts: entry.ts,
|
|
1607
|
+
message
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function isActionableStatusError(source, entry) {
|
|
1611
|
+
if (source === "service") {
|
|
1612
|
+
return entry.level === "error";
|
|
1613
|
+
}
|
|
1614
|
+
if (entry.level !== "error") {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
return /(^|[\s[])(error|fatal)([\]\s:]|$)|\b(traceback|exception|failed|failure|unhandledRejection|uncaughtException)\b/iu.test(
|
|
1618
|
+
entry.message
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
function formatRecentError(error, t, language) {
|
|
1622
|
+
if (!error) {
|
|
1623
|
+
return t("status.recentError.none");
|
|
1624
|
+
}
|
|
1625
|
+
return t("status.recentError.format", {
|
|
1626
|
+
time: error.ts ? formatTimestamp(error.ts, language) : t("status.timeUnknown"),
|
|
1627
|
+
source: formatLogSourceLabel(error.source, t),
|
|
1628
|
+
message: error.message
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
function formatLogSourceLabel(source, t) {
|
|
1632
|
+
switch (source) {
|
|
1633
|
+
case "service":
|
|
1634
|
+
return t("status.logSource.service");
|
|
1635
|
+
case "daemon":
|
|
1636
|
+
return t("status.logSource.daemon");
|
|
1637
|
+
case "gateway":
|
|
1638
|
+
return t("status.logSource.gateway");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function formatTimestamp(value, language) {
|
|
1642
|
+
const timestamp = Date.parse(value);
|
|
1643
|
+
if (!Number.isFinite(timestamp)) {
|
|
1644
|
+
return value;
|
|
1645
|
+
}
|
|
1646
|
+
return new Intl.DateTimeFormat(language === "zh-CN" ? "zh-CN" : "en-US", {
|
|
1647
|
+
dateStyle: "short",
|
|
1648
|
+
timeStyle: "medium",
|
|
1649
|
+
hour12: false
|
|
1650
|
+
}).format(new Date(timestamp));
|
|
1651
|
+
}
|
|
1652
|
+
function readTimestampMs(value) {
|
|
1653
|
+
if (!value) {
|
|
1654
|
+
return 0;
|
|
1655
|
+
}
|
|
1656
|
+
const timestamp = Date.parse(value);
|
|
1657
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
1658
|
+
}
|
|
1288
1659
|
function formatHermesVersion(version) {
|
|
1289
1660
|
return version.version ?? version.raw;
|
|
1290
1661
|
}
|
|
@@ -1335,6 +1706,225 @@ function printInstallDiagnostics(info, t, verbose) {
|
|
|
1335
1706
|
}
|
|
1336
1707
|
console.log(t("doctor.installNpxFallback"));
|
|
1337
1708
|
}
|
|
1709
|
+
function resolveLogSources(paths, options) {
|
|
1710
|
+
const hasExplicitSelection = Boolean(
|
|
1711
|
+
options.service || options.daemon || options.gateway || options.all
|
|
1712
|
+
);
|
|
1713
|
+
const includeService = options.all || options.service || !hasExplicitSelection;
|
|
1714
|
+
const includeDaemon = options.all || options.daemon || !hasExplicitSelection;
|
|
1715
|
+
const includeGateway = options.all || options.gateway;
|
|
1716
|
+
const sources = [];
|
|
1717
|
+
if (includeService) {
|
|
1718
|
+
sources.push({ label: "service", filePath: getLinkLogFile(paths), kind: "jsonl" });
|
|
1719
|
+
}
|
|
1720
|
+
if (includeDaemon) {
|
|
1721
|
+
sources.push({ label: "daemon", filePath: daemonLogFile(paths), kind: "text" });
|
|
1722
|
+
}
|
|
1723
|
+
if (includeGateway) {
|
|
1724
|
+
for (const filePath of getGatewayLogFiles(paths)) {
|
|
1725
|
+
sources.push({ label: "gateway", filePath, kind: "text" });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return sources;
|
|
1729
|
+
}
|
|
1730
|
+
function parseLogDisplayLevel(options, fallback) {
|
|
1731
|
+
const flagLevels = [];
|
|
1732
|
+
if (options.debug) {
|
|
1733
|
+
flagLevels.push("debug");
|
|
1734
|
+
}
|
|
1735
|
+
if (options.info) {
|
|
1736
|
+
flagLevels.push("info");
|
|
1737
|
+
}
|
|
1738
|
+
if (options.warn) {
|
|
1739
|
+
flagLevels.push("warn");
|
|
1740
|
+
}
|
|
1741
|
+
if (options.error) {
|
|
1742
|
+
flagLevels.push("error");
|
|
1743
|
+
}
|
|
1744
|
+
if (flagLevels.length > 1) {
|
|
1745
|
+
throw new Error("Choose only one log level filter");
|
|
1746
|
+
}
|
|
1747
|
+
if (options.level !== void 0 && flagLevels.length > 0) {
|
|
1748
|
+
throw new Error("Use either --level or a level shortcut, not both");
|
|
1749
|
+
}
|
|
1750
|
+
if (flagLevels.length === 1) {
|
|
1751
|
+
return flagLevels[0];
|
|
1752
|
+
}
|
|
1753
|
+
if (options.level === void 0) {
|
|
1754
|
+
return fallback;
|
|
1755
|
+
}
|
|
1756
|
+
const level = parseLogLevel(options.level.trim().toLowerCase());
|
|
1757
|
+
if (!level) {
|
|
1758
|
+
throw new Error("--level must be one of: debug, info, warn, error");
|
|
1759
|
+
}
|
|
1760
|
+
return level;
|
|
1761
|
+
}
|
|
1762
|
+
function parseLogTailLines(raw, defaultValue) {
|
|
1763
|
+
if (raw === void 0) {
|
|
1764
|
+
return defaultValue;
|
|
1765
|
+
}
|
|
1766
|
+
const value = Number.parseInt(raw, 10);
|
|
1767
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
1768
|
+
throw new Error("--tail must be a non-negative integer");
|
|
1769
|
+
}
|
|
1770
|
+
return Math.min(value, 2e3);
|
|
1771
|
+
}
|
|
1772
|
+
async function printRecentLogLines(sources, limit, minLevel) {
|
|
1773
|
+
for (const source of sources) {
|
|
1774
|
+
const lines = await readLastLogLines(source.filePath, limit);
|
|
1775
|
+
for (const line of lines) {
|
|
1776
|
+
printLogLine(source, line, minLevel);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
async function readLastLogLines(filePath, limit) {
|
|
1781
|
+
if (limit <= 0) {
|
|
1782
|
+
return [];
|
|
1783
|
+
}
|
|
1784
|
+
const raw = await readFile2(filePath, "utf8").catch(() => null);
|
|
1785
|
+
if (!raw) {
|
|
1786
|
+
return [];
|
|
1787
|
+
}
|
|
1788
|
+
return raw.split(/\r?\n/u).filter(Boolean).slice(-limit);
|
|
1789
|
+
}
|
|
1790
|
+
async function followLogSources(sources, minLevel) {
|
|
1791
|
+
const followers = await Promise.all(
|
|
1792
|
+
sources.map((source) => createLogFollower(source, minLevel))
|
|
1793
|
+
);
|
|
1794
|
+
await new Promise((resolve) => {
|
|
1795
|
+
const stop = () => {
|
|
1796
|
+
for (const follower of followers) {
|
|
1797
|
+
follower.close();
|
|
1798
|
+
}
|
|
1799
|
+
process.off("SIGINT", stop);
|
|
1800
|
+
process.off("SIGTERM", stop);
|
|
1801
|
+
resolve();
|
|
1802
|
+
};
|
|
1803
|
+
process.once("SIGINT", stop);
|
|
1804
|
+
process.once("SIGTERM", stop);
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
async function createLogFollower(source, minLevel) {
|
|
1808
|
+
let offset = await readFileSize(source.filePath);
|
|
1809
|
+
let pending = "";
|
|
1810
|
+
let closed = false;
|
|
1811
|
+
const poll = async () => {
|
|
1812
|
+
if (closed) {
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const size = await readFileSize(source.filePath);
|
|
1816
|
+
if (size === null) {
|
|
1817
|
+
offset = 0;
|
|
1818
|
+
pending = "";
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (offset === null || size < offset) {
|
|
1822
|
+
offset = 0;
|
|
1823
|
+
pending = "";
|
|
1824
|
+
}
|
|
1825
|
+
if (size <= offset) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const chunk = await readFileRange(source.filePath, offset, size);
|
|
1829
|
+
offset = size;
|
|
1830
|
+
const parts = `${pending}${chunk}`.split(/\r?\n/u);
|
|
1831
|
+
pending = parts.pop() ?? "";
|
|
1832
|
+
for (const line of parts) {
|
|
1833
|
+
if (line) {
|
|
1834
|
+
printLogLine(source, line, minLevel);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
watchFile(source.filePath, { interval: 1e3 }, () => {
|
|
1839
|
+
void poll().catch(() => void 0);
|
|
1840
|
+
});
|
|
1841
|
+
return {
|
|
1842
|
+
close() {
|
|
1843
|
+
closed = true;
|
|
1844
|
+
unwatchFile(source.filePath);
|
|
1845
|
+
if (pending) {
|
|
1846
|
+
printLogLine(source, pending, minLevel);
|
|
1847
|
+
pending = "";
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
async function readFileSize(filePath) {
|
|
1853
|
+
const info = await stat2(filePath).catch(() => null);
|
|
1854
|
+
return info?.isFile() ? info.size : null;
|
|
1855
|
+
}
|
|
1856
|
+
async function readFileRange(filePath, start, end) {
|
|
1857
|
+
const length = Math.max(0, end - start);
|
|
1858
|
+
if (length === 0) {
|
|
1859
|
+
return "";
|
|
1860
|
+
}
|
|
1861
|
+
const handle = await open(filePath, "r");
|
|
1862
|
+
try {
|
|
1863
|
+
const buffer = Buffer.alloc(length);
|
|
1864
|
+
const result = await handle.read(buffer, 0, length, start);
|
|
1865
|
+
return buffer.subarray(0, result.bytesRead).toString("utf8");
|
|
1866
|
+
} finally {
|
|
1867
|
+
await handle.close();
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function printLogLine(source, line, minLevel) {
|
|
1871
|
+
if (!logLineMatchesLevel(source, line, minLevel)) {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
console.log(`[${source.label}] ${formatLogLine(source, line)}`);
|
|
1875
|
+
}
|
|
1876
|
+
function logLineMatchesLevel(source, line, minLevel) {
|
|
1877
|
+
return LOG_LEVEL_PRIORITY[readLogLineLevel(source, line)] >= LOG_LEVEL_PRIORITY[minLevel];
|
|
1878
|
+
}
|
|
1879
|
+
function readLogLineLevel(source, line) {
|
|
1880
|
+
if (source.kind === "jsonl") {
|
|
1881
|
+
try {
|
|
1882
|
+
const entry = JSON.parse(line);
|
|
1883
|
+
if (isLogLevel(entry.level)) {
|
|
1884
|
+
return entry.level;
|
|
1885
|
+
}
|
|
1886
|
+
} catch {
|
|
1887
|
+
return "info";
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return inferTextLogLevel(line);
|
|
1891
|
+
}
|
|
1892
|
+
function formatLogLine(source, line) {
|
|
1893
|
+
if (source.kind !== "jsonl") {
|
|
1894
|
+
return line;
|
|
1895
|
+
}
|
|
1896
|
+
try {
|
|
1897
|
+
const entry = JSON.parse(line);
|
|
1898
|
+
const ts = typeof entry.ts === "string" ? entry.ts : "";
|
|
1899
|
+
const level = typeof entry.level === "string" ? entry.level.toUpperCase() : "INFO";
|
|
1900
|
+
const message = typeof entry.message === "string" ? entry.message : line;
|
|
1901
|
+
const fields = entry.fields && typeof entry.fields === "object" ? ` ${JSON.stringify(entry.fields)}` : "";
|
|
1902
|
+
return `${ts} ${level} ${message}${fields}`.trim();
|
|
1903
|
+
} catch {
|
|
1904
|
+
return line;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
var LOG_LEVEL_PRIORITY = {
|
|
1908
|
+
debug: 10,
|
|
1909
|
+
info: 20,
|
|
1910
|
+
warn: 30,
|
|
1911
|
+
error: 40
|
|
1912
|
+
};
|
|
1913
|
+
function isLogLevel(value) {
|
|
1914
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
1915
|
+
}
|
|
1916
|
+
function inferTextLogLevel(line) {
|
|
1917
|
+
if (/\b(error|fatal|traceback|failed|failure)\b/iu.test(line)) {
|
|
1918
|
+
return "error";
|
|
1919
|
+
}
|
|
1920
|
+
if (/\b(warn|warning)\b/iu.test(line)) {
|
|
1921
|
+
return "warn";
|
|
1922
|
+
}
|
|
1923
|
+
if (/\b(debug|trace)\b/iu.test(line)) {
|
|
1924
|
+
return "debug";
|
|
1925
|
+
}
|
|
1926
|
+
return "info";
|
|
1927
|
+
}
|
|
1338
1928
|
function pairingPreflightProgressKey(stage) {
|
|
1339
1929
|
switch (stage) {
|
|
1340
1930
|
case "hermes_files":
|