hyperframes 0.8.78 → 0.8.80
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.
- package/dist/cli.js +2958 -1893
- package/dist/fontLocalizeCli.js +160 -54
- package/dist/hyperframe-runtime.js +57 -57
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +57 -57
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/registry/localSemantic.js +6 -25
- package/dist/skills/media-use/audio/references/remove-background.md +1 -1
- package/dist/skills/media-use/registry/localSemantic.js +6 -25
- package/dist/skills/media-use/scripts/compatibility.test.mjs +2 -0
- package/dist/skills/media-use/scripts/lib/cache.mjs +6 -8
- package/dist/skills/media-use/scripts/lib/candidates.test.mjs +2 -2
- package/dist/skills/media-use/scripts/lib/config-lock.d.mts +3 -0
- package/dist/skills/media-use/scripts/lib/config-lock.mjs +45 -0
- package/dist/skills/media-use/scripts/lib/config-lock.test.mjs +79 -0
- package/dist/skills/media-use/scripts/lib/manifest.test.mjs +2 -0
- package/dist/skills/media-use/scripts/lib/media-home.mjs +20 -0
- package/dist/skills/media-use/scripts/lib/media-home.test.mjs +61 -0
- package/dist/skills/media-use/scripts/lib/misses.mjs +6 -6
- package/dist/skills/media-use/scripts/lib/misses.test.mjs +12 -1
- package/dist/skills/media-use/scripts/lib/prefs-store.mjs +2 -2
- package/dist/skills/media-use/scripts/lib/prefs-store.test.mjs +1 -1
- package/dist/skills/media-use/scripts/lib/recipe-store.mjs +2 -2
- package/dist/skills/media-use/scripts/lib/recipe-store.test.mjs +1 -1
- package/dist/skills/media-use/scripts/lib/stats.test.mjs +1 -1
- package/dist/skills/media-use/scripts/lib/telemetry.mjs +26 -9
- package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +64 -0
- package/dist/skills/registry/localSemantic.js +6 -25
- package/dist/studio/assets/hyperframes-player-CgnoxDoD.js +464 -0
- package/dist/studio/assets/{index-CX_4MYbk.js → index-CsG24Jub.js} +190 -187
- package/dist/studio/assets/{index-C_n__Uh3.js → index-DZGVvqDT.js} +1 -1
- package/dist/studio/assets/{index-DdpuHaGN.js → index-J9HXe7mU.js} +1 -1
- package/dist/studio/{chunk-5VANA2TG.js → chunk-I7XNAIOF.js} +2 -1
- package/dist/studio/{domEditingLayers-IGPLTI5E.js → domEditingLayers-MHFDKAQ7.js} +2 -2
- package/dist/studio/index.d.ts +40 -16
- package/dist/studio/index.html +1 -1
- package/dist/studio/index.js +1918 -1408
- package/dist/studio/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/studio/assets/hyperframes-player-BdnQGwG5.js +0 -464
- /package/dist/studio/{chunk-5VANA2TG.js.map → chunk-I7XNAIOF.js.map} +0 -0
- /package/dist/studio/{domEditingLayers-IGPLTI5E.js.map → domEditingLayers-MHFDKAQ7.js.map} +0 -0
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
|
|
2
2
|
import { join, basename } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
import { readManifest, appendRecord, normalizePrompt } from "./manifest.mjs";
|
|
5
|
+
import { globalMediaDir, mediaHome } from "./media-home.mjs";
|
|
6
|
+
|
|
7
|
+
export { globalMediaDir };
|
|
6
8
|
|
|
7
9
|
const SCHEMA_PREFIX = "mu-v1-";
|
|
8
10
|
const KEY_HEX_CHARS = 16;
|
|
9
11
|
const COMPLETE_SENTINEL = ".hf-complete";
|
|
10
12
|
|
|
11
|
-
export function globalMediaDir() {
|
|
12
|
-
return join(homedir(), ".media");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
13
|
export function contentHash(filePath) {
|
|
16
14
|
const bytes = readFileSync(filePath);
|
|
17
15
|
return createHash("sha256").update(bytes).digest("hex");
|
|
@@ -30,11 +28,11 @@ function markComplete(entryDir) {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// The manifest helpers append their own ".media" to the dir they get, so the
|
|
33
|
-
// global manifest must be addressed by
|
|
31
|
+
// global manifest must be addressed by mediaHome(), not by globalMediaDir() — passing
|
|
34
32
|
// the latter nested it at ~/.media/.media/manifest.jsonl, invisible to the
|
|
35
33
|
// Studio /api/assets/global route (which reads the documented flat path).
|
|
36
34
|
export function readGlobalManifest() {
|
|
37
|
-
return readManifest(
|
|
35
|
+
return readManifest(mediaHome());
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
// Resolve a content-sha (full or unambiguous prefix) to a reusable global-cache
|
|
@@ -105,7 +103,7 @@ export function cachePut(filePath, record) {
|
|
|
105
103
|
reusable: true,
|
|
106
104
|
cached_path: dest,
|
|
107
105
|
};
|
|
108
|
-
appendRecord(
|
|
106
|
+
appendRecord(mediaHome(), globalRecord);
|
|
109
107
|
return { sha, cached_path: dest };
|
|
110
108
|
}
|
|
111
109
|
|
|
@@ -6,13 +6,13 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { listCandidates, formatCandidates, CANDIDATE_CAP } from "./candidates.mjs";
|
|
7
7
|
import { findGlobalBySha } from "./cache.mjs";
|
|
8
8
|
|
|
9
|
-
// candidates + findGlobalBySha are offline (no heygen), so we can
|
|
9
|
+
// candidates + findGlobalBySha are offline (no heygen), so we can point HYPERFRAMES_MEDIA_HOME
|
|
10
10
|
// to a temp dir and seed a fake global ~/.media manifest deterministically.
|
|
11
11
|
function sandbox() {
|
|
12
12
|
const root = mkdtempSync(join(tmpdir(), "mu-cand-"));
|
|
13
13
|
const project = join(root, "proj");
|
|
14
14
|
const home = join(root, "home");
|
|
15
|
-
process.env.
|
|
15
|
+
process.env.HYPERFRAMES_MEDIA_HOME = home;
|
|
16
16
|
return { root, project, home };
|
|
17
17
|
}
|
|
18
18
|
function seedManifest(dir, records) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The one lock on ~/.hyperframes/config.json, for the CLI, its auto-update step and media-use. The auto-update step
|
|
2
|
+
// embeds this function's source, so it must use only its arguments and globals.
|
|
3
|
+
export function withFileLock(lockPath, fs, task) {
|
|
4
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
5
|
+
const started = Date.now();
|
|
6
|
+
for (;;) {
|
|
7
|
+
let fd;
|
|
8
|
+
try {
|
|
9
|
+
fd = fs.openSync(lockPath, "wx");
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error.code !== "EEXIST") throw error;
|
|
12
|
+
// Never taken over: a lock older than any hold was left by a process that stopped, and a person removes it.
|
|
13
|
+
let leftover = false;
|
|
14
|
+
try {
|
|
15
|
+
leftover = Date.now() - fs.statSync(lockPath).mtimeMs > 5000;
|
|
16
|
+
} catch {}
|
|
17
|
+
if (leftover || Date.now() - started > 10000)
|
|
18
|
+
throw Object.assign(
|
|
19
|
+
new Error(
|
|
20
|
+
`Settings are locked by another hyperframes process. If none is running, delete ${lockPath}`,
|
|
21
|
+
),
|
|
22
|
+
{ code: "HF_SETTINGS_LOCKED" },
|
|
23
|
+
);
|
|
24
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
fs.writeSync(fd, token);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
fs.rmSync(lockPath, { force: true });
|
|
31
|
+
throw error;
|
|
32
|
+
} finally {
|
|
33
|
+
fs.closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return task();
|
|
39
|
+
} finally {
|
|
40
|
+
// No code removes another's lock, so release needs no check (only a person deleting a live one defeats it).
|
|
41
|
+
try {
|
|
42
|
+
fs.rmSync(lockPath);
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { withFileLock } from "./config-lock.mjs";
|
|
8
|
+
|
|
9
|
+
function lockIn() {
|
|
10
|
+
const dir = fs.mkdtempSync(join(tmpdir(), "hf-config-lock-"));
|
|
11
|
+
return {
|
|
12
|
+
lock: join(dir, "config.json.lock"),
|
|
13
|
+
cleanup: () => fs.rmSync(dir, { recursive: true }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("returns the task's result even when the lock cannot be released", () => {
|
|
18
|
+
const { lock, cleanup } = lockIn();
|
|
19
|
+
try {
|
|
20
|
+
const refusing = {
|
|
21
|
+
...fs,
|
|
22
|
+
rmSync(path, options) {
|
|
23
|
+
if (path === lock) throw Object.assign(new Error("EPERM"), { code: "EPERM" });
|
|
24
|
+
return fs.rmSync(path, options);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
assert.equal(
|
|
28
|
+
withFileLock(lock, refusing, () => "saved"),
|
|
29
|
+
"saved",
|
|
30
|
+
);
|
|
31
|
+
} finally {
|
|
32
|
+
cleanup();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("refuses a lock left by a process that stopped, names it, and never removes it", () => {
|
|
37
|
+
const { lock, cleanup } = lockIn();
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(lock, "dead");
|
|
40
|
+
const past = new Date(Date.now() - 60_000);
|
|
41
|
+
fs.utimesSync(lock, past, past);
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
let ran = false;
|
|
44
|
+
|
|
45
|
+
assert.throws(
|
|
46
|
+
() => withFileLock(lock, fs, () => (ran = true)),
|
|
47
|
+
(error) => error.code === "HF_SETTINGS_LOCKED" && error.message.includes(lock),
|
|
48
|
+
);
|
|
49
|
+
assert.ok(Date.now() - started < 1000);
|
|
50
|
+
assert.equal(ran, false);
|
|
51
|
+
assert.equal(fs.readFileSync(lock, "utf8"), "dead");
|
|
52
|
+
} finally {
|
|
53
|
+
cleanup();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("waits for a process that holds the lock, then takes it", async () => {
|
|
58
|
+
const { lock, cleanup } = lockIn();
|
|
59
|
+
try {
|
|
60
|
+
const script = `
|
|
61
|
+
const fs = require("fs");
|
|
62
|
+
const lock = process.argv[1];
|
|
63
|
+
fs.writeFileSync(lock, "other", { flag: "wx" });
|
|
64
|
+
process.stdout.write("locked\\n");
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
fs.writeFileSync(lock + ".released", String(Date.now()));
|
|
67
|
+
fs.rmSync(lock);
|
|
68
|
+
}, 300);`;
|
|
69
|
+
const other = spawn(process.execPath, ["-e", script, lock]);
|
|
70
|
+
await new Promise((ready) => other.stdout.once("data", ready));
|
|
71
|
+
|
|
72
|
+
const ranAt = withFileLock(lock, fs, () => Date.now());
|
|
73
|
+
|
|
74
|
+
assert.ok(ranAt >= Number(fs.readFileSync(`${lock}.released`, "utf8")));
|
|
75
|
+
await new Promise((exited) => other.once("exit", exited));
|
|
76
|
+
} finally {
|
|
77
|
+
cleanup();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
// The one owner of where the person's media library lives. This repo's test runs set
|
|
5
|
+
// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot HYPERFRAMES_MEDIA_HOME fails instead
|
|
6
|
+
// of writing fixtures into the media library apps list as the person's own files.
|
|
7
|
+
export function mediaHome() {
|
|
8
|
+
const override = process.env.HYPERFRAMES_MEDIA_HOME;
|
|
9
|
+
if (override) return override;
|
|
10
|
+
if (process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"media-use: a test reached the real ~/.media; set HYPERFRAMES_MEDIA_HOME to a temp dir",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return homedir();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function globalMediaDir() {
|
|
19
|
+
return join(mediaHome(), ".media");
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
|
|
8
|
+
import { cachePut } from "./cache.mjs";
|
|
9
|
+
import { globalMediaDir } from "./media-home.mjs";
|
|
10
|
+
|
|
11
|
+
const HOME = mkdtempSync(join(tmpdir(), "media-home-"));
|
|
12
|
+
process.env.HYPERFRAMES_MEDIA_HOME = HOME;
|
|
13
|
+
|
|
14
|
+
test("the global cache writes into the media home a test points it at", () => {
|
|
15
|
+
const work = mkdtempSync(join(tmpdir(), "media-home-src-"));
|
|
16
|
+
const file = join(work, "clip.wav");
|
|
17
|
+
writeFileSync(file, "media-home regression bytes");
|
|
18
|
+
cachePut(file, { id: "bgm_001", type: "bgm", provenance: { prompt: "media home probe" } });
|
|
19
|
+
const manifest = join(HOME, ".media", "manifest.jsonl");
|
|
20
|
+
assert.ok(existsSync(manifest), "the temp media home holds the global manifest");
|
|
21
|
+
assert.match(readFileSync(manifest, "utf8"), /media home probe/);
|
|
22
|
+
assert.equal(globalMediaDir(), join(HOME, ".media"));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// A child process that resolves the media dir, with only the given media-home variables set.
|
|
26
|
+
function runProbe(env) {
|
|
27
|
+
const lib = new URL("./media-home.mjs", import.meta.url).href;
|
|
28
|
+
const childEnv = { ...process.env };
|
|
29
|
+
delete childEnv.HYPERFRAMES_MEDIA_HOME;
|
|
30
|
+
delete childEnv.HYPERFRAMES_MEDIA_HOME_REQUIRED;
|
|
31
|
+
delete childEnv.NODE_TEST_CONTEXT;
|
|
32
|
+
const run = spawnSync(
|
|
33
|
+
process.execPath,
|
|
34
|
+
[
|
|
35
|
+
"--input-type=module",
|
|
36
|
+
"-e",
|
|
37
|
+
`const m = await import(${JSON.stringify(lib)}); console.log("dir=" + m.globalMediaDir());`,
|
|
38
|
+
],
|
|
39
|
+
{ encoding: "utf8", env: { ...childEnv, ...env } },
|
|
40
|
+
);
|
|
41
|
+
return { status: run.status, output: `${run.stdout}${run.stderr}` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("a repo test run that never points the media home anywhere fails instead of writing", () => {
|
|
45
|
+
const { status, output } = runProbe({ HYPERFRAMES_MEDIA_HOME_REQUIRED: "1" });
|
|
46
|
+
assert.notEqual(status, 0, output);
|
|
47
|
+
assert.match(output, /set HYPERFRAMES_MEDIA_HOME to a temp dir/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Node sets NODE_TEST_CONTEXT for every project's test runs, so another project whose own tests
|
|
51
|
+
// spawn this CLI must get the person's real library, not the guard.
|
|
52
|
+
test("another project's node --test run still resolves the real media home", () => {
|
|
53
|
+
const home = mkdtempSync(join(tmpdir(), "media-home-user-"));
|
|
54
|
+
const { status, output } = runProbe({
|
|
55
|
+
HOME: home,
|
|
56
|
+
USERPROFILE: home,
|
|
57
|
+
NODE_TEST_CONTEXT: "child-v8",
|
|
58
|
+
});
|
|
59
|
+
assert.equal(status, 0, output);
|
|
60
|
+
assert.ok(output.includes(`dir=${join(home, ".media")}`), output);
|
|
61
|
+
});
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { globalMediaDir } from "./media-home.mjs";
|
|
4
4
|
|
|
5
5
|
const MISSES_FILE = "misses.jsonl";
|
|
6
6
|
|
|
7
7
|
function missesPath() {
|
|
8
|
-
return join(
|
|
8
|
+
return join(globalMediaDir(), MISSES_FILE);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function recordMiss({ type, intent, provider_override, local_only }) {
|
|
12
12
|
try {
|
|
13
|
-
const
|
|
14
|
-
mkdirSync(
|
|
13
|
+
const path = missesPath();
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
15
|
appendFileSync(
|
|
16
|
-
|
|
16
|
+
path,
|
|
17
17
|
JSON.stringify({
|
|
18
18
|
ts: new Date().toISOString(),
|
|
19
19
|
type,
|
|
@@ -9,7 +9,7 @@ function sandbox() {
|
|
|
9
9
|
const root = mkdtempSync(join(tmpdir(), "mu-misses-"));
|
|
10
10
|
const home = join(root, "home");
|
|
11
11
|
mkdirSync(home, { recursive: true });
|
|
12
|
-
process.env.
|
|
12
|
+
process.env.HYPERFRAMES_MEDIA_HOME = home;
|
|
13
13
|
return { root, home };
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -54,6 +54,17 @@ test("recordMiss swallows filesystem failures", () => {
|
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
test("recordMiss stays best-effort when the media home cannot be resolved", () => {
|
|
58
|
+
const savedEnv = { ...process.env };
|
|
59
|
+
delete process.env.HYPERFRAMES_MEDIA_HOME;
|
|
60
|
+
process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED = "1";
|
|
61
|
+
try {
|
|
62
|
+
assert.doesNotThrow(() => recordMiss({ type: "sfx", intent: "no home", local_only: true }));
|
|
63
|
+
} finally {
|
|
64
|
+
restoreEnv(savedEnv);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
57
68
|
test("readMisses skips corrupt lines", () => {
|
|
58
69
|
const savedEnv = { ...process.env };
|
|
59
70
|
const { root, home } = sandbox();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { globalMediaDir } from "./media-home.mjs";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Remembered defaults — the lightweight tier of HyperFrames user memory.
|
|
@@ -41,7 +41,7 @@ export function projectPrefsPath(projectDir) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function userPrefsPath() {
|
|
44
|
-
return join(
|
|
44
|
+
return join(globalMediaDir(), PREFS_FILE);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function emptyFile() {
|
|
@@ -19,7 +19,7 @@ function sandbox() {
|
|
|
19
19
|
mkdirSync(home, { recursive: true });
|
|
20
20
|
mkdirSync(projectA, { recursive: true });
|
|
21
21
|
mkdirSync(projectB, { recursive: true });
|
|
22
|
-
process.env.
|
|
22
|
+
process.env.HYPERFRAMES_MEDIA_HOME = home;
|
|
23
23
|
return { root, home, projectA, projectB };
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
rmSync,
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
11
|
import { basename, join, resolve } from "node:path";
|
|
12
|
+
import { globalMediaDir } from "./media-home.mjs";
|
|
13
13
|
import { appendRecord, mediaDir, nextId } from "./manifest.mjs";
|
|
14
14
|
import { regenerateIndex } from "./index-gen.mjs";
|
|
15
15
|
import { mergedPreferences } from "./prefs-store.mjs";
|
|
@@ -58,7 +58,7 @@ export function projectRecipesDir(projectDir) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function userRecipesDir() {
|
|
61
|
-
return join(
|
|
61
|
+
return join(globalMediaDir(), "recipes");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function slugifyRecipeName(name) {
|
|
@@ -83,7 +83,7 @@ function sandbox() {
|
|
|
83
83
|
mkdirSync(project, { recursive: true });
|
|
84
84
|
writeFileSync(join(project, "frame.md"), "# Frame spec\nbackground: #101014\n");
|
|
85
85
|
writeFileSync(join(project, "STORYBOARD.md"), STORYBOARD);
|
|
86
|
-
process.env.
|
|
86
|
+
process.env.HYPERFRAMES_MEDIA_HOME = home;
|
|
87
87
|
return { root, home, project };
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -11,7 +11,7 @@ function sandbox() {
|
|
|
11
11
|
const projectDir = join(root, "project");
|
|
12
12
|
mkdirSync(home, { recursive: true });
|
|
13
13
|
mkdirSync(projectDir, { recursive: true });
|
|
14
|
-
process.env.
|
|
14
|
+
process.env.HYPERFRAMES_MEDIA_HOME = home;
|
|
15
15
|
return { root, home, projectDir };
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
// never carry intent text, file names, or paths.
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { withFileLock } from "./config-lock.mjs";
|
|
10
|
+
import { globalMediaDir } from "./media-home.mjs";
|
|
8
11
|
|
|
9
12
|
const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
|
|
10
13
|
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
@@ -61,10 +64,24 @@ function readSharedConfig() {
|
|
|
61
64
|
return {};
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// Best effort: skipped when the lock cannot be taken or the file cannot be read, never written unlocked.
|
|
68
|
+
function updateSharedConfig(patch) {
|
|
69
|
+
const file = sharedConfigPath();
|
|
70
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
71
|
+
try {
|
|
72
|
+
withFileLock(`${file}.lock`, fs, () => {
|
|
73
|
+
let config = {};
|
|
74
|
+
try {
|
|
75
|
+
config = JSON.parse(readFileSync(file, "utf8"));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code !== "ENOENT") return;
|
|
78
|
+
}
|
|
79
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return;
|
|
80
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
81
|
+
writeFileSync(tmp, JSON.stringify({ ...config, ...patch }, null, 2) + "\n", { mode: 0o600 });
|
|
82
|
+
renameSync(tmp, file);
|
|
83
|
+
});
|
|
84
|
+
} catch {}
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
// Adopt a pre-existing media-use-only id (~/.media/anon-id from before this
|
|
@@ -72,7 +89,7 @@ function writeSharedConfig(config) {
|
|
|
72
89
|
// one — otherwise cross-surface continuity would start over on upgrade.
|
|
73
90
|
function legacyMediaAnonId() {
|
|
74
91
|
try {
|
|
75
|
-
const file = join(
|
|
92
|
+
const file = join(globalMediaDir(), "anon-id");
|
|
76
93
|
if (existsSync(file)) {
|
|
77
94
|
const id = readFileSync(file, "utf8").trim();
|
|
78
95
|
if (id) return id;
|
|
@@ -92,7 +109,7 @@ function anonymousId() {
|
|
|
92
109
|
return config.anonymousId.trim();
|
|
93
110
|
}
|
|
94
111
|
const id = legacyMediaAnonId() || randomUUID();
|
|
95
|
-
|
|
112
|
+
updateSharedConfig({ anonymousId: id });
|
|
96
113
|
return id;
|
|
97
114
|
} catch {
|
|
98
115
|
return "anon"; // best-effort; a shared bucket is fine if the fs is read-only
|
|
@@ -132,7 +149,7 @@ function showTelemetryNotice() {
|
|
|
132
149
|
"If you sign in to HeyGen, usage links to your account email or username. Opt out with HYPERFRAMES_NO_TELEMETRY=1 or DO_NOT_TRACK=1.",
|
|
133
150
|
].join("\n"),
|
|
134
151
|
);
|
|
135
|
-
|
|
152
|
+
updateSharedConfig({ telemetryNoticeShown: true });
|
|
136
153
|
} catch {
|
|
137
154
|
// notice is best-effort; never surface into the command
|
|
138
155
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { test } from "node:test";
|
|
3
4
|
import {
|
|
4
5
|
existsSync,
|
|
@@ -18,6 +19,7 @@ function sandbox() {
|
|
|
18
19
|
const home = join(root, "home");
|
|
19
20
|
mkdirSync(home, { recursive: true });
|
|
20
21
|
process.env.HOME = home;
|
|
22
|
+
process.env.HYPERFRAMES_MEDIA_HOME = home;
|
|
21
23
|
return { root, home };
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -371,3 +373,65 @@ test("read-only telemetry state degrades without throwing", async () => {
|
|
|
371
373
|
__resetTelemetryForTest();
|
|
372
374
|
}
|
|
373
375
|
});
|
|
376
|
+
|
|
377
|
+
async function trackQuietly(home) {
|
|
378
|
+
const originalFetch = globalThis.fetch;
|
|
379
|
+
const originalError = console.error;
|
|
380
|
+
globalThis.fetch = async () => ({ ok: true });
|
|
381
|
+
console.error = () => {};
|
|
382
|
+
try {
|
|
383
|
+
withoutTelemetryOptOut();
|
|
384
|
+
await track("media_use_resolve", { type: "bgm" });
|
|
385
|
+
return readFileSync(join(home, ".hyperframes/config.json"), "utf8");
|
|
386
|
+
} finally {
|
|
387
|
+
globalThis.fetch = originalFetch;
|
|
388
|
+
console.error = originalError;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
test("waits for another process's locked settings write, then keeps the no it saved", async () => {
|
|
393
|
+
const savedEnv = { ...process.env };
|
|
394
|
+
const { root, home } = sandbox();
|
|
395
|
+
const dir = join(home, ".hyperframes");
|
|
396
|
+
mkdirSync(dir, { recursive: true });
|
|
397
|
+
const holder = spawn(process.execPath, [
|
|
398
|
+
"-e",
|
|
399
|
+
`const fs = require("fs");
|
|
400
|
+
const dir = process.argv[1];
|
|
401
|
+
fs.writeFileSync(dir + "/config.json.lock", "", { flag: "wx" });
|
|
402
|
+
process.stdout.write("locked\\n");
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
fs.writeFileSync(dir + "/other.tmp", JSON.stringify({ localEmbeddingEnabled: false }));
|
|
405
|
+
fs.renameSync(dir + "/other.tmp", dir + "/config.json");
|
|
406
|
+
fs.rmSync(dir + "/config.json.lock");
|
|
407
|
+
}, 1000);`,
|
|
408
|
+
dir,
|
|
409
|
+
]);
|
|
410
|
+
const exited = new Promise((done) => holder.on("exit", done));
|
|
411
|
+
try {
|
|
412
|
+
await new Promise((locked) => holder.stdout.once("data", locked));
|
|
413
|
+
const saved = JSON.parse(await trackQuietly(home));
|
|
414
|
+
await exited;
|
|
415
|
+
assert.equal(saved.localEmbeddingEnabled, false);
|
|
416
|
+
assert.equal(saved.telemetryNoticeShown, true);
|
|
417
|
+
assert.equal(typeof saved.anonymousId, "string");
|
|
418
|
+
} finally {
|
|
419
|
+
restoreEnv(savedEnv);
|
|
420
|
+
rmSync(root, { recursive: true, force: true });
|
|
421
|
+
__resetTelemetryForTest();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("leaves a settings file it cannot read untouched", async () => {
|
|
426
|
+
const savedEnv = { ...process.env };
|
|
427
|
+
const { root, home } = sandbox();
|
|
428
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
429
|
+
writeFileSync(join(home, ".hyperframes/config.json"), "{not json");
|
|
430
|
+
try {
|
|
431
|
+
assert.equal(await trackQuietly(home), "{not json");
|
|
432
|
+
} finally {
|
|
433
|
+
restoreEnv(savedEnv);
|
|
434
|
+
rmSync(root, { recursive: true, force: true });
|
|
435
|
+
__resetTelemetryForTest();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
@@ -50,30 +50,10 @@ var CACHE_DIR = join(homedir(), ".cache", "hyperframes", "optional");
|
|
|
50
50
|
function optionalPackageDir(name, cacheDir = CACHE_DIR) {
|
|
51
51
|
return join(cacheDir, `${name.replace("/", "__")}@${OPTIONAL_PACKAGES[name]}`);
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const dir = optionalPackageDir(name, deps.cacheDir);
|
|
57
|
-
const installed = deps.loadInstalled(dir, name);
|
|
58
|
-
if (installed !== null) return installed;
|
|
59
|
-
const version = OPTIONAL_PACKAGES[name];
|
|
60
|
-
deps.log(`installing ${name} for ${feature}, once`);
|
|
61
|
-
try {
|
|
62
|
-
await deps.install(dir, name, version);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
const output = err.message.trim();
|
|
65
|
-
const advice = NETWORK_FAILURE.test(output) ? "Check your network connection, then retry" : "Retry";
|
|
66
|
-
throw new Error(
|
|
67
|
-
`${feature} needs ${name}, and installing it failed (${output.split("\n")[0]}). ${advice}, or install it yourself: npm install ${name}@${version} --prefix "${dir}"`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
const loaded = deps.loadInstalled(dir, name);
|
|
71
|
-
if (loaded === null) {
|
|
72
|
-
throw new Error(`${name}@${version} installed into ${dir} but could not be loaded.`);
|
|
73
|
-
}
|
|
74
|
-
return loaded;
|
|
53
|
+
function loadInstalledOptionalPackage(name, deps = defaultDeps) {
|
|
54
|
+
const found = deps.loadBesideCli(name) ?? deps.loadInstalled(optionalPackageDir(name, deps.cacheDir), name);
|
|
55
|
+
return found;
|
|
75
56
|
}
|
|
76
|
-
var NETWORK_FAILURE = /ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ECONNRESET|ECONNREFUSED|network/i;
|
|
77
57
|
function manifestPath(dir, name) {
|
|
78
58
|
return join(dir, "node_modules", name, "package.json");
|
|
79
59
|
}
|
|
@@ -187,6 +167,7 @@ import { Transform } from "stream";
|
|
|
187
167
|
import { pipeline } from "stream/promises";
|
|
188
168
|
|
|
189
169
|
// src/telemetry/config.ts
|
|
170
|
+
import * as fs from "fs";
|
|
190
171
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
191
172
|
import { join as join2 } from "path";
|
|
192
173
|
import { homedir as homedir2 } from "os";
|
|
@@ -333,9 +314,9 @@ function isChinese(code) {
|
|
|
333
314
|
|
|
334
315
|
// src/registry/localEmbedder.ts
|
|
335
316
|
var MAX_TOKENS = 512;
|
|
336
|
-
var FEATURE = "on-device search";
|
|
337
317
|
async function loadLocalEmbedder() {
|
|
338
|
-
const ort =
|
|
318
|
+
const ort = loadInstalledOptionalPackage("onnxruntime-node");
|
|
319
|
+
if (ort === null) throw new Error("the on-device runtime is not installed yet");
|
|
339
320
|
const config = configFromTokenizerJson(readFileSync4(localTokenizerPath(), "utf-8"));
|
|
340
321
|
const session = await ort.InferenceSession.create(localModelPath());
|
|
341
322
|
return {
|