@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.
Files changed (48) hide show
  1. package/bin/hellolynk-client-setup.mjs +79 -13
  2. package/clients/core/hellolynk-client-account.mjs +1 -1
  3. package/clients/core/hellolynk-client-profile.mjs +25 -2
  4. package/clients/core/hellolynk-client-update-entry.mjs +7 -3
  5. package/clients/core/hellolynk-client-update-runtime.mjs +62 -33
  6. package/clients/core/hellolynk-client-update.mjs +62 -18
  7. package/clients/core/hellolynk-darwin-host.mjs +269 -0
  8. package/clients/core/hellolynk-darwin-vault.mjs +153 -0
  9. package/clients/core/hellolynk-host-protected-store.mjs +20 -8
  10. package/clients/core/hellolynk-human-confirmation-ledger.mjs +53 -6
  11. package/clients/core/hellolynk-local-data-protection.mjs +22 -7
  12. package/clients/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
  13. package/clients/core/hellolynk-neutral-host-installation-store.mjs +3 -7
  14. package/clients/core/hellolynk-neutral-host-launcher.mjs +9 -3
  15. package/clients/core/hellolynk-neutral-host-process.mjs +1 -1
  16. package/clients/core/hellolynk-platform-credential-store.mjs +68 -14
  17. package/clients/core/hellolynk-release-retention.mjs +101 -2
  18. package/clients/core/hellolynk-workbench-connection.mjs +5 -3
  19. package/clients/platform/windows/credential-store.ps1 +46 -10
  20. package/hellolynk-client-pack-manifest.json +101 -81
  21. package/hellolynk-client-pack-pin.json +1 -1
  22. package/integrations/workbuddy/package.json +1 -1
  23. package/package.json +1 -1
  24. package/runtime/connect-activate.mjs +5 -1
  25. package/runtime/core/hellolynk-client-account.mjs +1 -1
  26. package/runtime/core/hellolynk-client-profile.mjs +25 -2
  27. package/runtime/core/hellolynk-client-update-entry.mjs +7 -3
  28. package/runtime/core/hellolynk-client-update-runtime.mjs +62 -33
  29. package/runtime/core/hellolynk-client-update.mjs +62 -18
  30. package/runtime/core/hellolynk-darwin-host.mjs +269 -0
  31. package/runtime/core/hellolynk-darwin-vault.mjs +153 -0
  32. package/runtime/core/hellolynk-host-protected-store.mjs +20 -8
  33. package/runtime/core/hellolynk-human-confirmation-ledger.mjs +53 -6
  34. package/runtime/core/hellolynk-local-data-protection.mjs +22 -7
  35. package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +7 -5
  36. package/runtime/core/hellolynk-neutral-host-installation-store.mjs +3 -7
  37. package/runtime/core/hellolynk-neutral-host-launcher.mjs +9 -3
  38. package/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
  39. package/runtime/core/hellolynk-platform-credential-store.mjs +68 -14
  40. package/runtime/core/hellolynk-release-retention.mjs +101 -2
  41. package/runtime/core/hellolynk-workbench-connection.mjs +5 -3
  42. package/runtime/neutral-host-release-loader.mjs +1 -1
  43. package/runtime/neutral-host-release-manifest.json +26 -18
  44. package/runtime/neutral-host-release-pin.json +1 -1
  45. package/runtime/platform/windows/credential-store.ps1 +46 -10
  46. package/tools/hellolynk-activate-neutral-host-clients.mjs +5 -1
  47. package/workbuddy-package-manifest.json +1 -1
  48. 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 jsonOutput = process.argv.includes("--json") || process.stdout.isTTY !== true;
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
- if (await exists(path.join(locations.hostRoot, "capabilities-v1.dpapi.clixml"))) {
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 (process.platform !== "win32") fail("hellolynk_npm_windows_only");
843
- const home = path.resolve(process.env.USERPROFILE || os.homedir());
844
- const localAppData = path.resolve(process.env.LOCALAPPDATA || path.join(home, "AppData", "Local"));
845
- const appData = path.resolve(process.env.APPDATA || path.join(home, "AppData", "Roaming"));
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 || path.join(localAppData, "HelloLynk", "host"));
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
- return `${[`HelloLynk for ${CLIENT_NAMES[metadata.client]}${say("(Windows,Node.js 22.16.0+)", " (Windows, Node.js 22.16.0+)")}`,
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("win32", options?.env || process.env);
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") return resolve(String(env.LOCALAPPDATA || join(home, "AppData", "Local")), "HelloLynk");
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
- if (process.platform !== "win32" || !path.isAbsolute(String(packageRoot || ""))) return Promise.resolve(null);
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
- if (manifest.hellolynkClient?.client !== client
25
- || !/^\d+\.\d+\.\d+$/.test(manifest.version || "")) return null;
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, CLIENT_UPDATE_CLIENTS } from "./hellolynk-client-update.mjs";
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) !== "win32") return disabled();
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 worker = await readJson(path.join(directory, "worker.json"), null).catch((error) => { if (!damaged(error)) throw error; return null; });
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(path.join(directory, "worker.json"), { client,
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(path.join(directory, "worker.json"), { client, pid });
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, "releases", `${record.version}-${record.sha256}`, record.filename);
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.json.
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
- let packageRoot;
414
- if (config.client === "dsh") {
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 same = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
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
  }