Skip to content

Commit f6e419e

Browse files
fix(cli): use a healthy pinned Sherpa copy beside the CLI (#5095)
* docs(cli): map the pinned Sherpa runtime loading contract * fix(cli): use a healthy pinned Sherpa copy beside the CLI * chore(cli): keep runtime loader comments within the ratchet * docs(cli): wrap the runtime contract palette declarations * chore(cli): keep the Sherpa contract note out of the repo
1 parent d09e003 commit f6e419e

9 files changed

Lines changed: 503 additions & 112 deletions

File tree

‎packages/cli/src/commands/models.test.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import { CliRuntimeError, consumeCommandResult } from "../utils/commandResult.js";
33

4+
const cachedRuntimePath =
5+
"/cache/optional/sherpa-onnx-node@1.13.8/node_modules/sherpa-onnx-node/index.js";
46
const sherpa = {
57
SHERPA_RUNTIME_DIR: "/cache/optional/sherpa-onnx-node@1.13.8",
68
PARAKEET_MODEL_DIR: "/cache/parakeet/parakeet-tdt-0.6b-v3-int8",
@@ -44,7 +46,10 @@ describe("models install parakeet --json", () => {
4446
consumeCommandResult();
4547
vi.spyOn(console, "log").mockImplementation(() => {});
4648
sherpa.sherpaUnsupportedReason.mockReturnValue(null);
47-
sherpa.installSherpaRuntime.mockResolvedValue(true);
49+
sherpa.installSherpaRuntime.mockResolvedValue({
50+
installed: true,
51+
runtimePath: cachedRuntimePath,
52+
});
4853
sherpa.ensureParakeetModel.mockResolvedValue(false);
4954
});
5055
afterEach(() => vi.restoreAllMocks());
@@ -58,17 +63,32 @@ describe("models install parakeet --json", () => {
5863
model: "parakeet-tdt-0.6b-v3",
5964
changed: true,
6065
runtimeDir: sherpa.SHERPA_RUNTIME_DIR,
66+
runtimePath: cachedRuntimePath,
6167
modelDir: sherpa.PARAKEET_MODEL_DIR,
6268
},
6369
});
6470
expect(dispose).toHaveBeenCalled();
6571
});
6672

6773
it("reports changed: false when the runtime loads and the model verifies", async () => {
68-
sherpa.installSherpaRuntime.mockResolvedValue(false);
74+
sherpa.installSherpaRuntime.mockResolvedValue({
75+
installed: false,
76+
runtimePath: cachedRuntimePath,
77+
});
6978
expect((await install()).out).toMatchObject({ ok: true, changed: false });
7079
});
7180

81+
it("reports a selected beside copy while retaining the cache destination", async () => {
82+
const runtimePath = "/bundle/node_modules/sherpa-onnx-node/index.js";
83+
sherpa.installSherpaRuntime.mockResolvedValue({ installed: false, runtimePath });
84+
expect((await install()).out).toMatchObject({
85+
ok: true,
86+
changed: false,
87+
runtimeDir: sherpa.SHERPA_RUNTIME_DIR,
88+
runtimePath,
89+
});
90+
});
91+
7292
it("reports a failed install as ok:false with exit 1", async () => {
7393
sherpa.ensureParakeetModel.mockRejectedValue(new Error("tokens.txt did not match"));
7494
expect(await install()).toEqual({

‎packages/cli/src/commands/models.ts‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,18 @@ function downloadProgress(spin: Spinner) {
4343
type Sherpa = typeof import("../whisper/sherpa.js");
4444
type Spinner = ReturnType<typeof clack.spinner> | null;
4545

46-
/** Installs what is missing; true when anything changed. */
46+
/** Installs missing pieces and carries the selected runtime provenance into the result. */
4747
async function installMissing(sherpa: Sherpa, spin: Spinner, signal: AbortSignal) {
48-
const runtimeInstalled = await sherpa.installSherpaRuntime({ signal });
48+
const runtime = await sherpa.installSherpaRuntime({ signal });
4949
spin?.message("Verifying the Parakeet model...");
5050
const modelFetched = await sherpa.ensureParakeetModel({
5151
signal,
5252
onBytes: downloadProgress(spin),
5353
});
54-
return runtimeInstalled || modelFetched;
54+
return { changed: runtime.installed || modelFetched, runtimePath: runtime.runtimePath };
5555
}
5656

57-
/** A synchronous child's Ctrl-C shows as its error before the scope's listener runs. */
57+
/** Cancellation can reach the child before the scope observes the signal. */
5858
const wasCancelled = (err: unknown, signal: AbortSignal, sherpa: Sherpa) =>
5959
signal.aborted || err instanceof sherpa.DecodeCancelled;
6060

@@ -68,12 +68,19 @@ async function installParakeet(json: boolean): Promise<void> {
6868
const cancellation = createRenderCancellationScope();
6969
spin?.start("Checking the sherpa-onnx runtime (installing it from npm if it does not load)...");
7070
try {
71-
const changed = await installMissing(sherpa, spin, cancellation.signal);
71+
const { changed, runtimePath } = await installMissing(sherpa, spin, cancellation.signal);
7272
spin?.stop(c.success(changed ? "Parakeet installed" : "Parakeet is already installed"));
7373
if (json) {
7474
const { SHERPA_RUNTIME_DIR: runtimeDir, PARAKEET_MODEL_DIR: modelDir } = sherpa;
7575
console.log(
76-
JSON.stringify({ ok: true, model: PARAKEET_MODEL_LABEL, changed, runtimeDir, modelDir }),
76+
JSON.stringify({
77+
ok: true,
78+
model: PARAKEET_MODEL_LABEL,
79+
changed,
80+
runtimeDir,
81+
runtimePath,
82+
modelDir,
83+
}),
7784
);
7885
}
7986
} catch (err) {

‎packages/cli/src/commands/transcribe.test.ts‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { runCommand } from "citty";
22
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3-
import { chmodSync, existsSync, writeFileSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
3+
import {
4+
chmodSync,
5+
mkdirSync,
6+
existsSync,
7+
writeFileSync,
8+
readFileSync,
9+
mkdtempSync,
10+
rmSync,
11+
} from "node:fs";
412
import { join } from "node:path";
513
import { tmpdir } from "node:os";
14+
import { pathToFileURL } from "node:url";
615
import { WhisperUnavailableError } from "../whisper/manager.js";
716
import { CliRuntimeError, consumeCommandResult } from "../utils/commandResult.js";
817

@@ -18,10 +27,24 @@ vi.mock("../whisper/transcribe.js", () => ({
1827

1928
// Engine selection: which runners look installed, and the sherpa decode child it spawns.
2029
const runners = { sherpa: false, mlx: false };
21-
vi.mock("../whisper/sherpa.js", async (importOriginal) => ({
22-
...(await importOriginal<typeof import("../whisper/sherpa.js")>()),
23-
sherpaParakeetInstalled: () => runners.sherpa,
24-
}));
30+
let runtimeDir: string;
31+
vi.mock("../whisper/sherpa.js", async (importOriginal) => {
32+
const sherpa = await importOriginal<typeof import("../whisper/sherpa.js")>();
33+
return {
34+
...sherpa,
35+
sherpaParakeetInstalled: () => runners.sherpa,
36+
transcribeWithSherpa: (
37+
wavPath: string,
38+
dir: string,
39+
options: Parameters<typeof sherpa.transcribeWithSherpa>[2],
40+
) =>
41+
sherpa.transcribeWithSherpa(wavPath, dir, {
42+
...options,
43+
runtimeDir,
44+
cliUrl: pathToFileURL(join(runtimeDir, "dist", "cli.js")).href,
45+
}),
46+
};
47+
});
2548
const mlxMock = vi.fn();
2649
vi.mock("../whisper/parakeet.js", async (importOriginal) => ({
2750
...(await importOriginal<typeof import("../whisper/parakeet.js")>()),
@@ -165,6 +188,12 @@ describe("transcribe command", () => {
165188

166189
describe("engine selection", () => {
167190
beforeEach(() => {
191+
runtimeDir = mkdtempSync(join(tmpdir(), "hf-transcribe-runtime-"));
192+
dirs.push(runtimeDir);
193+
const pkg = join(runtimeDir, "node_modules", "sherpa-onnx-node");
194+
mkdirSync(pkg, { recursive: true });
195+
writeFileSync(join(pkg, "package.json"), JSON.stringify({ main: "index.js" }));
196+
writeFileSync(join(pkg, "index.js"), "module.exports = {};");
168197
transcribeMock.mockImplementation(async (_in: string, dir: string) =>
169198
fakeTranscript(dir, "whisper"),
170199
);

‎packages/cli/src/utils/optionalPackages.test.ts‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ describe("loadOptionalPackage", () => {
132132

133133
describe("a copy installed beside the CLI", () => {
134134
// node_modules/hyperframes/dist/cli.js with onnxruntime-node installed next to hyperframes.
135-
function layout(version: string) {
135+
function layout(version: string, name = "onnxruntime-node") {
136136
const root = mkdtempSync(join(tmpdir(), "hf-beside-"));
137-
const pkg = join(root, "node_modules", "onnxruntime-node");
137+
const pkg = join(root, "node_modules", name);
138138
mkdirSync(pkg, { recursive: true });
139139
writeFileSync(join(pkg, "package.json"), JSON.stringify({ version, main: "index.js" }));
140140
writeFileSync(join(pkg, "index.js"), `module.exports = { copy: "beside ${version}" };`);
@@ -153,6 +153,27 @@ describe("a copy installed beside the CLI", () => {
153153
}
154154
});
155155

156+
it.each(["1.13.8", "1.13.7"])("accepts only the Sherpa 1.13.8 pin, received %s", (version) => {
157+
const { root, cliUrl } = layout(version, "sherpa-onnx-node");
158+
try {
159+
const result = loadBesideCli("sherpa-onnx-node", cliUrl);
160+
if (version === "1.13.8") expect(result).toEqual({ copy: "beside 1.13.8" });
161+
else expect(result).toBeNull();
162+
} finally {
163+
rmSync(root, { recursive: true, force: true });
164+
}
165+
});
166+
167+
it("keeps a missing optional-package entry eligible for cache fallback", () => {
168+
const { root, cliUrl } = layout("1.21.1");
169+
try {
170+
rmSync(join(root, "node_modules", "onnxruntime-node", "index.js"));
171+
expect(loadBesideCli("onnxruntime-node", cliUrl)).toBeNull();
172+
} finally {
173+
rmSync(root, { recursive: true, force: true });
174+
}
175+
});
176+
156177
it("ignores it when its manifest is unreadable, instead of throwing", () => {
157178
const { root, cliUrl } = layout(OPTIONAL_PACKAGES["onnxruntime-node"]);
158179
writeFileSync(join(root, "node_modules", "onnxruntime-node", "package.json"), "{ version: 1");

‎packages/cli/src/utils/optionalPackages.ts‎

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export const OPTIONAL_PACKAGES = {
3030
"@google/genai": "1.52.0",
3131
} as const satisfies Record<OptionalPackage, string>;
3232

33+
export const PINNED_PACKAGES = {
34+
...OPTIONAL_PACKAGES,
35+
"sherpa-onnx-node": "1.13.8",
36+
} as const;
37+
38+
type PinnedPackage = keyof typeof PINNED_PACKAGES;
39+
3340
export const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "optional");
3441

3542
export interface OptionalPackageDeps {
@@ -53,7 +60,7 @@ export function installedOptionalPackageVersion(
5360
cacheDir = CACHE_DIR,
5461
cliUrl = import.meta.url,
5562
): string | null {
56-
if (pinnedCopyBesideCli(name, cliUrl)) return OPTIONAL_PACKAGES[name];
63+
if (pinnedPackageBesideCli(name, cliUrl)?.resolution === "entry") return OPTIONAL_PACKAGES[name];
5764
const dir = optionalPackageDir(name, cacheDir);
5865
if (!isInstalled(dir, name)) return null;
5966
return (JSON.parse(readFileSync(manifestPath(dir, name), "utf-8")) as { version: string })
@@ -113,28 +120,51 @@ export function isInstalled(dir: string, name: string): boolean {
113120
return existsSync(manifestPath(dir, name));
114121
}
115122

116-
export function loadInstalled(dir: string, name: string): unknown | null {
123+
export function installedPackagePath(dir: string, name: string): string | null {
117124
if (!isInstalled(dir, name)) return null;
118-
return createRequire(join(dir, "package.json"))(name);
125+
return createRequire(join(dir, "package.json")).resolve(name);
119126
}
120127

121-
function pinnedCopyBesideCli(name: OptionalPackage, cliUrl: string): boolean {
128+
function loadInstalled(dir: string, name: string): unknown | null {
129+
const entry = installedPackagePath(dir, name);
130+
return entry === null ? null : createRequire(join(dir, "package.json"))(entry);
131+
}
132+
133+
export function pinnedPackageBesideCli(
134+
name: PinnedPackage,
135+
cliUrl = import.meta.url,
136+
): { resolution: "entry" | "package"; path: string } | null {
122137
const req = createRequire(cliUrl);
123138
try {
124-
const entry = realpathSync(req.resolve(name));
139+
let entry: string;
140+
try {
141+
entry = realpathSync(req.resolve(name));
142+
} catch {
143+
const copy = (req.resolve.paths(name) ?? [])
144+
.map((dir) => join(dir, name))
145+
.find((dir) => existsSync(join(dir, "package.json")));
146+
return copy && hasPinnedVersion(copy, name)
147+
? { resolution: "package", path: realpathSync(copy) }
148+
: null;
149+
}
125150
const copy = (req.resolve.paths(name) ?? [])
126151
.map((dir) => join(dir, name))
127152
.find((dir) => existsSync(dir) && entry.startsWith(realpathSync(dir) + sep));
128-
if (!copy) return false;
129-
const manifest = readFileSync(join(copy, "package.json"), "utf-8");
130-
return (JSON.parse(manifest) as { version?: string }).version === OPTIONAL_PACKAGES[name];
153+
if (!copy) return null;
154+
return hasPinnedVersion(copy, name) ? { resolution: "entry", path: entry } : null;
131155
} catch {
132-
return false;
156+
return null;
133157
}
134158
}
135159

136-
export function loadBesideCli(name: OptionalPackage, cliUrl = import.meta.url): unknown | null {
137-
return pinnedCopyBesideCli(name, cliUrl) ? createRequire(cliUrl)(name) : null;
160+
function hasPinnedVersion(copy: string, name: PinnedPackage): boolean {
161+
const manifest = readFileSync(join(copy, "package.json"), "utf-8");
162+
return (JSON.parse(manifest) as { version?: string }).version === PINNED_PACKAGES[name];
163+
}
164+
165+
export function loadBesideCli(name: PinnedPackage, cliUrl = import.meta.url): unknown | null {
166+
const copy = pinnedPackageBesideCli(name, cliUrl);
167+
return copy?.resolution === "entry" ? createRequire(cliUrl)(copy.path) : null;
138168
}
139169

140170
export function runNpm(args: string[], signal?: AbortSignal): Promise<void> {

0 commit comments

Comments
 (0)