@program-video/cli 0.1.12 → 0.2.1

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.
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/binary-manager.ts
4
+ import { execFile } from "child_process";
5
+ import { createWriteStream } from "fs";
6
+ import { access, chmod, mkdir, rename, rm, stat } from "fs/promises";
7
+ import { arch, homedir, platform } from "os";
8
+ import { join } from "path";
9
+ import { pipeline } from "stream/promises";
10
+ import { promisify } from "util";
11
+ import { createGunzip } from "zlib";
12
+ var execFileAsync = promisify(execFile);
13
+ var BIN_DIR = join(homedir(), ".program", "bin");
14
+ var LIB_DIR = join(homedir(), ".program", "lib");
15
+ var MODELS_DIR = join(homedir(), ".program", "models");
16
+ function getPlatformKey() {
17
+ const os = platform();
18
+ const cpu = arch();
19
+ if (os === "darwin" && cpu === "arm64") return "darwin_arm64";
20
+ if (os === "darwin" && cpu === "x64") return "darwin_x64";
21
+ if (os === "linux" && cpu === "x64") return "linux_x64";
22
+ if (os === "linux" && cpu === "arm64") return "linux_arm64";
23
+ throw new Error(`Unsupported platform: ${os}-${cpu}`);
24
+ }
25
+ var BINARY_DEFS = {
26
+ "yt-dlp": {
27
+ darwin_arm64: {
28
+ url: "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos"
29
+ },
30
+ darwin_x64: {
31
+ url: "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos"
32
+ },
33
+ linux_x64: {
34
+ url: "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux"
35
+ },
36
+ linux_arm64: {
37
+ url: "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64"
38
+ }
39
+ },
40
+ ffmpeg: {
41
+ darwin_arm64: {
42
+ url: "https://evermeet.cx/ffmpeg/getrelease/zip",
43
+ extract: "zip",
44
+ innerPath: "ffmpeg"
45
+ },
46
+ darwin_x64: {
47
+ url: "https://evermeet.cx/ffmpeg/getrelease/zip",
48
+ extract: "zip",
49
+ innerPath: "ffmpeg"
50
+ },
51
+ linux_x64: {
52
+ url: "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
53
+ extract: "tar.gz",
54
+ innerPath: "ffmpeg"
55
+ },
56
+ linux_arm64: {
57
+ url: "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz",
58
+ extract: "tar.gz",
59
+ innerPath: "ffmpeg"
60
+ }
61
+ },
62
+ ffprobe: {
63
+ darwin_arm64: {
64
+ url: "https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip",
65
+ extract: "zip",
66
+ innerPath: "ffprobe"
67
+ },
68
+ darwin_x64: {
69
+ url: "https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip",
70
+ extract: "zip",
71
+ innerPath: "ffprobe"
72
+ },
73
+ linux_x64: {
74
+ url: "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
75
+ extract: "tar.gz",
76
+ innerPath: "ffprobe"
77
+ },
78
+ linux_arm64: {
79
+ url: "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz",
80
+ extract: "tar.gz",
81
+ innerPath: "ffprobe"
82
+ }
83
+ }
84
+ };
85
+ var WHISPER_MODEL_URL = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin";
86
+ var WHISPER_MODEL_NAME = "ggml-base.en.bin";
87
+ function getBinPath(name) {
88
+ return join(BIN_DIR, name);
89
+ }
90
+ function getWhisperModelPath() {
91
+ return join(MODELS_DIR, WHISPER_MODEL_NAME);
92
+ }
93
+ async function hasBinary(name) {
94
+ try {
95
+ await access(join(BIN_DIR, name));
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+ async function hasWhisperModel() {
102
+ try {
103
+ const s = await stat(join(MODELS_DIR, WHISPER_MODEL_NAME));
104
+ return s.size > 100 * 1024 * 1024;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ async function checkSetup() {
110
+ const missing = [];
111
+ for (const name of ["yt-dlp", "ffmpeg", "ffprobe"]) {
112
+ if (!await hasBinary(name) && !await isOnPath(name)) {
113
+ missing.push(name);
114
+ }
115
+ }
116
+ const hasWhisperBin = await hasBinary("whisper-cli") || await isOnPath("mlx_whisper") || await isOnPath("whisper");
117
+ if (!hasWhisperBin) missing.push("whisper-cli");
118
+ if (!hasWhisperBin && !await hasWhisperModel()) {
119
+ missing.push("whisper-model");
120
+ }
121
+ return { ready: missing.length === 0, missing };
122
+ }
123
+ async function resolveBinary(name) {
124
+ if (await hasBinary(name)) {
125
+ return join(BIN_DIR, name);
126
+ }
127
+ return name;
128
+ }
129
+ async function setupAll(progress) {
130
+ await mkdir(BIN_DIR, { recursive: true });
131
+ await mkdir(LIB_DIR, { recursive: true });
132
+ await mkdir(MODELS_DIR, { recursive: true });
133
+ const platformKey = getPlatformKey();
134
+ for (const [name, platforms] of Object.entries(BINARY_DEFS)) {
135
+ if (await hasBinary(name)) {
136
+ progress?.onSkip(name);
137
+ continue;
138
+ }
139
+ if (await isOnPath(name)) {
140
+ progress?.onSkip(name);
141
+ continue;
142
+ }
143
+ const def = platforms[platformKey];
144
+ try {
145
+ progress?.onStart(name, 0);
146
+ await downloadBinary(name, def, progress);
147
+ progress?.onDone(name);
148
+ } catch (error) {
149
+ const err = error;
150
+ progress?.onError(name, err.message);
151
+ }
152
+ }
153
+ if (!await hasBinary("whisper-cli") && !await isOnPath("mlx_whisper") && !await isOnPath("whisper")) {
154
+ try {
155
+ progress?.onStart("whisper-cli", 0);
156
+ await downloadWhisperCpp(platformKey, progress);
157
+ progress?.onDone("whisper-cli");
158
+ } catch (error) {
159
+ const err = error;
160
+ progress?.onError("whisper-cli", err.message);
161
+ }
162
+ } else {
163
+ progress?.onSkip("whisper-cli");
164
+ }
165
+ if (!await hasWhisperModel()) {
166
+ try {
167
+ progress?.onStart("whisper-model", 0);
168
+ await downloadFile(
169
+ WHISPER_MODEL_URL,
170
+ join(MODELS_DIR, WHISPER_MODEL_NAME),
171
+ progress ? (dl, total) => progress.onProgress("whisper-model", dl, total) : void 0
172
+ );
173
+ progress?.onDone("whisper-model");
174
+ } catch (error) {
175
+ const err = error;
176
+ progress?.onError("whisper-model", err.message);
177
+ }
178
+ } else {
179
+ progress?.onSkip("whisper-model");
180
+ }
181
+ }
182
+ async function isOnPath(name) {
183
+ try {
184
+ await execFileAsync("which", [name]);
185
+ return true;
186
+ } catch {
187
+ return false;
188
+ }
189
+ }
190
+ async function downloadFile(url, dest, onProgress, headers) {
191
+ const res = await fetch(url, { redirect: "follow", headers });
192
+ if (!res.ok || !res.body) {
193
+ throw new Error(
194
+ `Download failed: ${res.status} ${res.statusText} from ${url}`
195
+ );
196
+ }
197
+ const total = parseInt(res.headers.get("content-length") ?? "0", 10);
198
+ let downloaded = 0;
199
+ const fileStream = createWriteStream(dest);
200
+ const reader = res.body.getReader();
201
+ try {
202
+ while (true) {
203
+ const { done, value } = await reader.read();
204
+ if (done || !value) break;
205
+ fileStream.write(Buffer.from(value));
206
+ downloaded += value.length;
207
+ onProgress?.(downloaded, total);
208
+ }
209
+ } finally {
210
+ fileStream.end();
211
+ await new Promise((resolve, reject) => {
212
+ fileStream.on("finish", resolve);
213
+ fileStream.on("error", reject);
214
+ });
215
+ }
216
+ }
217
+ async function downloadBinary(name, def, progress) {
218
+ const destPath = join(BIN_DIR, name);
219
+ if (!def.extract) {
220
+ await downloadFile(
221
+ def.url,
222
+ destPath,
223
+ progress ? (dl, total) => progress.onProgress(name, dl, total) : void 0
224
+ );
225
+ await chmod(destPath, 493);
226
+ return;
227
+ }
228
+ const tmpPath = `${destPath}.tmp`;
229
+ if (def.extract === "zip") {
230
+ const zipPath = `${destPath}.zip`;
231
+ await downloadFile(
232
+ def.url,
233
+ zipPath,
234
+ progress ? (dl, total) => progress.onProgress(name, dl, total) : void 0
235
+ );
236
+ await execFileAsync("unzip", ["-o", "-j", zipPath, "-d", BIN_DIR]);
237
+ await rm(zipPath, { force: true });
238
+ if (def.innerPath) {
239
+ const extractedPath = join(BIN_DIR, def.innerPath);
240
+ if (extractedPath !== destPath) {
241
+ await rename(extractedPath, destPath);
242
+ }
243
+ }
244
+ await chmod(destPath, 493);
245
+ } else if (def.extract === "gz") {
246
+ const gzPath = `${destPath}.gz`;
247
+ await downloadFile(
248
+ def.url,
249
+ gzPath,
250
+ progress ? (dl, total) => progress.onProgress(name, dl, total) : void 0
251
+ );
252
+ const { createReadStream } = await import("fs");
253
+ await pipeline(
254
+ createReadStream(gzPath),
255
+ createGunzip(),
256
+ createWriteStream(tmpPath)
257
+ );
258
+ await rm(gzPath, { force: true });
259
+ await rename(tmpPath, destPath);
260
+ await chmod(destPath, 493);
261
+ }
262
+ }
263
+ async function downloadWhisperCpp(platformKey, progress) {
264
+ const metaRes = await fetch(
265
+ "https://formulae.brew.sh/api/formula/whisper-cpp.json"
266
+ );
267
+ if (!metaRes.ok) {
268
+ throw new Error(`Failed to fetch whisper-cpp metadata: ${metaRes.status}`);
269
+ }
270
+ const meta = await metaRes.json();
271
+ const bottleKeyMap = {
272
+ darwin_arm64: ["arm64_sequoia", "arm64_sonoma", "arm64_tahoe"],
273
+ darwin_x64: ["sonoma"],
274
+ linux_x64: ["x86_64_linux"],
275
+ linux_arm64: ["arm64_linux"]
276
+ };
277
+ const bottleKeys = bottleKeyMap[platformKey];
278
+ const bottles = meta.bottle.stable.files;
279
+ const match = bottleKeys.find((k) => bottles[k]);
280
+ if (!match) {
281
+ throw new Error(`No pre-built whisper-cpp available for ${platformKey}`);
282
+ }
283
+ const bottle = bottles[match];
284
+ if (!bottle) {
285
+ throw new Error(`No pre-built whisper-cpp available for ${platformKey}`);
286
+ }
287
+ const bottleUrl = bottle.url;
288
+ const tokenRes = await fetch(
289
+ "https://ghcr.io/token?scope=repository:homebrew/core/whisper-cpp:pull"
290
+ );
291
+ if (!tokenRes.ok) {
292
+ throw new Error(`Failed to get GHCR token: ${tokenRes.status}`);
293
+ }
294
+ const { token: ghcrToken } = await tokenRes.json();
295
+ const tarPath = join(BIN_DIR, "whisper-cpp-bottle.tar.gz");
296
+ await downloadFile(
297
+ bottleUrl,
298
+ tarPath,
299
+ progress ? (dl, total) => progress.onProgress("whisper-cli", dl, total) : void 0,
300
+ { Authorization: `Bearer ${ghcrToken}` }
301
+ );
302
+ const extractDir = join(BIN_DIR, "_whisper_extract");
303
+ await mkdir(extractDir, { recursive: true });
304
+ await execFileAsync("tar", ["xzf", tarPath, "-C", extractDir]);
305
+ await rm(tarPath, { force: true });
306
+ const { readdirSync, existsSync } = await import("fs");
307
+ const wcDir = join(extractDir, "whisper-cpp");
308
+ if (!existsSync(wcDir)) {
309
+ throw new Error("Unexpected bottle structure: whisper-cpp/ not found");
310
+ }
311
+ const versions = readdirSync(wcDir);
312
+ const ver = versions[0];
313
+ if (!ver) {
314
+ throw new Error("Unexpected bottle structure: no version directory");
315
+ }
316
+ const versionDir = join(wcDir, ver);
317
+ const libexecBin = join(versionDir, "libexec", "bin", "whisper-cli");
318
+ const directBin = join(versionDir, "bin", "whisper-cli");
319
+ const sourceBin = existsSync(libexecBin) ? libexecBin : directBin;
320
+ if (!existsSync(sourceBin)) {
321
+ throw new Error("whisper-cli binary not found in bottle");
322
+ }
323
+ await rename(sourceBin, join(BIN_DIR, "whisper-cli"));
324
+ await chmod(join(BIN_DIR, "whisper-cli"), 493);
325
+ await mkdir(LIB_DIR, { recursive: true });
326
+ for (const libDir of [
327
+ join(versionDir, "lib"),
328
+ join(versionDir, "libexec", "lib")
329
+ ]) {
330
+ if (!existsSync(libDir)) continue;
331
+ for (const f of readdirSync(libDir)) {
332
+ if (f.endsWith(".dylib") || f.endsWith(".so")) {
333
+ await rename(join(libDir, f), join(LIB_DIR, f)).catch(() => {
334
+ });
335
+ }
336
+ }
337
+ }
338
+ await rm(extractDir, { recursive: true, force: true });
339
+ }
340
+
341
+ export {
342
+ getBinPath,
343
+ getWhisperModelPath,
344
+ checkSetup,
345
+ resolveBinary,
346
+ setupAll
347
+ };
348
+ //# sourceMappingURL=chunk-WMJSGRMP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/binary-manager.ts"],"sourcesContent":["// ============================================\n// BINARY MANAGER\n// ============================================\n// Downloads and manages tool binaries in ~/.program/bin/\n// Users never need to install anything manually.\n\nimport { execFile } from \"node:child_process\";\nimport { createWriteStream } from \"node:fs\";\nimport { access, chmod, mkdir, rename, rm, stat } from \"node:fs/promises\";\nimport { arch, homedir, platform } from \"node:os\";\nimport { join } from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport { promisify } from \"node:util\";\nimport { createGunzip } from \"node:zlib\";\n\nconst execFileAsync = promisify(execFile);\n\nconst BIN_DIR = join(homedir(), \".program\", \"bin\");\nconst LIB_DIR = join(homedir(), \".program\", \"lib\");\nconst MODELS_DIR = join(homedir(), \".program\", \"models\");\n\n// ============================================\n// Binary definitions per platform\n// ============================================\n\ninterface BinaryDef {\n url: string;\n extract?: \"zip\" | \"gz\" | \"tar.gz\";\n // For zip/tar archives: the filename inside the archive to extract\n innerPath?: string;\n}\n\ntype PlatformKey = \"darwin_arm64\" | \"darwin_x64\" | \"linux_x64\" | \"linux_arm64\";\n\nfunction getPlatformKey(): PlatformKey {\n const os = platform();\n const cpu = arch();\n if (os === \"darwin\" && cpu === \"arm64\") return \"darwin_arm64\";\n if (os === \"darwin\" && cpu === \"x64\") return \"darwin_x64\";\n if (os === \"linux\" && cpu === \"x64\") return \"linux_x64\";\n if (os === \"linux\" && cpu === \"arm64\") return \"linux_arm64\";\n throw new Error(`Unsupported platform: ${os}-${cpu}`);\n}\n\nconst BINARY_DEFS: Record<string, Record<PlatformKey, BinaryDef>> = {\n \"yt-dlp\": {\n darwin_arm64: {\n url: \"https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos\",\n },\n darwin_x64: {\n url: \"https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos\",\n },\n linux_x64: {\n url: \"https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux\",\n },\n linux_arm64: {\n url: \"https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64\",\n },\n },\n ffmpeg: {\n darwin_arm64: {\n url: \"https://evermeet.cx/ffmpeg/getrelease/zip\",\n extract: \"zip\",\n innerPath: \"ffmpeg\",\n },\n darwin_x64: {\n url: \"https://evermeet.cx/ffmpeg/getrelease/zip\",\n extract: \"zip\",\n innerPath: \"ffmpeg\",\n },\n linux_x64: {\n url: \"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz\",\n extract: \"tar.gz\",\n innerPath: \"ffmpeg\",\n },\n linux_arm64: {\n url: \"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz\",\n extract: \"tar.gz\",\n innerPath: \"ffmpeg\",\n },\n },\n ffprobe: {\n darwin_arm64: {\n url: \"https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip\",\n extract: \"zip\",\n innerPath: \"ffprobe\",\n },\n darwin_x64: {\n url: \"https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip\",\n extract: \"zip\",\n innerPath: \"ffprobe\",\n },\n linux_x64: {\n url: \"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz\",\n extract: \"tar.gz\",\n innerPath: \"ffprobe\",\n },\n linux_arm64: {\n url: \"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz\",\n extract: \"tar.gz\",\n innerPath: \"ffprobe\",\n },\n },\n};\n\n// Whisper.cpp model URL (base.en is small + accurate for English)\nconst WHISPER_MODEL_URL =\n \"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin\";\nconst WHISPER_MODEL_NAME = \"ggml-base.en.bin\";\n\n// ============================================\n// Public API\n// ============================================\n\n/**\n * Get the absolute path to a managed binary.\n * Returns the managed path if downloaded, falls back to system PATH.\n */\nexport function getBinPath(name: string): string {\n return join(BIN_DIR, name);\n}\n\nexport function getWhisperModelPath(): string {\n return join(MODELS_DIR, WHISPER_MODEL_NAME);\n}\n\n/**\n * Check if a managed binary exists.\n */\nasync function hasBinary(name: string): Promise<boolean> {\n try {\n await access(join(BIN_DIR, name));\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if whisper model is downloaded.\n */\nasync function hasWhisperModel(): Promise<boolean> {\n try {\n const s = await stat(join(MODELS_DIR, WHISPER_MODEL_NAME));\n // Model should be at least 100MB\n return s.size > 100 * 1024 * 1024;\n } catch {\n return false;\n }\n}\n\n/**\n * Check which binaries/models are missing.\n */\nexport async function checkSetup(): Promise<{\n ready: boolean;\n missing: string[];\n}> {\n const missing: string[] = [];\n\n for (const name of [\"yt-dlp\", \"ffmpeg\", \"ffprobe\"]) {\n // Check managed path first, then system PATH\n if (!(await hasBinary(name)) && !(await isOnPath(name))) {\n missing.push(name);\n }\n }\n\n // Check whisper: managed whisper.cpp binary OR system whisper/mlx_whisper\n const hasWhisperBin =\n (await hasBinary(\"whisper-cli\")) ||\n (await isOnPath(\"mlx_whisper\")) ||\n (await isOnPath(\"whisper\"));\n if (!hasWhisperBin) missing.push(\"whisper-cli\");\n\n if (!hasWhisperBin && !(await hasWhisperModel())) {\n missing.push(\"whisper-model\");\n }\n\n return { ready: missing.length === 0, missing };\n}\n\n/**\n * Resolve a binary: prefer managed, fall back to system PATH.\n */\nexport async function resolveBinary(name: string): Promise<string> {\n if (await hasBinary(name)) {\n return join(BIN_DIR, name);\n }\n // Fall back to system PATH\n return name;\n}\n\n// ============================================\n// Setup / Download\n// ============================================\n\nexport interface SetupProgress {\n onStart: (name: string, total: number) => void;\n onProgress: (name: string, downloaded: number, total: number) => void;\n onDone: (name: string) => void;\n onSkip: (name: string) => void;\n onError: (name: string, error: string) => void;\n}\n\n/**\n * Download all missing binaries and models.\n */\nexport async function setupAll(progress?: SetupProgress): Promise<void> {\n await mkdir(BIN_DIR, { recursive: true });\n await mkdir(LIB_DIR, { recursive: true });\n await mkdir(MODELS_DIR, { recursive: true });\n\n const platformKey = getPlatformKey();\n\n // Download tool binaries\n for (const [name, platforms] of Object.entries(BINARY_DEFS)) {\n if (await hasBinary(name)) {\n progress?.onSkip(name);\n continue;\n }\n // If it's on system PATH, skip\n if (await isOnPath(name)) {\n progress?.onSkip(name);\n continue;\n }\n\n const def = platforms[platformKey];\n\n try {\n progress?.onStart(name, 0);\n await downloadBinary(name, def, progress);\n progress?.onDone(name);\n } catch (error: unknown) {\n const err = error as Error;\n progress?.onError(name, err.message);\n }\n }\n\n // Download whisper.cpp binary\n if (\n !(await hasBinary(\"whisper-cli\")) &&\n !(await isOnPath(\"mlx_whisper\")) &&\n !(await isOnPath(\"whisper\"))\n ) {\n try {\n progress?.onStart(\"whisper-cli\", 0);\n await downloadWhisperCpp(platformKey, progress);\n progress?.onDone(\"whisper-cli\");\n } catch (error: unknown) {\n const err = error as Error;\n progress?.onError(\"whisper-cli\", err.message);\n }\n } else {\n progress?.onSkip(\"whisper-cli\");\n }\n\n // Download whisper model\n if (!(await hasWhisperModel())) {\n try {\n progress?.onStart(\"whisper-model\", 0);\n await downloadFile(\n WHISPER_MODEL_URL,\n join(MODELS_DIR, WHISPER_MODEL_NAME),\n progress\n ? (dl, total) => progress.onProgress(\"whisper-model\", dl, total)\n : undefined,\n );\n progress?.onDone(\"whisper-model\");\n } catch (error: unknown) {\n const err = error as Error;\n progress?.onError(\"whisper-model\", err.message);\n }\n } else {\n progress?.onSkip(\"whisper-model\");\n }\n}\n\n// ============================================\n// Internal helpers\n// ============================================\n\nasync function isOnPath(name: string): Promise<boolean> {\n try {\n await execFileAsync(\"which\", [name]);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function downloadFile(\n url: string,\n dest: string,\n onProgress?: (downloaded: number, total: number) => void,\n headers?: Record<string, string>,\n): Promise<void> {\n const res = await fetch(url, { redirect: \"follow\", headers });\n if (!res.ok || !res.body) {\n throw new Error(\n `Download failed: ${res.status} ${res.statusText} from ${url}`,\n );\n }\n\n const total = parseInt(res.headers.get(\"content-length\") ?? \"0\", 10);\n let downloaded = 0;\n\n const fileStream = createWriteStream(dest);\n const reader = res.body.getReader();\n\n try {\n while (true) {\n const { done, value } = (await reader.read()) as {\n done: boolean;\n value: Uint8Array | undefined;\n };\n if (done || !value) break;\n fileStream.write(Buffer.from(value));\n downloaded += value.length;\n onProgress?.(downloaded, total);\n }\n } finally {\n fileStream.end();\n await new Promise<void>((resolve, reject) => {\n fileStream.on(\"finish\", resolve);\n fileStream.on(\"error\", reject);\n });\n }\n}\n\nasync function downloadBinary(\n name: string,\n def: BinaryDef,\n progress?: SetupProgress,\n): Promise<void> {\n const destPath = join(BIN_DIR, name);\n\n if (!def.extract) {\n // Direct binary download\n await downloadFile(\n def.url,\n destPath,\n progress\n ? (dl, total) => progress.onProgress(name, dl, total)\n : undefined,\n );\n await chmod(destPath, 0o755);\n return;\n }\n\n // Download to temp file then extract\n const tmpPath = `${destPath}.tmp`;\n\n if (def.extract === \"zip\") {\n const zipPath = `${destPath}.zip`;\n await downloadFile(\n def.url,\n zipPath,\n progress\n ? (dl, total) => progress.onProgress(name, dl, total)\n : undefined,\n );\n\n // Extract with unzip\n await execFileAsync(\"unzip\", [\"-o\", \"-j\", zipPath, \"-d\", BIN_DIR]);\n await rm(zipPath, { force: true });\n\n // If innerPath specified, rename\n if (def.innerPath) {\n const extractedPath = join(BIN_DIR, def.innerPath);\n if (extractedPath !== destPath) {\n await rename(extractedPath, destPath);\n }\n }\n await chmod(destPath, 0o755);\n } else if (def.extract === \"gz\") {\n // .gz (single file gzipped)\n const gzPath = `${destPath}.gz`;\n await downloadFile(\n def.url,\n gzPath,\n progress\n ? (dl, total) => progress.onProgress(name, dl, total)\n : undefined,\n );\n\n const { createReadStream } = await import(\"node:fs\");\n await pipeline(\n createReadStream(gzPath),\n createGunzip(),\n createWriteStream(tmpPath),\n );\n await rm(gzPath, { force: true });\n await rename(tmpPath, destPath);\n await chmod(destPath, 0o755);\n }\n}\n\nasync function downloadWhisperCpp(\n platformKey: PlatformKey,\n progress?: SetupProgress,\n): Promise<void> {\n // Download pre-built whisper-cli from Homebrew bottles (no build tools needed).\n\n // Fetch bottle metadata from Homebrew API\n const metaRes = await fetch(\n \"https://formulae.brew.sh/api/formula/whisper-cpp.json\",\n );\n if (!metaRes.ok) {\n throw new Error(`Failed to fetch whisper-cpp metadata: ${metaRes.status}`);\n }\n const meta = (await metaRes.json()) as {\n bottle: {\n stable: {\n files: Record<string, { url: string; sha256: string }>;\n };\n };\n };\n\n // Map our platform key to Homebrew bottle key\n const bottleKeyMap: Record<PlatformKey, string[]> = {\n darwin_arm64: [\"arm64_sequoia\", \"arm64_sonoma\", \"arm64_tahoe\"],\n darwin_x64: [\"sonoma\"],\n linux_x64: [\"x86_64_linux\"],\n linux_arm64: [\"arm64_linux\"],\n };\n\n const bottleKeys = bottleKeyMap[platformKey];\n const bottles = meta.bottle.stable.files;\n const match = bottleKeys.find((k) => bottles[k]);\n if (!match) {\n throw new Error(`No pre-built whisper-cpp available for ${platformKey}`);\n }\n\n const bottle = bottles[match];\n if (!bottle) {\n throw new Error(`No pre-built whisper-cpp available for ${platformKey}`);\n }\n const bottleUrl = bottle.url;\n\n // GHCR requires an anonymous bearer token even for public packages\n const tokenRes = await fetch(\n \"https://ghcr.io/token?scope=repository:homebrew/core/whisper-cpp:pull\",\n );\n if (!tokenRes.ok) {\n throw new Error(`Failed to get GHCR token: ${tokenRes.status}`);\n }\n const { token: ghcrToken } = (await tokenRes.json()) as { token: string };\n\n // Download the bottle tarball with auth\n const tarPath = join(BIN_DIR, \"whisper-cpp-bottle.tar.gz\");\n await downloadFile(\n bottleUrl,\n tarPath,\n progress\n ? (dl, total) => progress.onProgress(\"whisper-cli\", dl, total)\n : undefined,\n { Authorization: `Bearer ${ghcrToken}` },\n );\n\n // Extract whisper-cli binary and its dylib from the tarball\n const extractDir = join(BIN_DIR, \"_whisper_extract\");\n await mkdir(extractDir, { recursive: true });\n await execFileAsync(\"tar\", [\"xzf\", tarPath, \"-C\", extractDir]);\n await rm(tarPath, { force: true });\n\n // Find whisper-cli inside extracted bottle (at whisper-cpp/*/libexec/bin/whisper-cli)\n const { readdirSync, existsSync } = await import(\"node:fs\");\n\n // Walk: whisper-cpp/<version>/\n const wcDir = join(extractDir, \"whisper-cpp\");\n if (!existsSync(wcDir)) {\n throw new Error(\"Unexpected bottle structure: whisper-cpp/ not found\");\n }\n const versions = readdirSync(wcDir);\n const ver = versions[0];\n if (!ver) {\n throw new Error(\"Unexpected bottle structure: no version directory\");\n }\n const versionDir = join(wcDir, ver);\n\n // Copy whisper-cli from libexec/bin (self-contained) or bin/\n const libexecBin = join(versionDir, \"libexec\", \"bin\", \"whisper-cli\");\n const directBin = join(versionDir, \"bin\", \"whisper-cli\");\n const sourceBin = existsSync(libexecBin) ? libexecBin : directBin;\n\n if (!existsSync(sourceBin)) {\n throw new Error(\"whisper-cli binary not found in bottle\");\n }\n\n await rename(sourceBin, join(BIN_DIR, \"whisper-cli\"));\n await chmod(join(BIN_DIR, \"whisper-cli\"), 0o755);\n\n // Copy dylibs to lib/ — whisper-cli rpath is @loader_path/../lib\n await mkdir(LIB_DIR, { recursive: true });\n for (const libDir of [\n join(versionDir, \"lib\"),\n join(versionDir, \"libexec\", \"lib\"),\n ]) {\n if (!existsSync(libDir)) continue;\n for (const f of readdirSync(libDir)) {\n if (f.endsWith(\".dylib\") || f.endsWith(\".so\")) {\n await rename(join(libDir, f), join(LIB_DIR, f)).catch(() => {\n /* ignore rename failures */\n });\n }\n }\n }\n\n await rm(extractDir, { recursive: true, force: true });\n}\n"],"mappings":"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","names":[]}