fix: cache paths follow HYPERFRAMES_CACHE_DIR

Every place HyperFrames keeps downloaded or built files now takes its
folder from one helper, hyperframesCacheDir() in @hyperframes/core. The
default stays ~/.cache/hyperframes; HYPERFRAMES_CACHE_DIR moves it.
This commit is contained in:
Miguel Angel Simon Sierra
2026-10-29 00:47:34 -04:00
parent a5496b1887
commit dabb0f5b39
21 changed files with 126 additions and 47 deletions
+3
View File
@@ -1835,6 +1835,9 @@ default 3); `--json` prints the results as JSON.
## Keep the environment healthy
HyperFrames keeps what it downloads and builds (the headless browser, models, optional packages, fonts and project
history) under `~/.cache/hyperframes`. Set `HYPERFRAMES_CACHE_DIR` to keep it somewhere else.
### `doctor`
Check the machine for everything a render needs.
@@ -1,10 +1,10 @@
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, rename, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import type { AudioAnalysisProvider, AudioSignal } from "@hyperframes/core/audio-analysis";
import { AVAILABLE, runFile, unavailable, withWavFile } from "./shared.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
const SOUND_ANALYSIS = "/System/Library/Frameworks/SoundAnalysis.framework";
const MUSIC_UNDERSTANDING_MACOS = 27;
@@ -62,7 +62,7 @@ function atLeastOneWindow(signal: AudioSignal): AudioSignal {
};
}
const helperDir = () => join(homedir(), ".cache", "hyperframes", "audio-helpers");
const helperDir = () => hyperframesCacheDir("audio-helpers");
function helperPath(name: string, source: string): string {
const hash = createHash("sha256").update(source).digest("hex").slice(0, 12);
@@ -1,17 +1,12 @@
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { tmpdir } from "node:os";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
import { basename, join } from "node:path";
import { processIsAlive } from "../utils/processTree.js";
const COMPILED_MODEL = /^onnxruntime-.*\.mlmodelc?$/;
const COREML_TEMP_MANIFESTS = join(
homedir(),
".cache",
"hyperframes",
"background-removal",
"coreml-temp",
);
const COREML_TEMP_MANIFESTS = hyperframesCacheDir("background-removal", "coreml-temp");
interface Manifest {
pid: number;
@@ -1,9 +1,10 @@
import { existsSync, mkdirSync } from "node:fs";
import { homedir, platform, arch } from "node:os";
import { platform, arch } from "node:os";
import { join } from "node:path";
import { downloadFile } from "../utils/download.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
export const MODELS_DIR = join(homedir(), ".cache", "hyperframes", "background-removal", "models");
export const MODELS_DIR = hyperframesCacheDir("background-removal", "models");
export const DEFAULT_MODEL = "u2net_human_seg" as const;
export type ModelId = typeof DEFAULT_MODEL;
+3 -2
View File
@@ -24,6 +24,7 @@ import { BrowserDownloadStalledError, installInChild } from "./installInChild.js
import { normalizeErrorMessage } from "../utils/errorMessage.js";
import { runRenderSetupWorker } from "../utils/cancellableProcess.js";
import { unzipArchive } from "./unzipArchive.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
type PuppeteerBrowsers = typeof import("@puppeteer/browsers");
@@ -39,8 +40,8 @@ async function loadPuppeteerBrowsers(): Promise<PuppeteerBrowsers> {
}
}
const CACHE_ROOT_DIR = join(homedir(), ".cache", "hyperframes");
const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "chrome");
const CACHE_ROOT_DIR = hyperframesCacheDir();
const CACHE_DIR = hyperframesCacheDir("chrome");
const ARCHIVE_PLUS_UNPACKED_SHELL_MB = 512;
// Puppeteer's managed cache — where `@puppeteer/browsers install
// chrome-headless-shell` (and `puppeteer install`) drop binaries. The engine's
+4 -3
View File
@@ -15,7 +15,7 @@ import {
rmSync,
writeFileSync,
} from "node:fs";
import { homedir, tmpdir } from "node:os";
import { tmpdir } from "node:os";
import { basename, dirname, extname, join, posix, relative, resolve } from "node:path";
import { promisify } from "node:util";
import { parseHTML } from "linkedom";
@@ -41,6 +41,7 @@ import {
stagedSize,
TEXT_FILE_EXTS,
} from "../utils/publishProject.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
const execFileAsync = promisify(execFile);
@@ -200,7 +201,7 @@ interface CacheEntry {
}
function defaultOffloadCachePath(): string {
return join(homedir(), ".cache", "hyperframes", "cloud-assets.json");
return hyperframesCacheDir("cloud-assets.json");
}
function readCache(path: string): Record<string, CacheEntry> {
@@ -650,7 +651,7 @@ async function prepareEntryGifs(
downloadDir: outputDir,
outputDir,
outputSrcPrefix: posix.relative(posix.dirname(entryPath), GIF_PREFIX),
cacheDir: join(homedir(), ".cache", "hyperframes", "animated-gif"),
cacheDir: hyperframesCacheDir("animated-gif"),
sourceAssets,
});
if (result.preparedGifs.length === 0) return;
@@ -11,14 +11,14 @@ import {
renameSync,
unlinkSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { downloadFile } from "../../utils/download.js";
import { isMissingPackageError } from "../../utils/missingPackage.js";
import { VERSION } from "../../version.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
const DOCS_URL = "https://hyperframes.heygen.com/deploy/aws-lambda";
const HANDLER_CACHE_DIR = join(homedir(), ".cache", "hyperframes", "lambda-handler");
const HANDLER_CACHE_DIR = hyperframesCacheDir("lambda-handler");
type AdapterSdk = typeof import("@hyperframes/aws-lambda/sdk");
+2 -2
View File
@@ -1,9 +1,9 @@
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { downloadFile } from "../utils/download.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "tts");
const CACHE_DIR = hyperframesCacheDir("tts");
const MODELS_DIR = join(CACHE_DIR, "models");
const VOICES_DIR = join(CACHE_DIR, "voices");
+2 -2
View File
@@ -2,7 +2,6 @@
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
import { join, dirname, basename } from "node:path";
import { homedir } from "node:os";
import {
ensureModel,
ensureVoices,
@@ -12,6 +11,7 @@ import {
} from "./manager.js";
import { findPython, hasPythonPackage } from "./python.js";
import { writeNewFileSync } from "../utils/writeNewFile.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
// ---------------------------------------------------------------------------
// Inline Python script for Kokoro synthesis
@@ -115,7 +115,7 @@ const ESPEAK_LANG_OVERRIDES: Partial<Record<SupportedLang, string>> = {
// Cache the script to avoid rewriting it on every invocation.
// The filename carries a version suffix so older installs automatically
// upgrade when the script body changes (e.g., adding the `lang` kwarg).
const SCRIPT_DIR = join(homedir(), ".cache", "hyperframes", "tts");
const SCRIPT_DIR = hyperframesCacheDir("tts");
const SCRIPT_PATH = join(SCRIPT_DIR, "synth-v4.py");
function ensureSynthScript(): string {
+2 -2
View File
@@ -12,9 +12,9 @@ import {
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { homedir } from "node:os";
import { basename, dirname, join, sep } from "node:path";
import { buildNpmCommand } from "./npxCommand.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
/** Module type of each optional package; the keys are the only names the loader accepts. */
export interface OptionalPackageModules {
@@ -37,7 +37,7 @@ export const PINNED_PACKAGES = {
type PinnedPackage = keyof typeof PINNED_PACKAGES;
export const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "optional");
export const CACHE_DIR = hyperframesCacheDir("optional");
/** Installed on first use only when a host bundled the CLI without its sharp dependency. */
export const SHARP_VERSION = "0.35.3";
+2 -1
View File
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
import { join } from "node:path";
import { Writable } from "node:stream";
import { fileURLToPath } from "node:url";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
export const WARM_IDLE_MS = 5 * 60_000;
export const HANDSHAKE_TIMEOUT_MS = 2_000;
@@ -50,7 +51,7 @@ export function warmCheckDirectory(env = process.env): string | undefined {
? join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "hyperframes", "warm")
: env.XDG_RUNTIME_DIR
? join(env.XDG_RUNTIME_DIR, "hyperframes-warm")
: join(homedir(), ".cache", "hyperframes", "warm");
: hyperframesCacheDir("warm");
try {
mkdirSync(dir, { recursive: true, mode: 0o700 });
} catch {
+5 -4
View File
@@ -1,14 +1,15 @@
// fallow-ignore-file code-duplication complexity
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
import { homedir, platform } from "node:os";
import { platform } from "node:os";
import { join } from "node:path";
import { findFFmpeg } from "../browser/ffmpeg.js";
import { downloadFile } from "../utils/download.js";
import { stoppableExecFile } from "../utils/stoppableExecFile.js";
import { verifies, type ModelFile } from "./modelFile.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
const MODELS_DIR = join(homedir(), ".cache", "hyperframes", "whisper", "models");
const MODELS_DIR = hyperframesCacheDir("whisper", "models");
const DEFAULT_MODEL = "small.en";
export type WhisperSource = "env" | "system" | "brew" | "build";
@@ -144,7 +145,7 @@ function findFromSystem(): WhisperResult | undefined {
// --- Build from source ------------------------------------------------------
const BUILD_DIR = join(homedir(), ".cache", "hyperframes", "whisper", "whisper.cpp");
const BUILD_DIR = hyperframesCacheDir("whisper", "whisper.cpp");
const WHISPER_REPO = "https://github.com/ggml-org/whisper.cpp.git";
function findBuiltBinary(): WhisperResult | undefined {
@@ -168,7 +169,7 @@ async function buildFromSource(
if (!existsSync(BUILD_DIR)) {
onProgress?.("Downloading whisper.cpp...");
mkdirSync(join(homedir(), ".cache", "hyperframes", "whisper"), {
mkdirSync(hyperframesCacheDir("whisper"), {
recursive: true,
});
await stoppableExecFile("git", ["clone", "--depth", "1", WHISPER_REPO, BUILD_DIR], {
+2 -8
View File
@@ -1,7 +1,7 @@
import { execFile, spawn, type ExecFileException } from "node:child_process";
import { randomUUID } from "node:crypto";
import { mkdirSync, renameSync, rmSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
import { join } from "node:path";
import { createInterface } from "node:readline";
import { fileURLToPath } from "node:url";
@@ -45,13 +45,7 @@ export const SHERPA_RUNTIME_DIR = join(
const MODEL_REPO = "csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8";
const MODEL_REVISION = "2bda32ec70b097a55adaa07d9a7173915b43cc78";
export const PARAKEET_MODEL_DIR = join(
homedir(),
".cache",
"hyperframes",
"parakeet",
"parakeet-tdt-0.6b-v3-int8",
);
export const PARAKEET_MODEL_DIR = hyperframesCacheDir("parakeet", "parakeet-tdt-0.6b-v3-int8");
export type { ModelFile };
+10
View File
@@ -80,6 +80,12 @@
"import": "./src/atomicFile.ts",
"types": "./src/atomicFile.ts"
},
"./cache-dir": {
"bun": "./src/cacheDir.ts",
"node": "./dist/cacheDir.js",
"import": "./src/cacheDir.ts",
"types": "./src/cacheDir.ts"
},
"./remove-tree": {
"bun": "./src/removeTree.ts",
"node": "./dist/removeTree.js",
@@ -662,6 +668,10 @@
"import": "./dist/atomicFile.js",
"types": "./dist/atomicFile.d.ts"
},
"./cache-dir": {
"import": "./dist/cacheDir.js",
"types": "./dist/cacheDir.d.ts"
},
"./remove-tree": {
"import": "./dist/removeTree.js",
"types": "./dist/removeTree.d.ts"
+60
View File
@@ -0,0 +1,60 @@
import { readdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { hyperframesCacheDir } from "./cacheDir.js";
afterEach(() => vi.unstubAllEnvs());
describe("hyperframesCacheDir", () => {
it("defaults to ~/.cache/hyperframes", () => {
vi.stubEnv("HYPERFRAMES_CACHE_DIR", "");
expect(hyperframesCacheDir()).toBe(join(homedir(), ".cache", "hyperframes"));
expect(hyperframesCacheDir("chrome")).toBe(join(homedir(), ".cache", "hyperframes", "chrome"));
});
it("follows HYPERFRAMES_CACHE_DIR, read on each call, a relative one resolved", () => {
vi.stubEnv("HYPERFRAMES_CACHE_DIR", "/elsewhere/cache");
expect(hyperframesCacheDir("whisper", "models")).toBe(
join("/elsewhere/cache", "whisper", "models"),
);
vi.stubEnv("HYPERFRAMES_CACHE_DIR", "rel/cache");
expect(hyperframesCacheDir("tts")).toBe(join(resolve("rel/cache"), "tts"));
});
});
/** Source files under every package's src, tests left out. */
function sources(dir: string): string[] {
return readdirSync(dir, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile() && /\.(m?[jt]sx?)$/.test(entry.name))
.filter((entry) => !/\.test\.|__tests__/.test(join(entry.parentPath, entry.name)))
.map((entry) => join(entry.parentPath, entry.name));
}
it("every cache path in every package goes through hyperframesCacheDir", () => {
const packages = resolve(__dirname, "..", "..");
const built: string[] = [];
for (const name of readdirSync(packages)) {
let files: string[];
try {
files = sources(join(packages, name, "src"));
} catch {
continue; // a package without src
}
for (const file of files) {
if (file.endsWith(join("core", "src", "cacheDir.ts"))) continue;
// Prose about the default path is fine; code that builds it is not.
const code = readFileSync(file, "utf8")
.split("\n")
.filter((line) => !/^\s*(\/\/|\*|\/\*)/.test(line))
.join("\n");
if (
/["'`]\.cache["'`]\s*,\s*["'`]hyperframes["'`]|["'`][^"'`\n]*\.cache\/hyperframes/.test(
code,
)
)
built.push(file.slice(packages.length + 1));
}
}
expect(built).toEqual([]);
});
+9
View File
@@ -0,0 +1,9 @@
import { homedir } from "node:os";
import { join, resolve } from "node:path";
/** Where HyperFrames keeps what it downloads, builds and caches: `HYPERFRAMES_CACHE_DIR` when set, else
* `~/.cache/hyperframes`. Read on each call. */
export function hyperframesCacheDir(...parts: string[]): string {
const named = process.env["HYPERFRAMES_CACHE_DIR"];
return join(named ? resolve(named) : join(homedir(), ".cache", "hyperframes"), ...parts);
}
@@ -8,7 +8,7 @@ import {
rmSync,
writeFileSync,
} from "node:fs";
import { homedir, tmpdir } from "node:os";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { FONT_ALIAS_MAP, resolveAliasDisplayName } from "./aliases.js";
@@ -25,6 +25,7 @@ import {
type FontDiagnostics,
type FontDiagPath,
} from "./fontDiagnostics.js";
import { hyperframesCacheDir } from "../cacheDir.js";
export type { FontDiagnostics };
@@ -1123,7 +1124,7 @@ function resolveFontCacheRoot(): string {
lambdaFontCacheRoot ??= mkdtempSync(join(tmpdir(), "hyperframes-fonts-"));
return lambdaFontCacheRoot;
}
return join(homedir(), ".cache", "hyperframes", "fonts");
return hyperframesCacheDir("fonts");
}
// Chrome UA triggers woff2 responses from Google Fonts CSS API
+3 -2
View File
@@ -10,8 +10,9 @@ import {
rmSync,
writeFileSync,
} from "node:fs";
import { homedir, tmpdir } from "node:os";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { hyperframesCacheDir } from "../cacheDir.js";
const { compress } = wawoff2 as {
compress: (input: Buffer | Uint8Array) => Promise<Uint8Array>;
@@ -41,7 +42,7 @@ function defaultCacheDir(): string {
process.env.HYPERFRAMES_FONT_CACHE_DIR ??
(process.env.AWS_LAMBDA_FUNCTION_NAME
? join(tmpdir(), "hyperframes", "fonts")
: join(homedir(), ".cache", "hyperframes", "fonts"));
: hyperframesCacheDir("fonts"));
return join(root, "local-compression-v1");
}
@@ -46,6 +46,7 @@ export type {
BrowserPoolState,
CaptureMode,
} from "./browserLeasePool.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
let _puppeteer: PuppeteerNode | undefined;
@@ -271,7 +272,7 @@ export function resolveHeadlessShellPath(
}
return fromEnv.path;
}
const ours = join(homedir(), ".cache", "hyperframes", "chrome", "chrome-headless-shell");
const ours = hyperframesCacheDir("chrome", "chrome-headless-shell");
const shared = join(homedir(), ".cache", "puppeteer", "chrome-headless-shell");
return (
findManagedHeadlessShell(ours) ??
+2 -2
View File
@@ -22,7 +22,6 @@ import {
readdirSync,
} from "node:fs";
import { resolve, join, extname, sep } from "node:path";
import { homedir } from "node:os";
import { getHyperframeRuntimeScript } from "../../core/src/generated/runtime-inline";
import puppeteer, { type Browser, type Page } from "puppeteer-core";
import {
@@ -32,6 +31,7 @@ import {
import { closeServer } from "./pageServer";
import { Sources, fingerprint } from "./sources";
import type { InspectRequest, Snippet } from "./types";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
const pageRunner = (page: Page): PageRunner => ({
run: <A, R>(fn: (arg: A) => R, arg: A) =>
@@ -59,7 +59,7 @@ function chromePath() {
"Set HF_INSPECT_CHROME to an installed Chrome executable; inspection does not download a browser.",
);
}
const root = join(homedir(), ".cache/hyperframes/chrome/chrome-headless-shell");
const root = hyperframesCacheDir("chrome", "chrome-headless-shell");
if (existsSync(root))
for (const version of readdirSync(root).sort().reverse()) {
const dir = join(root, version);
@@ -12,7 +12,6 @@ import {
realpathSync,
rmSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
import {
@@ -64,9 +63,10 @@ import {
type LogRecord,
type Manifest,
} from "./historyLog.js";
import { hyperframesCacheDir } from "@hyperframes/core/cache-dir";
/** Where hosts keep project histories unless told otherwise: outside every project, so no tidy-up takes one away. */
export const DEFAULT_HISTORY_ROOT = join(homedir(), ".cache", "hyperframes", "history");
export const DEFAULT_HISTORY_ROOT = hyperframesCacheDir("history");
export interface ProjectHistoryOptions {
projectDir: string;