Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"files": [
"bundle",
"embeddings",
"codex/bundle",
"codex/skills",
"cursor/bundle",
Expand Down
46 changes: 42 additions & 4 deletions src/cli/embeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -113,10 +154,7 @@ 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",
});
_installWithFallback(SHARED_DIR, SHARED_NODE_MODULES);
} else {
log(` Embeddings shared deps already present at ${SHARED_DIR}`);
}
Expand Down
79 changes: 79 additions & 0 deletions tests/cli/cli-embeddings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
TRANSFORMERS_PKG,
uninstallEmbeddings,
_linkAgentForTesting,
_installWithFallback,
} from "../../src/cli/embeddings.js";
import { _resetUserConfigForTesting, _setConfigPathForTesting, getEmbeddingsEnabled } from "../../src/user-config.js";

Expand Down Expand Up @@ -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);
});
});
Loading