@hellolynk-ai/workbuddy-mcp 0.3.98 → 0.3.100
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/bin/hellolynk-client-setup.mjs +79 -13
- package/clients/core/hellolynk-client-account.mjs +1 -1
- package/clients/core/hellolynk-client-profile.mjs +25 -2
- package/clients/core/hellolynk-client-update-entry.mjs +7 -3
- package/clients/core/hellolynk-client-update-runtime.mjs +62 -33
- package/clients/core/hellolynk-client-update.mjs +62 -18
- package/clients/core/hellolynk-darwin-host.mjs +269 -0
- package/clients/core/hellolynk-darwin-vault.mjs +153 -0
- package/clients/core/hellolynk-host-protected-store.mjs +20 -8
- package/clients/core/hellolynk-human-confirmation-ledger.mjs +53 -6
- package/clients/core/hellolynk-local-data-protection.mjs +22 -7
- package/clients/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
- package/clients/core/hellolynk-neutral-host-installation-store.mjs +3 -7
- package/clients/core/hellolynk-neutral-host-launcher.mjs +9 -3
- package/clients/core/hellolynk-neutral-host-process.mjs +1 -1
- package/clients/core/hellolynk-platform-credential-store.mjs +68 -14
- package/clients/core/hellolynk-release-retention.mjs +101 -2
- package/clients/core/hellolynk-workbench-connection.mjs +5 -3
- package/clients/platform/windows/credential-store.ps1 +46 -10
- package/hellolynk-client-pack-manifest.json +101 -81
- package/hellolynk-client-pack-pin.json +1 -1
- package/integrations/workbuddy/package.json +1 -1
- package/package.json +1 -1
- package/runtime/connect-activate.mjs +5 -1
- package/runtime/core/hellolynk-client-account.mjs +1 -1
- package/runtime/core/hellolynk-client-profile.mjs +25 -2
- package/runtime/core/hellolynk-client-update-entry.mjs +7 -3
- package/runtime/core/hellolynk-client-update-runtime.mjs +62 -33
- package/runtime/core/hellolynk-client-update.mjs +62 -18
- package/runtime/core/hellolynk-darwin-host.mjs +269 -0
- package/runtime/core/hellolynk-darwin-vault.mjs +153 -0
- package/runtime/core/hellolynk-host-protected-store.mjs +20 -8
- package/runtime/core/hellolynk-human-confirmation-ledger.mjs +53 -6
- package/runtime/core/hellolynk-local-data-protection.mjs +22 -7
- package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
- package/runtime/core/hellolynk-neutral-host-installation-store.mjs +3 -7
- package/runtime/core/hellolynk-neutral-host-launcher.mjs +9 -3
- package/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
- package/runtime/core/hellolynk-platform-credential-store.mjs +68 -14
- package/runtime/core/hellolynk-release-retention.mjs +101 -2
- package/runtime/core/hellolynk-workbench-connection.mjs +5 -3
- package/runtime/neutral-host-release-loader.mjs +1 -1
- package/runtime/neutral-host-release-manifest.json +26 -18
- package/runtime/neutral-host-release-pin.json +1 -1
- package/runtime/platform/windows/credential-store.ps1 +46 -10
- package/tools/hellolynk-activate-neutral-host-clients.mjs +5 -1
- package/workbuddy-package-manifest.json +1 -1
- package/workbuddy-package-pin.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import { lstatSync, realpathSync, writeSync } from "node:fs";
|
|
4
|
+
import { constants, lstatSync, realpathSync, writeSync } from "node:fs";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
@@ -16,11 +16,14 @@ const PACK_PIN = "hellolynk-client-pack-pin.json";
|
|
|
16
16
|
|
|
17
17
|
// A person at a terminal reads sentences in the system language; the updater, tests and other
|
|
18
18
|
// programs read one JSON line.
|
|
19
|
-
const
|
|
19
|
+
const explicitJson = process.argv.includes("--json");
|
|
20
|
+
const jsonOutput = explicitJson || process.stdout.isTTY !== true;
|
|
20
21
|
const CHINESE = /^zh\b/i.test(Intl.DateTimeFormat().resolvedOptions().locale);
|
|
21
22
|
const CLIENT_NAMES = Object.freeze({ codex: "Codex", openclaw: "OpenClaw", "trae-work": "TRAE Work",
|
|
22
23
|
workbuddy: "WorkBuddy", cursor: "Cursor", dsh: "DSH" });
|
|
23
24
|
const { packageJson, metadata } = await readPackage();
|
|
25
|
+
const macPackage = metadata.client === "codex" && packageJson.os?.[0] === "darwin"
|
|
26
|
+
&& packageJson.cpu?.[0] === "arm64";
|
|
24
27
|
const commandArguments = process.argv.slice(2).filter((value) => value !== "--json");
|
|
25
28
|
const command = String(commandArguments.shift() || "help").toLowerCase();
|
|
26
29
|
const waitForPending = command === "--wait-for-pending";
|
|
@@ -61,8 +64,14 @@ try {
|
|
|
61
64
|
"--expected-manifest-sha256", sourceRelease.outerManifestSha256,
|
|
62
65
|
]);
|
|
63
66
|
} else {
|
|
64
|
-
const originalLocations = resolveLocations();
|
|
65
67
|
const accountModule = await importStableCore(PACKAGE_ROOT, "hellolynk-client-account.mjs", "hellolynk_npm_account_runtime_invalid");
|
|
68
|
+
const originalLocations = resolveLocations(accountModule.clientAccountBaseRoot);
|
|
69
|
+
if (macPackage && command !== "uninstall") {
|
|
70
|
+
const config = await readJson(path.join(sourceReleaseRoot, ".mcp.json"), "hellolynk_npm_release_invalid", true);
|
|
71
|
+
const commandPath = config.mcpServers?.["hellolynk-codex"]?.command;
|
|
72
|
+
if (typeof commandPath !== "string" || !path.isAbsolute(commandPath)) fail("hellolynk_npm_release_invalid");
|
|
73
|
+
await fs.access(commandPath, constants.X_OK).catch(() => fail("hellolynk_npm_codex_mac_node_required"));
|
|
74
|
+
}
|
|
66
75
|
const baseRoot = accountModule.clientAccountBaseRoot(originalLocations.hostRoot);
|
|
67
76
|
const packageLocations = { ...originalLocations, hostRoot: baseRoot, finalCurrentRoot: path.dirname(baseRoot) };
|
|
68
77
|
const accounts = accountModule.createClientAccountActions({ client: metadata.client, storeRoot: baseRoot });
|
|
@@ -250,6 +259,10 @@ try {
|
|
|
250
259
|
await verifyExactBinding({ ...installed, locations, activation });
|
|
251
260
|
await verifyClientConfiguration({ ...installed, locations });
|
|
252
261
|
}
|
|
262
|
+
// A running unsigned updater asks the stable package for implicit JSON. A
|
|
263
|
+
// staged package may still prove its own first install after lost stdout.
|
|
264
|
+
const legacyUpdateProof = installed && !existingDoctor && !explicitJson && jsonOutput
|
|
265
|
+
&& process.env.HELLOLYNK_NEUTRAL_STORE_ROOT && samePath(PACKAGE_ROOT, installed.packageRoot);
|
|
253
266
|
print({
|
|
254
267
|
ok: true,
|
|
255
268
|
client: metadata.client,
|
|
@@ -260,7 +273,7 @@ try {
|
|
|
260
273
|
activated: activation.ready,
|
|
261
274
|
activationState: activation.state,
|
|
262
275
|
stableReleaseVerified: installed != null,
|
|
263
|
-
exactBindingVerified: installed != null,
|
|
276
|
+
...(legacyUpdateProof ? {} : { exactBindingVerified: installed != null }),
|
|
264
277
|
clientConfigurationVerified: installed != null,
|
|
265
278
|
installsNode: false,
|
|
266
279
|
installsGit: false,
|
|
@@ -429,7 +442,8 @@ async function uninstallClient(locations) {
|
|
|
429
442
|
if (receiverConfigured) await receiver.configureTraeNativeTaskWorker({ storeRoot: locations.hostRoot, enabled: false });
|
|
430
443
|
}
|
|
431
444
|
// A binding names the release it starts, so it cannot outlive the releases removed below.
|
|
432
|
-
|
|
445
|
+
const protectedFile = macPackage ? "protected-v1.sqlite" : "capabilities-v1.dpapi.clixml";
|
|
446
|
+
if (await exists(path.join(locations.hostRoot, protectedFile))) {
|
|
433
447
|
const store = await importStableCore(PACKAGE_ROOT, "hellolynk-host-protected-store.mjs", "hellolynk_npm_binding_invalid");
|
|
434
448
|
await store.createHostProtectedStore({ root: locations.hostRoot, followClientAccount: false }).disconnectLocalClient(metadata.client);
|
|
435
449
|
}
|
|
@@ -838,17 +852,20 @@ async function verifyRelease(releaseRoot, packageRoot, expectedMetadata = metada
|
|
|
838
852
|
return Object.freeze({ manifestSha256, outerManifestSha256 });
|
|
839
853
|
}
|
|
840
854
|
|
|
841
|
-
function resolveLocations() {
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
855
|
+
function resolveLocations(getDefaultHostRoot) {
|
|
856
|
+
if (macPackage) {
|
|
857
|
+
if (process.platform !== "darwin" || process.arch !== "arm64") fail("hellolynk_npm_platform_unsupported");
|
|
858
|
+
} else if (process.platform !== "win32") fail("hellolynk_npm_windows_only");
|
|
859
|
+
const home = path.resolve((macPackage ? process.env.HOME : process.env.USERPROFILE) || os.homedir());
|
|
860
|
+
const localAppData = macPackage ? path.join(home, "Library", "Application Support")
|
|
861
|
+
: path.resolve(process.env.LOCALAPPDATA || path.join(home, "AppData", "Local"));
|
|
862
|
+
const appData = macPackage ? localAppData : path.resolve(process.env.APPDATA || path.join(home, "AppData", "Roaming"));
|
|
846
863
|
const configuredCodexHome = String(process.env.CODEX_HOME || "");
|
|
847
864
|
if (configuredCodexHome && !path.isAbsolute(configuredCodexHome)) fail("hellolynk_npm_codex_home_invalid");
|
|
848
865
|
const codexHome = path.resolve(configuredCodexHome || path.join(home, ".codex"));
|
|
849
866
|
const configuredHostRoot = String(process.env.HELLOLYNK_NEUTRAL_STORE_ROOT || "").trim();
|
|
850
867
|
if (configuredHostRoot && !path.isAbsolute(configuredHostRoot)) fail("hellolynk_npm_store_root_invalid");
|
|
851
|
-
const hostRoot = path.resolve(configuredHostRoot ||
|
|
868
|
+
const hostRoot = path.resolve(configuredHostRoot || getDefaultHostRoot());
|
|
852
869
|
if (configuredHostRoot && path.basename(hostRoot).toLowerCase() !== "host") {
|
|
853
870
|
fail("hellolynk_npm_store_root_invalid");
|
|
854
871
|
}
|
|
@@ -1004,6 +1021,12 @@ async function startHostWhenAbsent(runtime, binding) {
|
|
|
1004
1021
|
}
|
|
1005
1022
|
|
|
1006
1023
|
async function secureStableReleaseTree(locations, releaseId, options = {}) {
|
|
1024
|
+
const releasePath = path.resolve(locations.finalCurrentRoot, "releases", releaseId);
|
|
1025
|
+
// An existing AppData root can be shared while a new release is redirected
|
|
1026
|
+
// into one desktop app's private cache. Other clients cannot use that release.
|
|
1027
|
+
if (process.platform === "win32" && realpathSync.native(releasePath).toLowerCase() !== releasePath.toLowerCase()) {
|
|
1028
|
+
fail("client_root_alias_rejected");
|
|
1029
|
+
}
|
|
1007
1030
|
const module = await importCore(
|
|
1008
1031
|
"hellolynk-platform-credential-store.mjs",
|
|
1009
1032
|
"hellolynk_npm_client_root_security_unavailable",
|
|
@@ -2246,6 +2269,13 @@ function runCodexJson(args, code, codexHome) {
|
|
|
2246
2269
|
}
|
|
2247
2270
|
|
|
2248
2271
|
function minimalEnv({ codexHome = null } = {}) {
|
|
2272
|
+
if (macPackage) return Object.freeze({
|
|
2273
|
+
PATH: process.env.PATH || "/usr/bin:/bin",
|
|
2274
|
+
HOME: process.env.HOME || os.homedir(),
|
|
2275
|
+
TMPDIR: process.env.TMPDIR || os.tmpdir(),
|
|
2276
|
+
LANG: process.env.LANG || "en_US.UTF-8",
|
|
2277
|
+
...(codexHome ? { CODEX_HOME: codexHome } : {}),
|
|
2278
|
+
});
|
|
2249
2279
|
return Object.freeze({
|
|
2250
2280
|
PATH: process.env.Path || process.env.PATH || "",
|
|
2251
2281
|
PATHEXT: process.env.PATHEXT || "",
|
|
@@ -2261,13 +2291,18 @@ function minimalEnv({ codexHome = null } = {}) {
|
|
|
2261
2291
|
|
|
2262
2292
|
function resolveCodexRunner(codexHome) {
|
|
2263
2293
|
if (resolvedCodexRunner) return resolvedCodexRunner;
|
|
2264
|
-
const searchPath = String(process.env.Path || process.env.PATH || "");
|
|
2294
|
+
const searchPath = String((macPackage ? process.env.PATH : process.env.Path || process.env.PATH) || "");
|
|
2265
2295
|
let sawLoaderCandidate = false;
|
|
2266
2296
|
let sawNativeCandidate = false;
|
|
2267
2297
|
for (const rawEntry of searchPath.split(path.delimiter)) {
|
|
2268
2298
|
try {
|
|
2269
2299
|
const logical = rawEntry.trim().replace(/^"(.*)"$/, "$1");
|
|
2270
2300
|
if (!logical || !path.isAbsolute(logical)) continue;
|
|
2301
|
+
if (macPackage) {
|
|
2302
|
+
const runner = probeMacCodex(path.join(logical, "codex"), codexHome);
|
|
2303
|
+
if (runner) return (resolvedCodexRunner = runner);
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2271
2306
|
// nvm/fnm and user profile mappings can put a junction on PATH. Resolve
|
|
2272
2307
|
// that directory once, then retain the ordinary-file checks below it.
|
|
2273
2308
|
const entry = realpathSync(path.resolve(logical));
|
|
@@ -2321,6 +2356,15 @@ function resolveCodexRunner(codexHome) {
|
|
|
2321
2356
|
}
|
|
2322
2357
|
|
|
2323
2358
|
function resolveCodexDesktopRunner(codexHome) {
|
|
2359
|
+
if (macPackage) {
|
|
2360
|
+
for (const directory of ["/Applications", path.join(os.homedir(), "Applications")]) {
|
|
2361
|
+
for (const app of ["ChatGPT.app", "Codex.app"]) {
|
|
2362
|
+
const runner = probeMacCodex(path.join(directory, app, "Contents", "Resources", "codex"), codexHome);
|
|
2363
|
+
if (runner) return runner;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2324
2368
|
if (process.platform !== "win32") return null;
|
|
2325
2369
|
const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT;
|
|
2326
2370
|
if (!systemRoot || !path.isAbsolute(systemRoot)) return null;
|
|
@@ -2348,6 +2392,22 @@ function resolveCodexDesktopRunner(codexHome) {
|
|
|
2348
2392
|
return null;
|
|
2349
2393
|
}
|
|
2350
2394
|
|
|
2395
|
+
function probeMacCodex(logicalPath, codexHome) {
|
|
2396
|
+
// npm and Homebrew commonly expose a symlink. Execute the canonical file directly.
|
|
2397
|
+
try {
|
|
2398
|
+
const command = realpathSync(logicalPath);
|
|
2399
|
+
const metadata = lstatSync(command);
|
|
2400
|
+
if (!metadata.isFile() || !(metadata.mode & 0o111)) return null;
|
|
2401
|
+
const result = spawnSync(command, ["--version"], {
|
|
2402
|
+
encoding: "utf8", shell: false, timeout: 5000, env: minimalEnv({ codexHome }),
|
|
2403
|
+
});
|
|
2404
|
+
if (result.status === 0 && /^codex-cli\s+\d+\.\d+\.\d+/u.test(String(result.stdout || "").trim())) {
|
|
2405
|
+
return Object.freeze({ command, argsPrefix: Object.freeze([]), kind: "native" });
|
|
2406
|
+
}
|
|
2407
|
+
} catch { /* Missing or unusable CLI. */ }
|
|
2408
|
+
return null;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2351
2411
|
function resolveRegularDescendantSync(anchor, anchorRealPath, segments) {
|
|
2352
2412
|
let observed = anchor;
|
|
2353
2413
|
for (let index = 0; index < segments.length; index += 1) {
|
|
@@ -2527,6 +2587,7 @@ function samePath(left, right) {
|
|
|
2527
2587
|
const resolved = normalizeWindowsNamespacePath(value);
|
|
2528
2588
|
let canonical = resolved;
|
|
2529
2589
|
try { canonical = realpathSync(resolved); } catch { /* comparison can still reject a missing path */ }
|
|
2590
|
+
if (macPackage) return canonical;
|
|
2530
2591
|
return normalizeWindowsNamespacePath(canonical).replace(/[\\/]+$/u, "").toLowerCase();
|
|
2531
2592
|
};
|
|
2532
2593
|
return normalize(left) === normalize(right);
|
|
@@ -2646,7 +2707,9 @@ function helpText() {
|
|
|
2646
2707
|
: [[say("安装", "Install"), "init"], [say("连接账号", "Connect an account"), connect],
|
|
2647
2708
|
[say("更新或修复", "Update or repair"), "setup"], [say("检查", "Check"), "doctor"],
|
|
2648
2709
|
[say("卸载", "Uninstall"), "uninstall"]];
|
|
2649
|
-
|
|
2710
|
+
const platform = macPackage ? " (macOS arm64, Node.js 22.16.0+)"
|
|
2711
|
+
: say("(Windows,Node.js 22.16.0+)", " (Windows, Node.js 22.16.0+)");
|
|
2712
|
+
return `${[`HelloLynk for ${CLIENT_NAMES[metadata.client]}${platform}`,
|
|
2650
2713
|
...rows.map(([label, args]) => `${label}${say(":", ": ")}${run} ${args}`),
|
|
2651
2714
|
say("加 --json 输出一行 JSON。", "Add --json for one line of JSON.")].join("\n")}\n`;
|
|
2652
2715
|
}
|
|
@@ -2720,6 +2783,9 @@ function failureText(failure) {
|
|
|
2720
2783
|
function failureReason(code, name) {
|
|
2721
2784
|
if (code === "hellolynk_node_22_16_required") return say("需要 Node.js 22.16 或更高版本。", "Node.js 22.16 or later is required.");
|
|
2722
2785
|
if (code === "hellolynk_npm_windows_only") return say("只支持 Windows。", "Windows only.");
|
|
2786
|
+
if (code === "hellolynk_npm_platform_unsupported") return say("此安装包适用于 Apple 芯片的 Mac。", "This package requires an Apple silicon Mac.");
|
|
2787
|
+
if (code === "hellolynk_npm_codex_mac_node_required") return say("请将 ChatGPT 安装到「应用程序」,打开 Codex 后再试。",
|
|
2788
|
+
"Install ChatGPT in Applications, open Codex, and try again.");
|
|
2723
2789
|
if (code === "hellolynk_npm_connect_id_invalid") {
|
|
2724
2790
|
return say("灵信号不对。请从官网复制从 hellolynk: 开始的完整灵信号。",
|
|
2725
2791
|
"This HelloLynk ID is not valid. Copy the complete ID, starting with hellolynk:, from the website.");
|
|
@@ -68,7 +68,7 @@ export function withClientAccountLock(client, root, operation) {
|
|
|
68
68
|
|
|
69
69
|
export function createClientAccountActions(options) {
|
|
70
70
|
const client = options?.client;
|
|
71
|
-
const root = options?.storeRoot || defaultNeutralHostStoreRoot(
|
|
71
|
+
const root = options?.storeRoot || defaultNeutralHostStoreRoot(process.platform, options?.env || process.env);
|
|
72
72
|
statePath(client, clientAccountBaseRoot(root));
|
|
73
73
|
const readAccountState = () => {
|
|
74
74
|
const state = readClientAccount(client, root);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -19,12 +20,34 @@ const SAFE_SCOPE = /^[a-z][a-z0-9_.:-]{0,127}$/;
|
|
|
19
20
|
const MAX_PROFILE_BYTES = 64 * 1024;
|
|
20
21
|
|
|
21
22
|
export function defaultHelloLynkClientRoot(platform = process.platform, env = process.env) {
|
|
22
|
-
const home = resolve(String(env.HOME || env.USERPROFILE || homedir()));
|
|
23
|
-
if (platform === "win32")
|
|
23
|
+
const home = resolve(String((platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME || env.USERPROFILE) || homedir()));
|
|
24
|
+
if (platform === "win32") {
|
|
25
|
+
const current = join(home, ".hellolynk", "hellolynk-v2");
|
|
26
|
+
// Once selected, another client's older installer must not move us back.
|
|
27
|
+
if (hasWindowsClientInstallation(current)) return current;
|
|
28
|
+
const existing = resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk");
|
|
29
|
+
// AppData created from a packaged desktop app can be private to that app.
|
|
30
|
+
// Keep an existing installation; an empty failed setup is not an account.
|
|
31
|
+
if (hasWindowsClientInstallation(existing)) return existing;
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
24
34
|
if (platform === "darwin") return join(home, "Library", "Application Support", "HelloLynk");
|
|
25
35
|
return resolve(String(env.XDG_DATA_HOME || join(home, ".local", "share")), "hellolynk");
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
function hasWindowsClientInstallation(root) {
|
|
39
|
+
for (const directory of ["profiles", "releases", "host", "credentials", "e2ee-private-keys"]) {
|
|
40
|
+
try {
|
|
41
|
+
const entries = readdirSync(join(root, directory), { withFileTypes: true });
|
|
42
|
+
if (entries.some((entry) => directory !== "host" || entry.name !== "runtime")) return true;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// An unreadable existing location must not silently become a fresh account.
|
|
45
|
+
if (error.code !== "ENOENT") return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
function safeId(value, code) {
|
|
29
52
|
const normalized = String(value || "").trim();
|
|
30
53
|
if (!SAFE_ID.test(normalized)) throw new Error(code);
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { clientAccountBaseRoot } from "./hellolynk-client-account.mjs";
|
|
4
4
|
import { startClientUpdateRuntime } from "./hellolynk-client-update-runtime.mjs";
|
|
5
|
+
import { clientUpdateCatalogId, MAC_CLIENT_VERSION } from "./hellolynk-client-update.mjs";
|
|
5
6
|
|
|
6
7
|
const running = new Map();
|
|
7
8
|
|
|
@@ -9,7 +10,9 @@ const running = new Map();
|
|
|
9
10
|
// to the Host. A checkout or an uninstalled test fixture never self-updates.
|
|
10
11
|
export function startInstalledClientUpdate(options = {}) {
|
|
11
12
|
const { client, packageRoot, storeRoot } = options;
|
|
12
|
-
|
|
13
|
+
const catalogId = clientUpdateCatalogId(client, process.platform);
|
|
14
|
+
const mac = process.platform === "darwin";
|
|
15
|
+
if (!catalogId || (mac && process.arch !== "arm64") || !path.isAbsolute(String(packageRoot || ""))) return Promise.resolve(null);
|
|
13
16
|
let baseRoot;
|
|
14
17
|
try { baseRoot = clientAccountBaseRoot(storeRoot); } catch { return Promise.resolve(null); }
|
|
15
18
|
const key = `${client}\0${path.resolve(packageRoot)}\0${baseRoot}`;
|
|
@@ -21,8 +24,9 @@ export function startInstalledClientUpdate(options = {}) {
|
|
|
21
24
|
const metadata = await fs.lstat(packageFile);
|
|
22
25
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 64 * 1024) return null;
|
|
23
26
|
const manifest = JSON.parse(await fs.readFile(packageFile, "utf8"));
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const version = mac ? MAC_CLIENT_VERSION : /^\d+\.\d+\.\d+$/u;
|
|
28
|
+
if (manifest.hellolynkClient?.client !== client || !version.test(manifest.version || "")
|
|
29
|
+
|| (mac && (manifest.os?.[0] !== "darwin" || manifest.cpu?.[0] !== "arm64"))) return null;
|
|
26
30
|
return startClientUpdateRuntime({
|
|
27
31
|
root: baseRoot, client, currentVersion: manifest.version,
|
|
28
32
|
installContext: { ...options.installContext, packageRoot: path.resolve(packageRoot) },
|
|
@@ -4,7 +4,8 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { createClientUpdateManager, extractClientUpdateArchive, compareClientUpdateVersions,
|
|
7
|
+
import { createClientUpdateManager, extractClientUpdateArchive, compareClientUpdateVersions, clientUpdateCatalogId,
|
|
8
|
+
CLIENT_UPDATE_CLIENTS, CLIENT_UPDATE_RELEASES, MAC_CLIENT_VERSION } from "./hellolynk-client-update.mjs";
|
|
8
9
|
import { withNeutralHostStoreLock } from "./hellolynk-neutral-host-installation-store.mjs";
|
|
9
10
|
import { readClientAccount } from "./hellolynk-client-account.mjs";
|
|
10
11
|
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
@@ -26,7 +27,7 @@ const RETRY_AFTER_MS = 10 * 60_000;
|
|
|
26
27
|
// waits only for DSH's lease, and a downloaded update is applied by a worker in
|
|
27
28
|
// the background. Checking the network is always detached.
|
|
28
29
|
export async function startClientUpdateRuntime(options = {}) {
|
|
29
|
-
if ((options.platform || process.platform)
|
|
30
|
+
if (!clientUpdateCatalogId(options.client, options.platform || process.platform)) return disabled();
|
|
30
31
|
let config;
|
|
31
32
|
try { config = normalize(options); await verifyInstalledContext(config); }
|
|
32
33
|
catch { return disabled(); }
|
|
@@ -94,8 +95,8 @@ export async function runClientUpdateWorker(options = {}) {
|
|
|
94
95
|
function createRuntime(config, options) {
|
|
95
96
|
const { root, client, currentVersion, installContext } = config;
|
|
96
97
|
const directory = path.join(root, "client-updates", client);
|
|
97
|
-
const statePath = path.join(directory, "runtime.json"), leasePath = path.join(directory, "leases.json"), installedPath = path.join(directory, "installed.json");
|
|
98
|
-
const manager = createClientUpdateManager({ ...options, root, client, currentVersion });
|
|
98
|
+
const statePath = path.join(directory, "runtime-v2.json"), leasePath = path.join(directory, "leases.json"), installedPath = path.join(directory, "installed.json");
|
|
99
|
+
const manager = createClientUpdateManager({ ...options, root, client, currentVersion, platform: config.platform });
|
|
99
100
|
const alive = options.isProcessAlive || processAlive;
|
|
100
101
|
async function gate(operation) {
|
|
101
102
|
await safeDirectory(directory);
|
|
@@ -200,7 +201,8 @@ function createRuntime(config, options) {
|
|
|
200
201
|
const attempted = runtime.sha256 === downloaded.sha256 && !(runtime.stage === "applying" || retryable);
|
|
201
202
|
if (downloaded.stage !== "downloaded" || downloaded.error || attempted) return;
|
|
202
203
|
// A damaged worker record means no known worker.
|
|
203
|
-
const
|
|
204
|
+
const workerPath = path.join(directory, "worker-v2.json");
|
|
205
|
+
const worker = await readJson(workerPath, null).catch((error) => { if (!damaged(error)) throw error; return null; });
|
|
204
206
|
if (worker?.client === client && Number.isSafeInteger(worker.pid) && alive(worker.pid)) return;
|
|
205
207
|
const launch = options.spawnWorker || launchWorker;
|
|
206
208
|
let pid;
|
|
@@ -210,13 +212,13 @@ function createRuntime(config, options) {
|
|
|
210
212
|
} catch (error) {
|
|
211
213
|
// Retain only this rejected launch attempt, not raw errors or proof of
|
|
212
214
|
// worker absence. Keep an earlier PID's conservative liveness check.
|
|
213
|
-
await atomicJson(
|
|
215
|
+
await atomicJson(workerPath, { client,
|
|
214
216
|
...(worker?.client === client && Number.isSafeInteger(worker.pid) ? { pid: worker.pid } : {}),
|
|
215
217
|
sha256: downloaded.sha256, error: "client_update_worker_launch_failed", failedAt: Date.now(),
|
|
216
218
|
}).catch(() => {});
|
|
217
219
|
throw error;
|
|
218
220
|
}
|
|
219
|
-
await atomicJson(
|
|
221
|
+
await atomicJson(workerPath, { client, pid });
|
|
220
222
|
});
|
|
221
223
|
}
|
|
222
224
|
async function applyPending() {
|
|
@@ -236,7 +238,7 @@ function createRuntime(config, options) {
|
|
|
236
238
|
if (sha(await safeFile(path.join(proof.packageRoot, "hellolynk-client-pack-manifest.json")))
|
|
237
239
|
!== sha(await safeFile(path.join(installContext.packageRoot, "hellolynk-client-pack-manifest.json")))) throw failure("adoption_invalid");
|
|
238
240
|
const doctor = await (options.runInstaller || runInstaller)({ executable: installContext.nodePath || process.execPath,
|
|
239
|
-
args: [path.join(installContext.packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor"], cwd: installContext.packageRoot, root, timeoutMs: 180_000 });
|
|
241
|
+
args: [path.join(installContext.packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor", "--json"], cwd: installContext.packageRoot, root, timeoutMs: 180_000 });
|
|
240
242
|
if (!doctorProves(doctor, client, currentVersion)) throw failure("adoption_invalid");
|
|
241
243
|
const adoptedRoot = path.join(path.dirname(root), "releases", `${client}-${sha(await safeFile(path.join(proof.packageRoot, "hellolynk-client-pack-manifest.json")))}`);
|
|
242
244
|
if (!options.runInstaller) await verifyPackage(adoptedRoot, client, currentVersion);
|
|
@@ -274,7 +276,7 @@ function createRuntime(config, options) {
|
|
|
274
276
|
try {
|
|
275
277
|
await verifyPackage(packageRoot, client, version);
|
|
276
278
|
const inspected = await run({ executable: installContext.nodePath || process.execPath,
|
|
277
|
-
args: [path.join(packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor"], cwd: packageRoot, root, timeoutMs: 180_000 });
|
|
279
|
+
args: [path.join(packageRoot, "bin", "hellolynk-client-setup.mjs"), "doctor", "--json"], cwd: packageRoot, root, timeoutMs: 180_000 });
|
|
278
280
|
return doctorProves(inspected, client, version);
|
|
279
281
|
} catch { return false; }
|
|
280
282
|
};
|
|
@@ -397,7 +399,7 @@ async function prepareCandidate(config, record) {
|
|
|
397
399
|
const directory = path.join(config.root, "client-updates", config.client);
|
|
398
400
|
if (!HASH.test(record.sha256) || typeof record.version !== "string" || !/^[0-9A-Za-z.+-]{1,100}$/.test(record.version)
|
|
399
401
|
|| typeof record.filename !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.zip$/.test(record.filename)) throw failure("candidate_invalid");
|
|
400
|
-
const artifactPath = path.join(directory,
|
|
402
|
+
const artifactPath = path.join(directory, CLIENT_UPDATE_RELEASES, `${record.version}-${record.sha256}`, record.filename);
|
|
401
403
|
if (artifactPath !== record.artifactPath) throw failure("candidate_path_invalid");
|
|
402
404
|
const archive = await safeFile(artifactPath, 64 * 1024 * 1024);
|
|
403
405
|
if (archive.length !== record.bytes || sha(archive) !== record.sha256) throw failure("artifact_integrity_invalid");
|
|
@@ -406,43 +408,63 @@ async function prepareCandidate(config, record) {
|
|
|
406
408
|
const extractedRoot = path.join(temporaryRoot, "extracted");
|
|
407
409
|
try {
|
|
408
410
|
// Re-extract the hash-verified archive; never execute a mutable earlier
|
|
409
|
-
// extractedRoot or an executable path supplied by state.
|
|
411
|
+
// extractedRoot or an executable path supplied by update state.
|
|
410
412
|
const sealed = path.join(temporaryRoot, record.filename);
|
|
411
413
|
await fs.writeFile(sealed, archive, { flag: "wx", mode: 0o600 });
|
|
412
|
-
await extractClientUpdateArchive({ archivePath: sealed, destination: extractedRoot });
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const descriptor = await readJson(path.join(extractedRoot, "trusted-release.json"));
|
|
416
|
-
const manifestBytes = await safeFile(path.join(extractedRoot, "dist", "package", "hellolynk-dsh-nav-build.json"));
|
|
417
|
-
if (descriptor?.contract !== "hellolynk-dsh-nav-release-v1" || descriptor.manifestSha256 !== sha(manifestBytes)) throw failure("package_pin_invalid");
|
|
418
|
-
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
419
|
-
if (manifest.contract !== "hellolynk-dsh-nav-build-v3") throw failure("package_pin_invalid");
|
|
420
|
-
await verifyRecords(path.join(extractedRoot, "dist", "package"), manifest.packageFiles, ["hellolynk-dsh-nav-build.json"]);
|
|
421
|
-
packageRoot = path.join(extractedRoot, "dist", "package", "runtime", "connect");
|
|
422
|
-
await safeFile(path.join(extractedRoot, "scripts", "install.mjs"));
|
|
423
|
-
} else {
|
|
424
|
-
const candidates = [extractedRoot, path.join(extractedRoot, "package"), path.join(extractedRoot, `hellolynk-${config.client}-${record.version}`, "package")];
|
|
425
|
-
const matches = [];
|
|
426
|
-
for (const candidate of candidates) if (await exists(path.join(candidate, "package.json"))) matches.push(candidate);
|
|
427
|
-
if (matches.length !== 1) throw failure("package_identity_invalid");
|
|
428
|
-
packageRoot = matches[0];
|
|
429
|
-
}
|
|
430
|
-
await verifyPackage(packageRoot, config.client, record.version);
|
|
414
|
+
await extractClientUpdateArchive({ archivePath: sealed, destination: extractedRoot, platform: config.platform });
|
|
415
|
+
const { packageRoot } = await verifyExtractedClientUpdatePackage(extractedRoot,
|
|
416
|
+
{ client: config.client, platform: config.platform, version: record.version });
|
|
431
417
|
return { temporaryRoot, extractedRoot, artifactPath: sealed, packageRoot, setupPath: path.join(packageRoot, "bin", "hellolynk-client-setup.mjs") };
|
|
432
418
|
} catch (error) { await fs.rm(temporaryRoot, { recursive: true, force: true }); throw error; }
|
|
433
419
|
}
|
|
434
420
|
|
|
421
|
+
export async function verifyExtractedClientUpdatePackage(extractedRoot, { client, platform, version }) {
|
|
422
|
+
if (!CLIENT_UPDATE_CLIENTS.includes(client) || typeof version !== "string" || !VERSION.test(version)) throw failure("package_identity_invalid");
|
|
423
|
+
let packageRoot;
|
|
424
|
+
if (client === "dsh") {
|
|
425
|
+
const descriptor = await readJson(path.join(extractedRoot, "trusted-release.json"));
|
|
426
|
+
const manifestBytes = await safeFile(path.join(extractedRoot, "dist", "package", "hellolynk-dsh-nav-build.json"));
|
|
427
|
+
if (descriptor?.contract !== "hellolynk-dsh-nav-release-v1" || descriptor.manifestSha256 !== sha(manifestBytes)) throw failure("package_pin_invalid");
|
|
428
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
429
|
+
if (manifest.contract !== "hellolynk-dsh-nav-build-v3") throw failure("package_pin_invalid");
|
|
430
|
+
await verifyRecords(path.join(extractedRoot, "dist", "package"), manifest.packageFiles, ["hellolynk-dsh-nav-build.json"]);
|
|
431
|
+
packageRoot = path.join(extractedRoot, "dist", "package", "runtime", "connect");
|
|
432
|
+
await safeFile(path.join(extractedRoot, "scripts", "install.mjs"));
|
|
433
|
+
} else {
|
|
434
|
+
const id = clientUpdateCatalogId(client, platform) || client;
|
|
435
|
+
const candidates = [extractedRoot, path.join(extractedRoot, "package"), path.join(extractedRoot, `hellolynk-${id}-${version}`, "package")];
|
|
436
|
+
const matches = [];
|
|
437
|
+
for (const candidate of candidates) if (await exists(path.join(candidate, "package.json"))) matches.push(candidate);
|
|
438
|
+
if (matches.length !== 1) throw failure("package_identity_invalid");
|
|
439
|
+
packageRoot = matches[0];
|
|
440
|
+
}
|
|
441
|
+
const checked = await verifyPackage(packageRoot, client, version);
|
|
442
|
+
return { packageRoot, ...checked };
|
|
443
|
+
}
|
|
444
|
+
|
|
435
445
|
async function verifyPackage(root, client, version) {
|
|
436
446
|
const pkg = await readJson(path.join(root, "package.json"));
|
|
437
447
|
const manifestBytes = await safeFile(path.join(root, "hellolynk-client-pack-manifest.json"));
|
|
438
448
|
const pin = await readJson(path.join(root, "hellolynk-client-pack-pin.json"));
|
|
439
449
|
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
440
450
|
if (pkg.version !== version || pkg.hellolynkClient?.client !== client || pkg.name !== NAMES[client]
|
|
451
|
+
|| (MAC_CLIENT_VERSION.test(version) && (!clientUpdateCatalogId(client, "darwin") || pkg.os?.[0] !== "darwin" || pkg.cpu?.[0] !== "arm64"))
|
|
441
452
|
|| manifest.contract !== "hellolynk-client-npm-pack-manifest-v1" || manifest.client !== client
|
|
442
453
|
|| manifest.packageName !== pkg.name || manifest.version !== version
|
|
443
454
|
|| pin.contract !== "hellolynk-client-npm-pack-pin-v1" || pin.manifestSha256 !== sha(manifestBytes)) throw failure("package_identity_invalid");
|
|
444
455
|
await verifyRecords(root, manifest.files, ["hellolynk-client-pack-manifest.json", "hellolynk-client-pack-pin.json"]);
|
|
445
456
|
if (!manifest.files.some((file) => file.path === "bin/hellolynk-client-setup.mjs")) throw failure("package_pin_invalid");
|
|
457
|
+
if (MAC_CLIENT_VERSION.test(version)) {
|
|
458
|
+
const helpers = client === "codex"
|
|
459
|
+
? ["plugins/hellolynk-codex/runtime/platform/darwin", "plugins/hellolynk-codex/runtime/bootstrap/platform/darwin"]
|
|
460
|
+
: ["runtime/platform/darwin"];
|
|
461
|
+
for (const relative of helpers) {
|
|
462
|
+
const file = `${relative}/hellolynk-keychain-helper.bin`;
|
|
463
|
+
if (!manifest.files.some(record => record.path === file)
|
|
464
|
+
|| (process.platform !== "win32" && !((await fs.lstat(path.join(root, file))).mode & 0o100))) throw failure("package_pin_invalid");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return { coreRoot: pkg.hellolynkClient.coreRoot, files: manifest.files };
|
|
446
468
|
}
|
|
447
469
|
async function verifyRecords(root, records, allowedExtra) {
|
|
448
470
|
if (!Array.isArray(records) || records.length < 1 || records.length > 5000) throw failure("package_pin_invalid");
|
|
@@ -471,7 +493,9 @@ async function verifyInstalledContext(config) {
|
|
|
471
493
|
await verifyPackage(context.packageRoot, config.client, config.currentVersion);
|
|
472
494
|
}
|
|
473
495
|
function normalize(value) {
|
|
496
|
+
const platform = value.platform || process.platform;
|
|
474
497
|
if (!CLIENT_UPDATE_CLIENTS.includes(value.client) || typeof value.currentVersion !== "string" || !VERSION.test(value.currentVersion)
|
|
498
|
+
|| (platform === "darwin" && !MAC_CLIENT_VERSION.test(value.currentVersion))
|
|
475
499
|
|| !path.isAbsolute(String(value.root || "")) || !path.isAbsolute(String(value.installContext?.packageRoot || ""))) throw failure("configuration_invalid");
|
|
476
500
|
const context = { packageRoot: path.resolve(value.installContext.packageRoot) };
|
|
477
501
|
for (const name of ["nodePath", "dshProfile", "dshRoot"]) {
|
|
@@ -484,7 +508,7 @@ function normalize(value) {
|
|
|
484
508
|
if (!HASH.test(value.installContext.expectedInstalledManifestSha256)) throw failure("configuration_invalid");
|
|
485
509
|
context.expectedInstalledManifestSha256 = value.installContext.expectedInstalledManifestSha256;
|
|
486
510
|
}
|
|
487
|
-
return { root: path.resolve(value.root), client: value.client, currentVersion: value.currentVersion, installContext: context };
|
|
511
|
+
return { root: path.resolve(value.root), client: value.client, currentVersion: value.currentVersion, platform, installContext: context };
|
|
488
512
|
}
|
|
489
513
|
async function launchWorker(config) {
|
|
490
514
|
// A host may execute this module from an ephemeral captured import graph.
|
|
@@ -510,8 +534,9 @@ async function launchWorker(config) {
|
|
|
510
534
|
async function runInstaller({ executable, args, cwd, root, timeoutMs }) {
|
|
511
535
|
executable = await runtimeSafety.resolveHelloLynkStandaloneNode({ preferredExecutable: executable });
|
|
512
536
|
const env = { ...runtimeSafety.standaloneHelloLynkNodeEnvironment(process.env), HELLOLYNK_NEUTRAL_STORE_ROOT: root };
|
|
537
|
+
const mac = process.platform === "darwin";
|
|
513
538
|
return new Promise((resolve, reject) => {
|
|
514
|
-
const child = spawn(executable, args, { cwd, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
539
|
+
const child = spawn(executable, args, { cwd, env, detached: mac, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
515
540
|
let stdout = "", stderr = "", finished = false, terminalError = null;
|
|
516
541
|
const timer = setTimeout(() => terminate(failure("installer_timeout")), timeoutMs);
|
|
517
542
|
function finish(error, value) { if (finished) return; finished = true; clearTimeout(timer); error ? reject(error) : resolve(value); }
|
|
@@ -529,6 +554,9 @@ async function runInstaller({ executable, args, cwd, root, timeoutMs }) {
|
|
|
529
554
|
killer.on("error", () => { clearTimeout(killTimer); if (!finished) killChild(); });
|
|
530
555
|
killer.once("close", () => { clearTimeout(killTimer); if (!finished) killChild(); });
|
|
531
556
|
} catch { killChild(); }
|
|
557
|
+
} else if (mac && child.pid) {
|
|
558
|
+
// The installer owns this process group, including a synchronous Codex CLI.
|
|
559
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch { killChild(); }
|
|
532
560
|
} else killChild();
|
|
533
561
|
}
|
|
534
562
|
child.on("error", () => {
|
|
@@ -580,7 +608,8 @@ function damaged(error) { return error instanceof SyntaxError || error?.code ===
|
|
|
580
608
|
// Windows paths compare case-insensitively and may differ by a link or short name.
|
|
581
609
|
async function samePath(left, right) {
|
|
582
610
|
if (typeof left !== "string" || typeof right !== "string" || !path.isAbsolute(left) || !path.isAbsolute(right)) return false;
|
|
583
|
-
const
|
|
611
|
+
const canonical = (value) => process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
612
|
+
const same = (a, b) => canonical(a) === canonical(b);
|
|
584
613
|
if (same(left, right)) return true;
|
|
585
614
|
try { return same(await fs.realpath(left), await fs.realpath(right)); } catch { return false; }
|
|
586
615
|
}
|