From d3b52b87bf369cd72c4b565806a15ee0fdb84969 Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Tue, 9 Jun 2026 02:07:52 +0000 Subject: [PATCH 1/3] fix(embeddings): retry install with --ignore-scripts on native build failure On Node v26 macOS, sharp has no prebuilt binary and fails to build from source without node-gyp. The first npm install attempt is kept as-is so platforms with prebuilt binaries work normally. On failure we retry with --ignore-scripts (sharp is image-only; text embeddings are unaffected), then explicitly re-run onnxruntime-node's postinstall so its binary is still downloaded. --- src/cli/embeddings.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/cli/embeddings.ts b/src/cli/embeddings.ts index 9f1ed3f6..7bd9e6a1 100644 --- a/src/cli/embeddings.ts +++ b/src/cli/embeddings.ts @@ -113,10 +113,26 @@ function ensureSharedDeps(): void { private: true, dependencies: { [TRANSFORMERS_PKG]: TRANSFORMERS_RANGE }, }); - execFileSync("npm", ["install", "--omit=dev", "--no-package-lock", "--no-audit", "--no-fund"], { - cwd: SHARED_DIR, - stdio: "inherit", - }); + const npmArgs = ["install", "--omit=dev", "--no-package-lock", "--no-audit", "--no-fund"]; + try { + execFileSync("npm", npmArgs, { cwd: SHARED_DIR, stdio: "inherit" }); + } catch { + // sharp has no prebuilt binary for some Node versions (e.g. Node v26 on macOS) + // and requires node-gyp to build from source. Retry without scripts — sharp is + // only used for image processing; text embeddings are unaffected. + warn(` Embeddings native build failed; retrying without install scripts`); + warn(` Embeddings (sharp image support unavailable — text embeddings unaffected)`); + execFileSync("npm", [...npmArgs, "--ignore-scripts"], { cwd: SHARED_DIR, stdio: "inherit" }); + // onnxruntime-node's postinstall downloads its prebuilt binary; re-run it + // explicitly since --ignore-scripts skipped it. + const onnxScript = join(SHARED_NODE_MODULES, "onnxruntime-node", "script", "install.js"); + if (existsSync(onnxScript)) { + execFileSync("node", [onnxScript], { + cwd: join(SHARED_NODE_MODULES, "onnxruntime-node"), + stdio: "inherit", + }); + } + } } else { log(` Embeddings shared deps already present at ${SHARED_DIR}`); } From 61a0b1325c334f17416ee3e7304b4d9294db5e3f Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Tue, 9 Jun 2026 02:18:09 +0000 Subject: [PATCH 2/3] fix(publish): include embeddings/ dir in npm files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit embeddings/embed-daemon.js is built by esbuild but was missing from the files array, so it was never published to npm. hivemind embeddings install copies this file to ~/.hivemind/embed-deps/embed-daemon.js — without it the daemon is absent and no embeddings are generated. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index df9c1008..4e1644c1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "files": [ "bundle", + "embeddings", "codex/bundle", "codex/skills", "cursor/bundle", From 1bfe89757b4a1e994bc38bd22425d24155ec809b Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Tue, 9 Jun 2026 02:34:35 +0000 Subject: [PATCH 3/3] test(embeddings): cover _installWithFallback retry paths + apply CodeRabbit fixes Extract install-with-fallback logic into exported _installWithFallback so it can be tested with an injected exec function. Tests cover all 5 branches: happy path, retry on native build failure, onnxruntime script re-run, onnxruntime script failure (warns, no throw), and hard-fail (throws). Also applies CodeRabbit feedback: log original error in catch, wrap retry execFileSync in its own try/catch with a descriptive throw, and wrap the onnxruntime script execution in try/catch with a user-visible warning. --- src/cli/embeddings.ts | 62 +++++++++++++++++-------- tests/cli/cli-embeddings.test.ts | 79 ++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/cli/embeddings.ts b/src/cli/embeddings.ts index 7bd9e6a1..61334fce 100644 --- a/src/cli/embeddings.ts +++ b/src/cli/embeddings.ts @@ -102,6 +102,47 @@ function isSymbolicLink(path: string): boolean { try { return lstatSync(path).isSymbolicLink(); } catch { return false; } } +type ExecFileSyncFn = typeof execFileSync; + +/** + * Run `npm install` for the shared embedding deps, falling back to + * `--ignore-scripts` if the first attempt fails (e.g. sharp has no prebuilt + * binary for Node v26 on macOS and requires node-gyp). After a successful + * fallback, explicitly re-runs onnxruntime-node's postinstall so its binary + * is still downloaded. Exported for unit testing. + */ +export function _installWithFallback( + sharedDir: string, + sharedNodeModules: string, + exec: ExecFileSyncFn = execFileSync, +): void { + const npmArgs = ["install", "--omit=dev", "--no-package-lock", "--no-audit", "--no-fund"]; + try { + exec("npm", npmArgs, { cwd: sharedDir, stdio: "inherit" }); + } catch (err) { + warn(` Embeddings install failed (${err instanceof Error ? err.message : String(err)}); retrying without install scripts`); + warn(` Embeddings (sharp image support unavailable — text embeddings unaffected)`); + try { + exec("npm", [...npmArgs, "--ignore-scripts"], { cwd: sharedDir, stdio: "inherit" }); + } catch (retryErr) { + throw new Error( + `Embedding deps install failed even without scripts: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}` + ); + } + const onnxScript = join(sharedNodeModules, "onnxruntime-node", "script", "install.js"); + if (existsSync(onnxScript)) { + try { + exec("node", [onnxScript], { + cwd: join(sharedNodeModules, "onnxruntime-node"), + stdio: "inherit", + }); + } catch (onnxErr) { + warn(` Embeddings onnxruntime-node install script failed — embeddings may not work: ${onnxErr instanceof Error ? onnxErr.message : String(onnxErr)}`); + } + } + } +} + function ensureSharedDeps(): void { if (!isSharedDepsInstalled()) { log(` Embeddings installing ${TRANSFORMERS_PKG}@${TRANSFORMERS_RANGE} into ${SHARED_DIR}`); @@ -113,26 +154,7 @@ function ensureSharedDeps(): void { private: true, dependencies: { [TRANSFORMERS_PKG]: TRANSFORMERS_RANGE }, }); - const npmArgs = ["install", "--omit=dev", "--no-package-lock", "--no-audit", "--no-fund"]; - try { - execFileSync("npm", npmArgs, { cwd: SHARED_DIR, stdio: "inherit" }); - } catch { - // sharp has no prebuilt binary for some Node versions (e.g. Node v26 on macOS) - // and requires node-gyp to build from source. Retry without scripts — sharp is - // only used for image processing; text embeddings are unaffected. - warn(` Embeddings native build failed; retrying without install scripts`); - warn(` Embeddings (sharp image support unavailable — text embeddings unaffected)`); - execFileSync("npm", [...npmArgs, "--ignore-scripts"], { cwd: SHARED_DIR, stdio: "inherit" }); - // onnxruntime-node's postinstall downloads its prebuilt binary; re-run it - // explicitly since --ignore-scripts skipped it. - const onnxScript = join(SHARED_NODE_MODULES, "onnxruntime-node", "script", "install.js"); - if (existsSync(onnxScript)) { - execFileSync("node", [onnxScript], { - cwd: join(SHARED_NODE_MODULES, "onnxruntime-node"), - stdio: "inherit", - }); - } - } + _installWithFallback(SHARED_DIR, SHARED_NODE_MODULES); } else { log(` Embeddings shared deps already present at ${SHARED_DIR}`); } diff --git a/tests/cli/cli-embeddings.test.ts b/tests/cli/cli-embeddings.test.ts index afa66c06..721c98c3 100644 --- a/tests/cli/cli-embeddings.test.ts +++ b/tests/cli/cli-embeddings.test.ts @@ -15,6 +15,7 @@ import { TRANSFORMERS_PKG, uninstallEmbeddings, _linkAgentForTesting, + _installWithFallback, } from "../../src/cli/embeddings.js"; import { _resetUserConfigForTesting, _setConfigPathForTesting, getEmbeddingsEnabled } from "../../src/user-config.js"; @@ -327,3 +328,81 @@ describe("uninstallEmbeddings — config flag side effect", () => { expect(getEmbeddingsEnabled()).toBe(false); }); }); + +// ── _installWithFallback ────────────────────────────────────────────────── + +describe("_installWithFallback — npm install + fallback logic", () => { + it("happy path: calls npm install once with standard args and never retries", () => { + const exec = vi.fn(); + _installWithFallback("/fake/shared", "/fake/shared/node_modules", exec); + expect(exec).toHaveBeenCalledTimes(1); + expect(exec).toHaveBeenCalledWith( + "npm", + ["install", "--omit=dev", "--no-package-lock", "--no-audit", "--no-fund"], + expect.objectContaining({ cwd: "/fake/shared" }), + ); + }); + + it("retry path: first call fails → retries with --ignore-scripts", () => { + let call = 0; + const exec = vi.fn(() => { + if (call++ === 0) throw new Error("sharp build failed"); + }) as any; + _installWithFallback("/fake/shared", "/fake/nm", exec); + expect(exec).toHaveBeenCalledTimes(2); + const [, secondArgs] = exec.mock.calls[1] as [string, string[]]; + expect(secondArgs).toContain("--ignore-scripts"); + }); + + it("retry path: runs onnxruntime install script when it exists after --ignore-scripts", () => { + const sharedNm = join(tmpHome, "node_modules"); + const onnxDir = join(sharedNm, "onnxruntime-node", "script"); + mkdirSync(onnxDir, { recursive: true }); + writeFileSync(join(onnxDir, "install.js"), "// stub"); + + let call = 0; + const exec = vi.fn(() => { + if (call++ === 0) throw new Error("sharp failed"); + }) as any; + + _installWithFallback(tmpHome, sharedNm, exec); + + // npm install (fail) + npm install --ignore-scripts + node onnxScript + expect(exec).toHaveBeenCalledTimes(3); + const [thirdCmd, thirdArgs] = exec.mock.calls[2] as [string, string[]]; + expect(thirdCmd).toBe("node"); + expect(thirdArgs[0]).toContain("onnxruntime-node"); + expect(thirdArgs[0]).toContain("install.js"); + }); + + it("retry path: onnxruntime script failure warns but does not throw", () => { + const sharedNm = join(tmpHome, "node_modules"); + const onnxDir = join(sharedNm, "onnxruntime-node", "script"); + mkdirSync(onnxDir, { recursive: true }); + writeFileSync(join(onnxDir, "install.js"), "// stub"); + + let call = 0; + const exec = vi.fn(() => { + if (call++ !== 1) throw new Error("fail"); // npm fail, onnx fail; retry succeeds + }) as any; + + expect(() => _installWithFallback(tmpHome, sharedNm, exec)).not.toThrow(); + }); + + it("hard-fail path: throws when retry with --ignore-scripts also fails", () => { + const exec = vi.fn(() => { throw new Error("disk full"); }) as any; + expect(() => _installWithFallback("/fake/shared", "/fake/nm", exec)) + .toThrow("Embedding deps install failed even without scripts"); + }); + + it("no onnxruntime script: skips the third exec call silently", () => { + // onnxruntime dir doesn't exist — existsSync returns false + let call = 0; + const exec = vi.fn(() => { + if (call++ === 0) throw new Error("sharp failed"); + }) as any; + _installWithFallback("/fake/shared", join(tmpHome, "absent-nm"), exec); + // only 2 calls: first npm (fail) + retry npm --ignore-scripts + expect(exec).toHaveBeenCalledTimes(2); + }); +});