@hermespilot/link 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/{chunk-UANE2YHT.js → chunk-PRYXZRQI.js} +575 -92
- package/dist/cli/index.js +1 -1
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +67 -0
package/README.md
CHANGED
|
@@ -15,6 +15,14 @@ npm install -g @hermespilot/link
|
|
|
15
15
|
|
|
16
16
|
The package installs the `hermeslink` command. It does not start the service automatically during installation.
|
|
17
17
|
|
|
18
|
+
If your shell cannot find `hermeslink` right after install, your npm global bin directory is probably not on `PATH`. On Unix-like systems you can run:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export PATH="$(npm prefix -g)/bin:$PATH"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can also invoke the installed binary directly with `$(npm prefix -g)/bin/hermeslink`.
|
|
25
|
+
|
|
18
26
|
## Common commands
|
|
19
27
|
|
|
20
28
|
```bash
|
|
@@ -2080,6 +2080,14 @@ async function ensureHermesApiServerConfig(profileName = "default", configPath =
|
|
|
2080
2080
|
}
|
|
2081
2081
|
return ensureHermesApiServerConfigUnlocked(profileName, configPath);
|
|
2082
2082
|
}
|
|
2083
|
+
async function repairHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
|
|
2084
|
+
if (profileName !== "default") {
|
|
2085
|
+
return withProfileApiServerPortAssignmentLock(
|
|
2086
|
+
() => repairHermesApiServerConfigUnlocked(profileName, configPath)
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
return repairHermesApiServerConfigUnlocked(profileName, configPath);
|
|
2090
|
+
}
|
|
2083
2091
|
async function ensureHermesApiServerConfigUnlocked(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
|
|
2084
2092
|
const existingRaw = await readFile2(configPath, "utf8").catch(
|
|
2085
2093
|
(error) => {
|
|
@@ -2096,47 +2104,60 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
|
|
|
2096
2104
|
const extra = ensureRecord(apiServer, "extra");
|
|
2097
2105
|
const configOnly = readApiServerConfig(apiServer);
|
|
2098
2106
|
const envOverrides = await readHermesApiServerEnvOverrides(profileName);
|
|
2099
|
-
const
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2103
|
-
const
|
|
2107
|
+
const configKey = configOnly.key?.trim() ? configOnly.key : null;
|
|
2108
|
+
const envKey = envOverrides.key?.trim() ? envOverrides.key : null;
|
|
2109
|
+
const configHost = configOnly.host?.trim() ? configOnly.host : null;
|
|
2110
|
+
const envHost = envOverrides.host?.trim() ? envOverrides.host : null;
|
|
2111
|
+
const configPort = readApiServerPort(configOnly.port);
|
|
2112
|
+
const envPort = readApiServerPort(envOverrides.port);
|
|
2113
|
+
const desiredHost = configHost ?? envHost ?? DEFAULT_HERMES_API_SERVER_HOST;
|
|
2114
|
+
const desiredPort = await resolveDesiredApiServerPort({
|
|
2115
|
+
profileName,
|
|
2116
|
+
configPort,
|
|
2117
|
+
envPort
|
|
2118
|
+
});
|
|
2119
|
+
const desiredKey = configKey ?? envKey ?? randomBytes(32).toString("base64url");
|
|
2104
2120
|
let changed = false;
|
|
2105
2121
|
let enabledAdded = false;
|
|
2106
2122
|
let hostAdded = false;
|
|
2107
2123
|
let portAdded = false;
|
|
2108
|
-
|
|
2109
|
-
if (!beforeEnabled) {
|
|
2124
|
+
if (apiServer.enabled !== true) {
|
|
2110
2125
|
apiServer.enabled = true;
|
|
2111
2126
|
enabledAdded = true;
|
|
2112
2127
|
changed = true;
|
|
2113
2128
|
}
|
|
2114
|
-
if (
|
|
2115
|
-
extra.host =
|
|
2116
|
-
hostAdded =
|
|
2129
|
+
if (configHost !== desiredHost) {
|
|
2130
|
+
extra.host = desiredHost;
|
|
2131
|
+
hostAdded = !configHost;
|
|
2117
2132
|
changed = true;
|
|
2118
2133
|
}
|
|
2119
|
-
if (
|
|
2120
|
-
|
|
2121
|
-
extra.port = assignedPort;
|
|
2122
|
-
portAdded = true;
|
|
2123
|
-
changed = true;
|
|
2124
|
-
} else if (!beforePort) {
|
|
2125
|
-
assignedPort = DEFAULT_HERMES_API_SERVER_PORT;
|
|
2126
|
-
extra.port = assignedPort;
|
|
2134
|
+
if (configPort !== desiredPort) {
|
|
2135
|
+
extra.port = desiredPort;
|
|
2127
2136
|
portAdded = true;
|
|
2128
2137
|
changed = true;
|
|
2129
2138
|
}
|
|
2130
|
-
if (
|
|
2131
|
-
extra.key =
|
|
2139
|
+
if (configKey !== desiredKey) {
|
|
2140
|
+
extra.key = desiredKey;
|
|
2132
2141
|
changed = true;
|
|
2133
2142
|
}
|
|
2134
|
-
|
|
2143
|
+
const desiredApiServer = readApiServerConfig(apiServer, true);
|
|
2144
|
+
const backupPath = changed && existingRaw ? `${configPath}.bak.${Date.now()}` : null;
|
|
2145
|
+
if (backupPath && existingRaw !== null) {
|
|
2146
|
+
await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
|
|
2147
|
+
metadataSourcePath: configPath
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
if (changed) {
|
|
2151
|
+
document.contents = document.createNode(config);
|
|
2152
|
+
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
2153
|
+
}
|
|
2154
|
+
const envChanged = await writeHermesApiServerEnv(profileName, desiredApiServer);
|
|
2155
|
+
if (!changed && !envChanged) {
|
|
2135
2156
|
return {
|
|
2136
2157
|
configPath,
|
|
2137
2158
|
apiServer: applyEnvOverrides(
|
|
2138
2159
|
readApiServerConfig(apiServer, true),
|
|
2139
|
-
|
|
2160
|
+
await readHermesApiServerEnvOverrides(profileName),
|
|
2140
2161
|
true
|
|
2141
2162
|
),
|
|
2142
2163
|
changed: false,
|
|
@@ -2148,36 +2169,79 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
|
|
|
2148
2169
|
notice: null
|
|
2149
2170
|
};
|
|
2150
2171
|
}
|
|
2151
|
-
const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
|
|
2152
|
-
if (backupPath && existingRaw !== null) {
|
|
2153
|
-
await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
|
|
2154
|
-
metadataSourcePath: configPath
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2157
|
-
document.contents = document.createNode(config);
|
|
2158
|
-
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
2159
2172
|
return {
|
|
2160
2173
|
configPath,
|
|
2161
2174
|
apiServer: applyEnvOverrides(
|
|
2162
2175
|
readApiServerConfig(apiServer, true),
|
|
2163
|
-
|
|
2176
|
+
await readHermesApiServerEnvOverrides(profileName),
|
|
2164
2177
|
true
|
|
2165
2178
|
),
|
|
2166
2179
|
changed: true,
|
|
2167
|
-
keyAdded: !
|
|
2180
|
+
keyAdded: !configKey,
|
|
2168
2181
|
enabledAdded,
|
|
2169
2182
|
hostAdded,
|
|
2170
2183
|
portAdded,
|
|
2171
2184
|
backupPath,
|
|
2172
2185
|
notice: buildNotice({
|
|
2173
|
-
keyAdded: !
|
|
2186
|
+
keyAdded: !configKey,
|
|
2174
2187
|
enabledAdded,
|
|
2175
2188
|
hostAdded,
|
|
2176
2189
|
portAdded,
|
|
2177
|
-
port:
|
|
2190
|
+
port: desiredPort
|
|
2178
2191
|
})
|
|
2179
2192
|
};
|
|
2180
2193
|
}
|
|
2194
|
+
async function repairHermesApiServerConfigUnlocked(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
|
|
2195
|
+
const existingRaw = await readFile2(configPath, "utf8").catch(
|
|
2196
|
+
(error) => {
|
|
2197
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
throw error;
|
|
2201
|
+
}
|
|
2202
|
+
);
|
|
2203
|
+
const document = existingRaw ? YAML.parseDocument(existingRaw) : new YAML.Document({});
|
|
2204
|
+
const config = toRecord(document.toJSON());
|
|
2205
|
+
const platforms = ensureRecord(config, "platforms");
|
|
2206
|
+
const apiServer = ensureRecord(platforms, "api_server");
|
|
2207
|
+
const extra = ensureRecord(apiServer, "extra");
|
|
2208
|
+
const previous = applyEnvOverrides(
|
|
2209
|
+
readApiServerConfig(apiServer, true),
|
|
2210
|
+
await readHermesApiServerEnvOverrides(profileName),
|
|
2211
|
+
true
|
|
2212
|
+
);
|
|
2213
|
+
const freshPort = await nextProfileApiServerPort(profileName);
|
|
2214
|
+
const freshKey = randomBytes(32).toString("base64url");
|
|
2215
|
+
apiServer.enabled = true;
|
|
2216
|
+
extra.host = DEFAULT_HERMES_API_SERVER_HOST;
|
|
2217
|
+
extra.port = freshPort;
|
|
2218
|
+
extra.key = freshKey;
|
|
2219
|
+
const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
|
|
2220
|
+
if (backupPath && existingRaw !== null) {
|
|
2221
|
+
await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
|
|
2222
|
+
metadataSourcePath: configPath
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
document.contents = document.createNode(config);
|
|
2226
|
+
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
2227
|
+
const apiServerConfig = readApiServerConfig(apiServer, true);
|
|
2228
|
+
await writeHermesApiServerEnv(profileName, apiServerConfig, { force: true });
|
|
2229
|
+
return {
|
|
2230
|
+
configPath,
|
|
2231
|
+
apiServer: applyEnvOverrides(
|
|
2232
|
+
apiServerConfig,
|
|
2233
|
+
await readHermesApiServerEnvOverrides(profileName),
|
|
2234
|
+
true
|
|
2235
|
+
),
|
|
2236
|
+
changed: true,
|
|
2237
|
+
keyAdded: !previous.key,
|
|
2238
|
+
enabledAdded: previous.enabled !== true,
|
|
2239
|
+
hostAdded: previous.host !== DEFAULT_HERMES_API_SERVER_HOST,
|
|
2240
|
+
portAdded: previous.port !== freshPort,
|
|
2241
|
+
backupPath,
|
|
2242
|
+
notice: "\u5DF2\u4E3A Hermes API Server \u91CD\u65B0\u5206\u914D\u672C\u673A\u7AEF\u53E3\u5E76\u8F6E\u6362 key\uFF0C\u7528\u4E8E\u4FEE\u590D\u65E7 Gateway \u6216 key \u4E0D\u4E00\u81F4\u5BFC\u81F4\u7684 401\u3002"
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2181
2245
|
async function readHermesConfigDocument(configPath) {
|
|
2182
2246
|
const existingRaw = await readFile2(configPath, "utf8").catch(
|
|
2183
2247
|
(error) => {
|
|
@@ -3511,6 +3575,21 @@ function shouldAssignDedicatedProfileApiServerPort(profileName, configuredPort)
|
|
|
3511
3575
|
}
|
|
3512
3576
|
return configuredPort === void 0 || configuredPort === DEFAULT_HERMES_API_SERVER_PORT;
|
|
3513
3577
|
}
|
|
3578
|
+
function readApiServerPort(value) {
|
|
3579
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
3580
|
+
}
|
|
3581
|
+
async function resolveDesiredApiServerPort(input) {
|
|
3582
|
+
if (shouldAssignDedicatedProfileApiServerPort(
|
|
3583
|
+
input.profileName,
|
|
3584
|
+
input.configPort ?? void 0
|
|
3585
|
+
)) {
|
|
3586
|
+
if (input.profileName !== "default" && input.envPort !== null && input.envPort !== DEFAULT_HERMES_API_SERVER_PORT) {
|
|
3587
|
+
return input.envPort;
|
|
3588
|
+
}
|
|
3589
|
+
return nextProfileApiServerPort(input.profileName);
|
|
3590
|
+
}
|
|
3591
|
+
return input.configPort ?? input.envPort ?? DEFAULT_HERMES_API_SERVER_PORT;
|
|
3592
|
+
}
|
|
3514
3593
|
async function nextProfileApiServerPort(profileName) {
|
|
3515
3594
|
const usedPorts = await readConfiguredApiServerPorts(profileName);
|
|
3516
3595
|
for (let port = PROFILE_API_SERVER_PORT_START; port <= PROFILE_API_SERVER_PORT_END; port += 1) {
|
|
@@ -3670,14 +3749,82 @@ async function writeHermesEnvValue(profileName, key, value) {
|
|
|
3670
3749
|
}
|
|
3671
3750
|
await atomicWriteFilePreservingMetadata(envPath, nextRaw);
|
|
3672
3751
|
}
|
|
3752
|
+
async function writeHermesApiServerEnv(profileName, config, options = {}) {
|
|
3753
|
+
const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
|
|
3754
|
+
const existingRaw = await readFile2(envPath, "utf8").catch(
|
|
3755
|
+
(error) => {
|
|
3756
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
3757
|
+
return null;
|
|
3758
|
+
}
|
|
3759
|
+
throw error;
|
|
3760
|
+
}
|
|
3761
|
+
);
|
|
3762
|
+
if (existingRaw === null && profileName === "default" && !options.force) {
|
|
3763
|
+
return false;
|
|
3764
|
+
}
|
|
3765
|
+
return writeHermesEnvValues(
|
|
3766
|
+
profileName,
|
|
3767
|
+
[
|
|
3768
|
+
["API_SERVER_ENABLED", config.enabled === false ? "false" : "true"],
|
|
3769
|
+
["API_SERVER_HOST", config.host ?? DEFAULT_HERMES_API_SERVER_HOST],
|
|
3770
|
+
["API_SERVER_PORT", String(config.port ?? DEFAULT_HERMES_API_SERVER_PORT)],
|
|
3771
|
+
["API_SERVER_KEY", config.key ?? ""]
|
|
3772
|
+
],
|
|
3773
|
+
existingRaw ?? ""
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3776
|
+
async function writeHermesEnvValues(profileName, values, existingRaw) {
|
|
3777
|
+
const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
|
|
3778
|
+
const raw = existingRaw ?? await readFile2(envPath, "utf8").catch((error) => {
|
|
3779
|
+
if (isNodeError3(error, "ENOENT")) {
|
|
3780
|
+
return "";
|
|
3781
|
+
}
|
|
3782
|
+
throw error;
|
|
3783
|
+
});
|
|
3784
|
+
const lines = raw ? raw.split(/\r?\n/u) : [];
|
|
3785
|
+
const remaining = new Map(values);
|
|
3786
|
+
const nextLines = lines.map((line) => {
|
|
3787
|
+
const trimmed = line.trim();
|
|
3788
|
+
for (const [key, value] of remaining) {
|
|
3789
|
+
const keyPattern = new RegExp(
|
|
3790
|
+
`^(?:export\\s+)?${escapeRegExp(key)}=`,
|
|
3791
|
+
"u"
|
|
3792
|
+
);
|
|
3793
|
+
if (keyPattern.test(trimmed)) {
|
|
3794
|
+
remaining.delete(key);
|
|
3795
|
+
return `${key}=${formatEnvValue(value)}`;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
return line;
|
|
3799
|
+
});
|
|
3800
|
+
if (remaining.size > 0 && nextLines.length > 0 && nextLines.at(-1) !== "") {
|
|
3801
|
+
nextLines.push("");
|
|
3802
|
+
}
|
|
3803
|
+
for (const [key, value] of remaining) {
|
|
3804
|
+
nextLines.push(`${key}=${formatEnvValue(value)}`);
|
|
3805
|
+
}
|
|
3806
|
+
const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
|
|
3807
|
+
if (nextRaw === raw) {
|
|
3808
|
+
return false;
|
|
3809
|
+
}
|
|
3810
|
+
if (raw) {
|
|
3811
|
+
await atomicWriteFilePreservingMetadata(
|
|
3812
|
+
`${envPath}.bak.${Date.now()}`,
|
|
3813
|
+
raw,
|
|
3814
|
+
{ metadataSourcePath: envPath }
|
|
3815
|
+
);
|
|
3816
|
+
}
|
|
3817
|
+
await atomicWriteFilePreservingMetadata(envPath, nextRaw);
|
|
3818
|
+
return true;
|
|
3819
|
+
}
|
|
3673
3820
|
function applyEnvOverrides(config, env, withDefaults) {
|
|
3674
|
-
const host =
|
|
3675
|
-
const port =
|
|
3821
|
+
const host = env.host ?? config.host;
|
|
3822
|
+
const port = env.port ?? config.port;
|
|
3676
3823
|
return {
|
|
3677
|
-
enabled:
|
|
3824
|
+
enabled: env.enabled ?? config.enabled,
|
|
3678
3825
|
host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
|
|
3679
3826
|
port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
|
|
3680
|
-
key:
|
|
3827
|
+
key: env.key ?? config.key
|
|
3681
3828
|
};
|
|
3682
3829
|
}
|
|
3683
3830
|
function parseEnvBoolean(value) {
|
|
@@ -3985,7 +4132,7 @@ import os2 from "os";
|
|
|
3985
4132
|
import path5 from "path";
|
|
3986
4133
|
|
|
3987
4134
|
// src/constants.ts
|
|
3988
|
-
var LINK_VERSION = "0.4.
|
|
4135
|
+
var LINK_VERSION = "0.4.7";
|
|
3989
4136
|
var LINK_COMMAND = "hermeslink";
|
|
3990
4137
|
var LINK_DEFAULT_PORT = 52379;
|
|
3991
4138
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4548,6 +4695,47 @@ async function ensureHermesApiServerAvailable(options = {}) {
|
|
|
4548
4695
|
});
|
|
4549
4696
|
return { available: true, configResult, started: true, start, health };
|
|
4550
4697
|
}
|
|
4698
|
+
if (health.authInvalid) {
|
|
4699
|
+
void options.logger?.warn("gateway_api_server_auth_repair_requested", {
|
|
4700
|
+
profile: profileName,
|
|
4701
|
+
port: configResult.apiServer.port ?? null,
|
|
4702
|
+
issue: health.issue ?? "auth_invalid"
|
|
4703
|
+
});
|
|
4704
|
+
const repairedConfigResult = await repairHermesApiServerConfig(profileName);
|
|
4705
|
+
const repairedStart = await startHermesGatewayOnce(
|
|
4706
|
+
options.paths ?? resolveRuntimePaths(),
|
|
4707
|
+
profileName,
|
|
4708
|
+
options.logger
|
|
4709
|
+
);
|
|
4710
|
+
health = await waitForHermesApiHealth(
|
|
4711
|
+
repairedConfigResult.apiServer,
|
|
4712
|
+
fetcher,
|
|
4713
|
+
options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS
|
|
4714
|
+
);
|
|
4715
|
+
if (health.healthy) {
|
|
4716
|
+
void options.logger?.info("gateway_api_server_auth_repair_succeeded", {
|
|
4717
|
+
profile: profileName,
|
|
4718
|
+
pid: repairedStart.pid,
|
|
4719
|
+
port: repairedConfigResult.apiServer.port ?? null,
|
|
4720
|
+
log_path: repairedStart.logPath,
|
|
4721
|
+
hermes_version: repairedStart.version?.version ?? null
|
|
4722
|
+
});
|
|
4723
|
+
return {
|
|
4724
|
+
available: true,
|
|
4725
|
+
configResult: repairedConfigResult,
|
|
4726
|
+
started: true,
|
|
4727
|
+
start: repairedStart,
|
|
4728
|
+
health
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
void options.logger?.error("gateway_api_server_auth_repair_failed", {
|
|
4732
|
+
profile: profileName,
|
|
4733
|
+
pid: repairedStart.pid,
|
|
4734
|
+
port: repairedConfigResult.apiServer.port ?? null,
|
|
4735
|
+
log_path: repairedStart.logPath,
|
|
4736
|
+
issue: health.issue ?? "unknown"
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4551
4739
|
const logHint = await readRecentGatewayLogHint(start.logPath);
|
|
4552
4740
|
void options.logger?.error("gateway_auto_start_failed", {
|
|
4553
4741
|
profile: profileName,
|
|
@@ -4638,12 +4826,9 @@ async function readHermesVersion(options = {}) {
|
|
|
4638
4826
|
}
|
|
4639
4827
|
async function execHermesVersion(hermesBin, logger) {
|
|
4640
4828
|
const failures = [];
|
|
4641
|
-
for (const args of [["version"], ["
|
|
4829
|
+
for (const args of [["--version"], ["version"]]) {
|
|
4642
4830
|
try {
|
|
4643
|
-
return await
|
|
4644
|
-
timeout: 5e3,
|
|
4645
|
-
windowsHide: true
|
|
4646
|
-
});
|
|
4831
|
+
return await spawnHermesVersionCommand(hermesBin, args, 5e3);
|
|
4647
4832
|
} catch (error) {
|
|
4648
4833
|
const failure = describeVersionCommandFailure(hermesBin, args, error);
|
|
4649
4834
|
failures.push(failure);
|
|
@@ -4659,6 +4844,85 @@ async function execHermesVersion(hermesBin, logger) {
|
|
|
4659
4844
|
});
|
|
4660
4845
|
throw new Error(summary);
|
|
4661
4846
|
}
|
|
4847
|
+
async function spawnHermesVersionCommand(hermesBin, args, timeoutMs) {
|
|
4848
|
+
return await new Promise((resolve, reject) => {
|
|
4849
|
+
const child = spawn(hermesBin, args, {
|
|
4850
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4851
|
+
windowsHide: true
|
|
4852
|
+
});
|
|
4853
|
+
let stdout = "";
|
|
4854
|
+
let stderr = "";
|
|
4855
|
+
let settled = false;
|
|
4856
|
+
const timer = setTimeout(() => {
|
|
4857
|
+
finish(() => {
|
|
4858
|
+
reject(
|
|
4859
|
+
createVersionCommandError(
|
|
4860
|
+
`${hermesBin} ${args.join(" ")} timed out after ${timeoutMs}ms`,
|
|
4861
|
+
{ stdout, stderr, killed: true }
|
|
4862
|
+
)
|
|
4863
|
+
);
|
|
4864
|
+
}, true);
|
|
4865
|
+
}, timeoutMs);
|
|
4866
|
+
const finish = (callback, kill = false) => {
|
|
4867
|
+
if (settled) {
|
|
4868
|
+
return;
|
|
4869
|
+
}
|
|
4870
|
+
settled = true;
|
|
4871
|
+
clearTimeout(timer);
|
|
4872
|
+
if (kill && child.pid && !child.killed) {
|
|
4873
|
+
child.kill();
|
|
4874
|
+
}
|
|
4875
|
+
callback();
|
|
4876
|
+
};
|
|
4877
|
+
const resolveIfVersionPrinted = () => {
|
|
4878
|
+
if (parseHermesVersion(stdout)) {
|
|
4879
|
+
finish(() => resolve({ stdout: `${stdout}
|
|
4880
|
+
${stderr}`.trim() }), true);
|
|
4881
|
+
}
|
|
4882
|
+
};
|
|
4883
|
+
child.stdout?.on("data", (chunk) => {
|
|
4884
|
+
stdout += chunk.toString();
|
|
4885
|
+
resolveIfVersionPrinted();
|
|
4886
|
+
});
|
|
4887
|
+
child.stderr?.on("data", (chunk) => {
|
|
4888
|
+
stderr += chunk.toString();
|
|
4889
|
+
});
|
|
4890
|
+
child.once("error", (error) => {
|
|
4891
|
+
finish(() => {
|
|
4892
|
+
reject(
|
|
4893
|
+
createVersionCommandError(error.message, {
|
|
4894
|
+
stdout,
|
|
4895
|
+
stderr,
|
|
4896
|
+
cause: error
|
|
4897
|
+
})
|
|
4898
|
+
);
|
|
4899
|
+
});
|
|
4900
|
+
});
|
|
4901
|
+
child.once("close", (code, signal) => {
|
|
4902
|
+
finish(() => {
|
|
4903
|
+
const raw = `${stdout}
|
|
4904
|
+
${stderr}`.trim();
|
|
4905
|
+
const version = parseHermesVersion(raw);
|
|
4906
|
+
if (code === 0 && (raw || version)) {
|
|
4907
|
+
resolve({ stdout: raw });
|
|
4908
|
+
return;
|
|
4909
|
+
}
|
|
4910
|
+
reject(
|
|
4911
|
+
createVersionCommandError(
|
|
4912
|
+
`${hermesBin} ${args.join(" ")} exited with code ${code ?? "null"}`,
|
|
4913
|
+
{
|
|
4914
|
+
stdout,
|
|
4915
|
+
stderr,
|
|
4916
|
+
code,
|
|
4917
|
+
signal,
|
|
4918
|
+
killed: child.killed
|
|
4919
|
+
}
|
|
4920
|
+
)
|
|
4921
|
+
);
|
|
4922
|
+
});
|
|
4923
|
+
});
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4662
4926
|
function assertHermesRunsApiSupported(version, status) {
|
|
4663
4927
|
if (status !== 404) {
|
|
4664
4928
|
return;
|
|
@@ -4916,11 +5180,9 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
4916
5180
|
detailed: record
|
|
4917
5181
|
};
|
|
4918
5182
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
return authIssue;
|
|
4923
|
-
}
|
|
5183
|
+
const authIssue = await probeHermesApiServerAuth(resolvedConfig, fetcher);
|
|
5184
|
+
if (authIssue) {
|
|
5185
|
+
return authIssue;
|
|
4924
5186
|
}
|
|
4925
5187
|
return { healthy: true };
|
|
4926
5188
|
}
|
|
@@ -4937,9 +5199,6 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
|
|
|
4937
5199
|
};
|
|
4938
5200
|
}
|
|
4939
5201
|
async function probeHermesApiServerAuth(config, fetcher) {
|
|
4940
|
-
if (!config.key) {
|
|
4941
|
-
return null;
|
|
4942
|
-
}
|
|
4943
5202
|
const response = await fetchWithTimeout(
|
|
4944
5203
|
`http://127.0.0.1:${config.port}/v1/models`,
|
|
4945
5204
|
{
|
|
@@ -5150,6 +5409,21 @@ async function probeHermesVersion(hermesBin, options) {
|
|
|
5150
5409
|
}
|
|
5151
5410
|
}
|
|
5152
5411
|
}
|
|
5412
|
+
function createVersionCommandError(message, details) {
|
|
5413
|
+
const error = new Error(message, { cause: details.cause });
|
|
5414
|
+
error.stdout = details.stdout;
|
|
5415
|
+
error.stderr = details.stderr;
|
|
5416
|
+
if (details.code !== void 0) {
|
|
5417
|
+
error.code = details.code;
|
|
5418
|
+
}
|
|
5419
|
+
if (details.signal !== void 0) {
|
|
5420
|
+
error.signal = details.signal;
|
|
5421
|
+
}
|
|
5422
|
+
if (details.killed !== void 0) {
|
|
5423
|
+
error.killed = details.killed;
|
|
5424
|
+
}
|
|
5425
|
+
return error;
|
|
5426
|
+
}
|
|
5153
5427
|
function describeVersionCommandFailure(hermesBin, args, error) {
|
|
5154
5428
|
const message = error instanceof Error ? error.message : String(error);
|
|
5155
5429
|
const details = readExecErrorDetails(error, message);
|
|
@@ -7597,12 +7871,12 @@ var ConversationMetadataCoordinator = class {
|
|
|
7597
7871
|
return { ...next, last_event_seq: event.seq, updated_at: event.created_at };
|
|
7598
7872
|
}
|
|
7599
7873
|
scheduleGeneratedTitleRefresh(conversationId) {
|
|
7600
|
-
for (const
|
|
7874
|
+
for (const delay3 of GENERATED_TITLE_RETRY_DELAYS_MS) {
|
|
7601
7875
|
setTimeout(() => {
|
|
7602
7876
|
void this.generateTitleFromFirstRound(conversationId).catch(
|
|
7603
7877
|
() => void 0
|
|
7604
7878
|
);
|
|
7605
|
-
},
|
|
7879
|
+
}, delay3);
|
|
7606
7880
|
}
|
|
7607
7881
|
}
|
|
7608
7882
|
async renameConversation(conversationId, title, input) {
|
|
@@ -11745,6 +12019,46 @@ async function callHermesApi(path26, init, options) {
|
|
|
11745
12019
|
startedAt,
|
|
11746
12020
|
response
|
|
11747
12021
|
);
|
|
12022
|
+
if (response.status !== 401) {
|
|
12023
|
+
return response;
|
|
12024
|
+
}
|
|
12025
|
+
void options.logger?.warn("hermes_api_request_repairing_after_401", {
|
|
12026
|
+
method,
|
|
12027
|
+
path: path26,
|
|
12028
|
+
profile: options.profileName ?? "default",
|
|
12029
|
+
port: config.port ?? null,
|
|
12030
|
+
duration_ms: Date.now() - startedAt
|
|
12031
|
+
});
|
|
12032
|
+
const profileName = options.profileName ?? "default";
|
|
12033
|
+
const repairedConfig = await repairHermesApiServerConfig(profileName);
|
|
12034
|
+
const repairedAvailability = await ensureHermesApiServerAvailable({
|
|
12035
|
+
fetchImpl: options.fetchImpl,
|
|
12036
|
+
forceRestart: true,
|
|
12037
|
+
logger: options.logger,
|
|
12038
|
+
profileName
|
|
12039
|
+
});
|
|
12040
|
+
config = repairedAvailability.configResult.apiServer ?? repairedConfig.apiServer;
|
|
12041
|
+
try {
|
|
12042
|
+
response = await request();
|
|
12043
|
+
} catch (error) {
|
|
12044
|
+
logHermesApiError(
|
|
12045
|
+
options.logger,
|
|
12046
|
+
method,
|
|
12047
|
+
path26,
|
|
12048
|
+
options.profileName,
|
|
12049
|
+
startedAt,
|
|
12050
|
+
error
|
|
12051
|
+
);
|
|
12052
|
+
throw error;
|
|
12053
|
+
}
|
|
12054
|
+
logHermesApiResponse(
|
|
12055
|
+
options.logger,
|
|
12056
|
+
method,
|
|
12057
|
+
path26,
|
|
12058
|
+
options.profileName,
|
|
12059
|
+
startedAt,
|
|
12060
|
+
response
|
|
12061
|
+
);
|
|
11748
12062
|
return response;
|
|
11749
12063
|
}
|
|
11750
12064
|
async function fetchHermesApi(fetcher, config, path26, init, options) {
|
|
@@ -16025,11 +16339,19 @@ function createHttpErrorMiddleware(logger) {
|
|
|
16025
16339
|
}
|
|
16026
16340
|
|
|
16027
16341
|
// src/hermes/profiles.ts
|
|
16028
|
-
import {
|
|
16342
|
+
import { execFile as execFile4 } from "child_process";
|
|
16343
|
+
import { readdir as readdir9, readFile as readFile12, rename as rename3, stat as stat12 } from "fs/promises";
|
|
16029
16344
|
import path18 from "path";
|
|
16345
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
16346
|
+
import { promisify as promisify4 } from "util";
|
|
16030
16347
|
import YAML2 from "yaml";
|
|
16031
16348
|
var DEFAULT_PROFILE = "default";
|
|
16032
16349
|
var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
|
|
16350
|
+
var PROFILE_DELETE_TIMEOUT_MS = 3e4;
|
|
16351
|
+
var PROFILE_GATEWAY_STOP_TIMEOUT_MS = 3e3;
|
|
16352
|
+
var PROFILE_DELETE_STABLE_ABSENCE_MS = 1200;
|
|
16353
|
+
var PROFILE_DELETE_VERIFY_INTERVAL_MS = 150;
|
|
16354
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
16033
16355
|
async function listHermesProfiles(paths = resolveRuntimePaths()) {
|
|
16034
16356
|
const profiles = /* @__PURE__ */ new Map();
|
|
16035
16357
|
profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
|
|
@@ -16101,12 +16423,7 @@ async function updateHermesProfileMetadata(name, metadata, paths = resolveRuntim
|
|
|
16101
16423
|
async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
|
|
16102
16424
|
assertMutableProfile(name);
|
|
16103
16425
|
const profile = await profileInfo(name, paths);
|
|
16104
|
-
const exists = await
|
|
16105
|
-
if (isNodeError14(error, "ENOENT")) {
|
|
16106
|
-
return false;
|
|
16107
|
-
}
|
|
16108
|
-
throw error;
|
|
16109
|
-
});
|
|
16426
|
+
const exists = await pathExists(profile.path);
|
|
16110
16427
|
if (!exists) {
|
|
16111
16428
|
throw new LinkHttpError(
|
|
16112
16429
|
404,
|
|
@@ -16114,7 +16431,14 @@ async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
|
|
|
16114
16431
|
`Profile "${name}" does not exist`
|
|
16115
16432
|
);
|
|
16116
16433
|
}
|
|
16117
|
-
await
|
|
16434
|
+
await deleteHermesProfileWithCliAndVerify(name, profile.path);
|
|
16435
|
+
if (!await waitForProfilePathToRemainAbsent(profile.path)) {
|
|
16436
|
+
throw new LinkHttpError(
|
|
16437
|
+
502,
|
|
16438
|
+
"hermes_profile_delete_incomplete",
|
|
16439
|
+
`Hermes CLI reported success, but Profile "${name}" still exists at ${profile.path}`
|
|
16440
|
+
);
|
|
16441
|
+
}
|
|
16118
16442
|
await deleteProfileIdentity(paths, {
|
|
16119
16443
|
profileName: profile.name,
|
|
16120
16444
|
profileUid: profile.uid
|
|
@@ -16170,9 +16494,168 @@ function assertProfileName(name) {
|
|
|
16170
16494
|
throw new LinkHttpError(400, "invalid_profile_name", "invalid profile name");
|
|
16171
16495
|
}
|
|
16172
16496
|
}
|
|
16497
|
+
async function pathExists(targetPath) {
|
|
16498
|
+
return await stat12(targetPath).then(() => true).catch((error) => {
|
|
16499
|
+
if (isNodeError14(error, "ENOENT")) {
|
|
16500
|
+
return false;
|
|
16501
|
+
}
|
|
16502
|
+
throw error;
|
|
16503
|
+
});
|
|
16504
|
+
}
|
|
16505
|
+
async function deleteHermesProfileWithCli(name) {
|
|
16506
|
+
try {
|
|
16507
|
+
await execFileAsync4(resolveHermesBin(), ["profile", "delete", name, "--yes"], {
|
|
16508
|
+
timeout: PROFILE_DELETE_TIMEOUT_MS,
|
|
16509
|
+
windowsHide: true
|
|
16510
|
+
});
|
|
16511
|
+
} catch (error) {
|
|
16512
|
+
throw new LinkHttpError(
|
|
16513
|
+
502,
|
|
16514
|
+
"hermes_profile_delete_failed",
|
|
16515
|
+
`Hermes CLI could not delete Profile "${name}": ${formatExecError(error)}`
|
|
16516
|
+
);
|
|
16517
|
+
}
|
|
16518
|
+
}
|
|
16519
|
+
async function deleteHermesProfileWithCliAndVerify(name, profilePath) {
|
|
16520
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
16521
|
+
await stopHermesGatewayProcessesForProfile(name);
|
|
16522
|
+
if (attempt > 0 && !await pathExists(profilePath)) {
|
|
16523
|
+
return;
|
|
16524
|
+
}
|
|
16525
|
+
await deleteHermesProfileWithCli(name);
|
|
16526
|
+
if (await waitForProfilePathToRemainAbsent(profilePath)) {
|
|
16527
|
+
return;
|
|
16528
|
+
}
|
|
16529
|
+
}
|
|
16530
|
+
}
|
|
16531
|
+
async function stopHermesGatewayProcessesForProfile(name) {
|
|
16532
|
+
const pids = await findHermesGatewayProcessIdsForProfile(name);
|
|
16533
|
+
if (pids.length === 0) {
|
|
16534
|
+
return;
|
|
16535
|
+
}
|
|
16536
|
+
for (const pid of pids) {
|
|
16537
|
+
killProcess(pid, "SIGTERM");
|
|
16538
|
+
}
|
|
16539
|
+
const remainingAfterTerm = await waitForProcessesToExit(
|
|
16540
|
+
pids,
|
|
16541
|
+
PROFILE_GATEWAY_STOP_TIMEOUT_MS
|
|
16542
|
+
);
|
|
16543
|
+
if (remainingAfterTerm.length === 0) {
|
|
16544
|
+
return;
|
|
16545
|
+
}
|
|
16546
|
+
for (const pid of remainingAfterTerm) {
|
|
16547
|
+
killProcess(pid, "SIGKILL");
|
|
16548
|
+
}
|
|
16549
|
+
const remainingAfterKill = await waitForProcessesToExit(
|
|
16550
|
+
remainingAfterTerm,
|
|
16551
|
+
PROFILE_GATEWAY_STOP_TIMEOUT_MS
|
|
16552
|
+
);
|
|
16553
|
+
if (remainingAfterKill.length > 0) {
|
|
16554
|
+
throw new LinkHttpError(
|
|
16555
|
+
502,
|
|
16556
|
+
"hermes_profile_gateway_stop_failed",
|
|
16557
|
+
`Could not stop Hermes Gateway process(es) for Profile "${name}": ${remainingAfterKill.join(", ")}`
|
|
16558
|
+
);
|
|
16559
|
+
}
|
|
16560
|
+
}
|
|
16561
|
+
async function findHermesGatewayProcessIdsForProfile(name) {
|
|
16562
|
+
if (process.platform === "win32") {
|
|
16563
|
+
return [];
|
|
16564
|
+
}
|
|
16565
|
+
try {
|
|
16566
|
+
const output = await execFileAsync4("ps", ["-axo", "pid=,command="], {
|
|
16567
|
+
timeout: 3e3,
|
|
16568
|
+
windowsHide: true
|
|
16569
|
+
});
|
|
16570
|
+
return parseHermesGatewayProcessIds(output.stdout.toString(), name);
|
|
16571
|
+
} catch {
|
|
16572
|
+
return [];
|
|
16573
|
+
}
|
|
16574
|
+
}
|
|
16575
|
+
function parseHermesGatewayProcessIds(output, name) {
|
|
16576
|
+
const pids = [];
|
|
16577
|
+
for (const line of output.split(/\r?\n/)) {
|
|
16578
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
16579
|
+
if (!match) {
|
|
16580
|
+
continue;
|
|
16581
|
+
}
|
|
16582
|
+
const pid = Number(match[1]);
|
|
16583
|
+
const command = match[2];
|
|
16584
|
+
if (Number.isSafeInteger(pid) && pid !== process.pid && isHermesGatewayRunCommandForProfile(command, name)) {
|
|
16585
|
+
pids.push(pid);
|
|
16586
|
+
}
|
|
16587
|
+
}
|
|
16588
|
+
return pids;
|
|
16589
|
+
}
|
|
16590
|
+
function isHermesGatewayRunCommandForProfile(command, name) {
|
|
16591
|
+
const escapedName = escapeRegExp2(name);
|
|
16592
|
+
return new RegExp(
|
|
16593
|
+
String.raw`(?:^|\s)-p\s+${escapedName}\s+gateway\s+run(?:\s|$)`
|
|
16594
|
+
).test(command);
|
|
16595
|
+
}
|
|
16596
|
+
function killProcess(pid, signal) {
|
|
16597
|
+
try {
|
|
16598
|
+
process.kill(pid, signal);
|
|
16599
|
+
} catch {
|
|
16600
|
+
}
|
|
16601
|
+
}
|
|
16602
|
+
async function waitForProcessesToExit(pids, timeoutMs) {
|
|
16603
|
+
const deadline = Date.now() + timeoutMs;
|
|
16604
|
+
let remaining = pids.filter(isProcessRunning);
|
|
16605
|
+
while (remaining.length > 0 && Date.now() < deadline) {
|
|
16606
|
+
await delay2(100);
|
|
16607
|
+
remaining = remaining.filter(isProcessRunning);
|
|
16608
|
+
}
|
|
16609
|
+
return remaining;
|
|
16610
|
+
}
|
|
16611
|
+
function isProcessRunning(pid) {
|
|
16612
|
+
try {
|
|
16613
|
+
process.kill(pid, 0);
|
|
16614
|
+
return true;
|
|
16615
|
+
} catch (error) {
|
|
16616
|
+
return isNodeError14(error, "EPERM");
|
|
16617
|
+
}
|
|
16618
|
+
}
|
|
16619
|
+
async function waitForProfilePathToRemainAbsent(profilePath) {
|
|
16620
|
+
const deadline = Date.now() + PROFILE_DELETE_STABLE_ABSENCE_MS;
|
|
16621
|
+
while (Date.now() < deadline) {
|
|
16622
|
+
if (await pathExists(profilePath)) {
|
|
16623
|
+
return false;
|
|
16624
|
+
}
|
|
16625
|
+
await delay2(PROFILE_DELETE_VERIFY_INTERVAL_MS);
|
|
16626
|
+
}
|
|
16627
|
+
return !await pathExists(profilePath);
|
|
16628
|
+
}
|
|
16629
|
+
function formatExecError(error) {
|
|
16630
|
+
if (!(error instanceof Error)) {
|
|
16631
|
+
return String(error);
|
|
16632
|
+
}
|
|
16633
|
+
const output = readExecErrorOutput2(error);
|
|
16634
|
+
return output ? `${error.message}
|
|
16635
|
+
${output}` : error.message;
|
|
16636
|
+
}
|
|
16637
|
+
function readExecErrorOutput2(error) {
|
|
16638
|
+
const parts = [];
|
|
16639
|
+
if ("stdout" in error && error.stdout != null) {
|
|
16640
|
+
const stdout = String(error.stdout).trim();
|
|
16641
|
+
if (stdout) {
|
|
16642
|
+
parts.push(stdout);
|
|
16643
|
+
}
|
|
16644
|
+
}
|
|
16645
|
+
if ("stderr" in error && error.stderr != null) {
|
|
16646
|
+
const stderr = String(error.stderr).trim();
|
|
16647
|
+
if (stderr) {
|
|
16648
|
+
parts.push(stderr);
|
|
16649
|
+
}
|
|
16650
|
+
}
|
|
16651
|
+
return parts.join("\n");
|
|
16652
|
+
}
|
|
16173
16653
|
function isNodeError14(error, code) {
|
|
16174
16654
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
16175
16655
|
}
|
|
16656
|
+
function escapeRegExp2(value) {
|
|
16657
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16658
|
+
}
|
|
16176
16659
|
async function countSkills(root) {
|
|
16177
16660
|
const entries = await readdir9(root, { withFileTypes: true }).catch(
|
|
16178
16661
|
(error) => {
|
|
@@ -16883,7 +17366,7 @@ import {
|
|
|
16883
17366
|
cp,
|
|
16884
17367
|
mkdir as mkdir10,
|
|
16885
17368
|
readFile as readFile13,
|
|
16886
|
-
rm as
|
|
17369
|
+
rm as rm6,
|
|
16887
17370
|
stat as stat13
|
|
16888
17371
|
} from "fs/promises";
|
|
16889
17372
|
import path19 from "path";
|
|
@@ -17184,7 +17667,7 @@ async function generateProfileName(displayName, paths) {
|
|
|
17184
17667
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
17185
17668
|
const suffix = randomSuffix(attempt);
|
|
17186
17669
|
const candidate = `${base}-${suffix}`.slice(0, 64).replace(/[-_]+$/u, "");
|
|
17187
|
-
if (!existing.has(candidate) && !await
|
|
17670
|
+
if (!existing.has(candidate) && !await pathExists2(resolveHermesProfileDir(candidate))) {
|
|
17188
17671
|
return candidate;
|
|
17189
17672
|
}
|
|
17190
17673
|
}
|
|
@@ -17210,7 +17693,7 @@ function randomSuffix(attempt) {
|
|
|
17210
17693
|
}
|
|
17211
17694
|
async function applyProfileCreationPostSteps(input) {
|
|
17212
17695
|
const profilePath = resolveHermesProfileDir(input.profileName);
|
|
17213
|
-
if (!await
|
|
17696
|
+
if (!await pathExists2(profilePath)) {
|
|
17214
17697
|
await ensureDirectoryWithInheritedMetadata(profilePath, 448);
|
|
17215
17698
|
}
|
|
17216
17699
|
if (input.sourceProfile && input.copyScopes.length > 0) {
|
|
@@ -17349,7 +17832,7 @@ async function writeEnvValues(profileName, values) {
|
|
|
17349
17832
|
const nextLines = lines.map((line) => {
|
|
17350
17833
|
const trimmed = line.trim();
|
|
17351
17834
|
for (const [key, value] of remaining) {
|
|
17352
|
-
const keyPattern = new RegExp(`^(?:export\\s+)?${
|
|
17835
|
+
const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp3(key)}=`, "u");
|
|
17353
17836
|
if (keyPattern.test(trimmed)) {
|
|
17354
17837
|
remaining.delete(key);
|
|
17355
17838
|
return `${key}=${formatEnvValue2(value)}`;
|
|
@@ -17376,10 +17859,10 @@ async function writeEnvValues(profileName, values) {
|
|
|
17376
17859
|
async function copySkills(sourceProfile, targetProfile) {
|
|
17377
17860
|
const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
|
|
17378
17861
|
const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
|
|
17379
|
-
if (!await
|
|
17862
|
+
if (!await pathExists2(sourceSkills)) {
|
|
17380
17863
|
return;
|
|
17381
17864
|
}
|
|
17382
|
-
await
|
|
17865
|
+
await rm6(targetSkills, { recursive: true, force: true });
|
|
17383
17866
|
await cp(sourceSkills, targetSkills, {
|
|
17384
17867
|
recursive: true,
|
|
17385
17868
|
force: true,
|
|
@@ -17427,7 +17910,7 @@ async function failProfileCreation(input) {
|
|
|
17427
17910
|
await input.writer.write(`
|
|
17428
17911
|
Rolling back ${input.rollbackProfileName}...
|
|
17429
17912
|
`);
|
|
17430
|
-
await
|
|
17913
|
+
await rm6(resolveHermesProfileDir(input.rollbackProfileName), {
|
|
17431
17914
|
recursive: true,
|
|
17432
17915
|
force: true
|
|
17433
17916
|
}).catch(() => void 0);
|
|
@@ -17478,14 +17961,14 @@ function profileCreationLogPath(paths) {
|
|
|
17478
17961
|
async function clearProfileCreationLogFiles(paths) {
|
|
17479
17962
|
const primary = profileCreationLogPath(paths);
|
|
17480
17963
|
await Promise.all([
|
|
17481
|
-
|
|
17964
|
+
rm6(primary, { force: true }).catch(() => void 0),
|
|
17482
17965
|
...Array.from(
|
|
17483
17966
|
{ length: PROFILE_CREATE_LOG_MAX_FILES },
|
|
17484
|
-
(_, index) =>
|
|
17967
|
+
(_, index) => rm6(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
|
|
17485
17968
|
)
|
|
17486
17969
|
]);
|
|
17487
17970
|
}
|
|
17488
|
-
async function
|
|
17971
|
+
async function pathExists2(targetPath) {
|
|
17489
17972
|
return await stat13(targetPath).then(() => true).catch((error) => {
|
|
17490
17973
|
if (isNodeError15(error, "ENOENT")) {
|
|
17491
17974
|
return false;
|
|
@@ -17531,7 +18014,7 @@ function formatEnvValue2(value) {
|
|
|
17531
18014
|
}
|
|
17532
18015
|
return JSON.stringify(value);
|
|
17533
18016
|
}
|
|
17534
|
-
function
|
|
18017
|
+
function escapeRegExp3(value) {
|
|
17535
18018
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
17536
18019
|
}
|
|
17537
18020
|
function isNodeError15(error, code) {
|
|
@@ -18860,7 +19343,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
|
|
|
18860
19343
|
`\u4E0D\u5141\u8BB8\u4ECE App \u5199\u5165 ${key}\u3002`
|
|
18861
19344
|
);
|
|
18862
19345
|
}
|
|
18863
|
-
const keyPattern = new RegExp(`^(?:export\\s+)?${
|
|
19346
|
+
const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp4(key)}=`, "u");
|
|
18864
19347
|
let replaced = false;
|
|
18865
19348
|
for (let index = 0; index < nextLines.length; index += 1) {
|
|
18866
19349
|
if (keyPattern.test(nextLines[index].trim())) {
|
|
@@ -19081,7 +19564,7 @@ function readBoolean3(value) {
|
|
|
19081
19564
|
function formatEnvValue3(value) {
|
|
19082
19565
|
return /^[A-Za-z0-9_./:@-]*$/u.test(value) ? value : JSON.stringify(value);
|
|
19083
19566
|
}
|
|
19084
|
-
function
|
|
19567
|
+
function escapeRegExp4(value) {
|
|
19085
19568
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
19086
19569
|
}
|
|
19087
19570
|
function isNodeError16(error, code) {
|
|
@@ -20337,7 +20820,7 @@ function readModelList(payload) {
|
|
|
20337
20820
|
// src/hermes/updates.ts
|
|
20338
20821
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
20339
20822
|
import { spawn as spawn3 } from "child_process";
|
|
20340
|
-
import { mkdir as mkdir11, readFile as readFile16, rm as
|
|
20823
|
+
import { mkdir as mkdir11, readFile as readFile16, rm as rm7 } from "fs/promises";
|
|
20341
20824
|
import path22 from "path";
|
|
20342
20825
|
var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
|
|
20343
20826
|
var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -20627,10 +21110,10 @@ function updateLogPath(paths) {
|
|
|
20627
21110
|
async function clearUpdateLogFiles(paths) {
|
|
20628
21111
|
const primary = updateLogPath(paths);
|
|
20629
21112
|
await Promise.all([
|
|
20630
|
-
|
|
21113
|
+
rm7(primary, { force: true }).catch(() => void 0),
|
|
20631
21114
|
...Array.from(
|
|
20632
21115
|
{ length: UPDATE_LOG_MAX_FILES },
|
|
20633
|
-
(_, index) =>
|
|
21116
|
+
(_, index) => rm7(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
|
|
20634
21117
|
)
|
|
20635
21118
|
]);
|
|
20636
21119
|
}
|
|
@@ -20722,17 +21205,17 @@ function readString17(payload, key) {
|
|
|
20722
21205
|
// src/link/updates.ts
|
|
20723
21206
|
import { spawn as spawn5 } from "child_process";
|
|
20724
21207
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
20725
|
-
import { mkdir as mkdir14, readFile as readFile18, rm as
|
|
21208
|
+
import { mkdir as mkdir14, readFile as readFile18, rm as rm10 } from "fs/promises";
|
|
20726
21209
|
import path24 from "path";
|
|
20727
21210
|
|
|
20728
21211
|
// src/daemon/process.ts
|
|
20729
21212
|
import { spawn as spawn4 } from "child_process";
|
|
20730
|
-
import { mkdir as mkdir13, readFile as readFile17, rm as
|
|
21213
|
+
import { mkdir as mkdir13, readFile as readFile17, rm as rm9 } from "fs/promises";
|
|
20731
21214
|
import path23 from "path";
|
|
20732
21215
|
|
|
20733
21216
|
// src/daemon/service.ts
|
|
20734
21217
|
import { createServer } from "http";
|
|
20735
|
-
import { mkdir as mkdir12, rm as
|
|
21218
|
+
import { mkdir as mkdir12, rm as rm8, writeFile as writeFile3 } from "fs/promises";
|
|
20736
21219
|
|
|
20737
21220
|
// src/relay/control-client.ts
|
|
20738
21221
|
import WebSocket from "ws";
|
|
@@ -20806,9 +21289,9 @@ function connectRelayControl(options) {
|
|
|
20806
21289
|
return;
|
|
20807
21290
|
}
|
|
20808
21291
|
reconnectAttempts += 1;
|
|
20809
|
-
const
|
|
20810
|
-
options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${
|
|
20811
|
-
retryTimer = setTimeout(connect,
|
|
21292
|
+
const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
|
|
21293
|
+
options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
|
|
21294
|
+
retryTimer = setTimeout(connect, delay3);
|
|
20812
21295
|
retryTimer.unref?.();
|
|
20813
21296
|
});
|
|
20814
21297
|
};
|
|
@@ -21845,7 +22328,7 @@ async function startLinkService(options = {}) {
|
|
|
21845
22328
|
await logger.info("service_stopped");
|
|
21846
22329
|
await logger.flush();
|
|
21847
22330
|
if (options.writePidFile) {
|
|
21848
|
-
await
|
|
22331
|
+
await rm8(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
21849
22332
|
}
|
|
21850
22333
|
}
|
|
21851
22334
|
};
|
|
@@ -22037,7 +22520,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
22037
22520
|
try {
|
|
22038
22521
|
process.kill(status.pid, "SIGTERM");
|
|
22039
22522
|
} catch {
|
|
22040
|
-
await
|
|
22523
|
+
await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
22041
22524
|
return await getDaemonStatus(paths);
|
|
22042
22525
|
}
|
|
22043
22526
|
for (let index = 0; index < 20; index += 1) {
|
|
@@ -22059,7 +22542,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
22059
22542
|
}
|
|
22060
22543
|
}
|
|
22061
22544
|
if (!isProcessAlive3(status.pid) || !await pidBackedServiceIsReachable(paths)) {
|
|
22062
|
-
await
|
|
22545
|
+
await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
22063
22546
|
}
|
|
22064
22547
|
return await getDaemonStatus(paths);
|
|
22065
22548
|
}
|
|
@@ -22067,7 +22550,7 @@ async function getDaemonStatus(paths = resolveRuntimePaths()) {
|
|
|
22067
22550
|
const pidFile = pidFilePath(paths);
|
|
22068
22551
|
const pid = await readPid(pidFile);
|
|
22069
22552
|
if (pid && !isProcessAlive3(pid)) {
|
|
22070
|
-
await
|
|
22553
|
+
await rm9(pidFile, { force: true }).catch(() => void 0);
|
|
22071
22554
|
return {
|
|
22072
22555
|
running: false,
|
|
22073
22556
|
pid: null,
|
|
@@ -22522,10 +23005,10 @@ function updateLogPath2(paths) {
|
|
|
22522
23005
|
async function clearUpdateLogFiles2(paths) {
|
|
22523
23006
|
const primary = updateLogPath2(paths);
|
|
22524
23007
|
await Promise.all([
|
|
22525
|
-
|
|
23008
|
+
rm10(primary, { force: true }).catch(() => void 0),
|
|
22526
23009
|
...Array.from(
|
|
22527
23010
|
{ length: UPDATE_LOG_MAX_FILES2 },
|
|
22528
|
-
(_, index) =>
|
|
23011
|
+
(_, index) => rm10(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
|
|
22529
23012
|
)
|
|
22530
23013
|
]);
|
|
22531
23014
|
}
|
|
@@ -22582,7 +23065,7 @@ function readString18(payload, key) {
|
|
|
22582
23065
|
|
|
22583
23066
|
// src/pairing/pairing.ts
|
|
22584
23067
|
import path25 from "path";
|
|
22585
|
-
import { rm as
|
|
23068
|
+
import { rm as rm11 } from "fs/promises";
|
|
22586
23069
|
|
|
22587
23070
|
// src/relay/bootstrap.ts
|
|
22588
23071
|
var RelayNetworkError = class extends Error {
|
|
@@ -22844,7 +23327,7 @@ async function readPairingClaim(sessionId, paths = resolveRuntimePaths()) {
|
|
|
22844
23327
|
};
|
|
22845
23328
|
}
|
|
22846
23329
|
async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
|
|
22847
|
-
await
|
|
23330
|
+
await rm11(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
|
|
22848
23331
|
}
|
|
22849
23332
|
async function claimPairing(input) {
|
|
22850
23333
|
const paths = input.paths ?? resolveRuntimePaths();
|
package/dist/cli/index.js
CHANGED
package/dist/http/app.js
CHANGED
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import process from "node:process";
|
|
2
4
|
|
|
3
5
|
function isTruthy(value) {
|
|
@@ -14,6 +16,49 @@ function shouldPrintInstallHint() {
|
|
|
14
16
|
return process.env.npm_config_global === "true";
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
function resolveNpmCommand() {
|
|
20
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveGlobalPrefix() {
|
|
24
|
+
const envPrefix =
|
|
25
|
+
process.env.npm_config_prefix?.trim() ??
|
|
26
|
+
process.env.npm_config_global_prefix?.trim() ??
|
|
27
|
+
"";
|
|
28
|
+
if (envPrefix) {
|
|
29
|
+
return envPrefix;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const output = execFileSync(resolveNpmCommand(), ["prefix", "-g"], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
});
|
|
35
|
+
return output.trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveGlobalBinDir(prefix) {
|
|
42
|
+
return process.platform === "win32" ? prefix : path.join(prefix, "bin");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveHermesLinkCommandPath(binDir) {
|
|
46
|
+
return process.platform === "win32"
|
|
47
|
+
? path.join(binDir, "hermeslink.cmd")
|
|
48
|
+
: path.join(binDir, "hermeslink");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pathIncludes(target) {
|
|
52
|
+
if (!target) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const normalizedTarget = path.resolve(target);
|
|
56
|
+
return (process.env.PATH ?? "")
|
|
57
|
+
.split(path.delimiter)
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.some((entry) => path.resolve(entry) === normalizedTarget);
|
|
60
|
+
}
|
|
61
|
+
|
|
17
62
|
function detectLanguage() {
|
|
18
63
|
const candidates = [
|
|
19
64
|
process.env.HERMESLINK_LANG,
|
|
@@ -42,13 +87,35 @@ async function main() {
|
|
|
42
87
|
}
|
|
43
88
|
|
|
44
89
|
const language = detectLanguage();
|
|
90
|
+
const globalPrefix = resolveGlobalPrefix();
|
|
91
|
+
const globalBinDir = globalPrefix ? resolveGlobalBinDir(globalPrefix) : "";
|
|
92
|
+
const commandPath = globalBinDir ? resolveHermesLinkCommandPath(globalBinDir) : "";
|
|
93
|
+
const binOnPath = globalBinDir ? pathIncludes(globalBinDir) : false;
|
|
45
94
|
console.log("");
|
|
46
95
|
if (language === "zh-CN") {
|
|
47
96
|
console.log("Hermes Link 已安装。");
|
|
48
97
|
console.log("运行 `hermeslink pair`,把这台电脑连接到 HermesPilot App。");
|
|
98
|
+
if (globalBinDir && !binOnPath) {
|
|
99
|
+
console.log(
|
|
100
|
+
`如果当前 shell 找不到 \`hermeslink\`,说明 npm 全局 bin 目录没有进 PATH:${globalBinDir}`,
|
|
101
|
+
);
|
|
102
|
+
console.log(`你可以直接运行:${commandPath} pair`);
|
|
103
|
+
if (process.platform !== "win32") {
|
|
104
|
+
console.log(`或者先补 PATH:export PATH="${globalBinDir}:$PATH"`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
49
107
|
} else {
|
|
50
108
|
console.log("Hermes Link installed.");
|
|
51
109
|
console.log("Run `hermeslink pair` to connect this computer with HermesPilot App.");
|
|
110
|
+
if (globalBinDir && !binOnPath) {
|
|
111
|
+
console.log(
|
|
112
|
+
`If your shell cannot find \`hermeslink\`, npm's global bin directory is not on PATH: ${globalBinDir}`,
|
|
113
|
+
);
|
|
114
|
+
console.log(`You can run it directly: ${commandPath} pair`);
|
|
115
|
+
if (process.platform !== "win32") {
|
|
116
|
+
console.log(`Or add it to PATH first: export PATH="${globalBinDir}:$PATH"`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
52
119
|
}
|
|
53
120
|
console.log("");
|
|
54
121
|
}
|