@hyperframes/studio-server 0.8.80 → 0.8.81

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/index.js CHANGED
@@ -22,21 +22,28 @@ import {
22
22
  isAutoProxyEnabled,
23
23
  proxyEtagSalt,
24
24
  resolvePreviewMediaCodecProbeCache
25
- } from "./chunk-YGANSFUU.js";
25
+ } from "./chunk-FTDTO3QP.js";
26
26
  import {
27
27
  ProxyCapacityError,
28
28
  ProxyTranscodeError,
29
29
  resolveProxy
30
- } from "./chunk-56JREE33.js";
30
+ } from "./chunk-G6IIFKXY.js";
31
31
  import {
32
32
  PROXY_VARIANT_CONFIG,
33
33
  decideMediaProxyEligibility,
34
+ isInHiddenOrVendorDir,
34
35
  isProxyVariantRequest,
36
+ isSafePath,
37
+ pinWithinProject,
35
38
  probeAssetCodec,
36
39
  probeMediaMetadata,
40
+ realFilePath,
41
+ realpath,
37
42
  recordProxyRequest,
38
- resolveProxyVariantRequest
39
- } from "./chunk-KOB22VPG.js";
43
+ resolveProxyVariantRequest,
44
+ resolveWithinProject,
45
+ walkDir
46
+ } from "./chunk-TJQ76IEA.js";
40
47
  import {
41
48
  STUDIO_MANUAL_EDITS_PATH,
42
49
  createStudioManualEditsRenderBodyScript,
@@ -52,64 +59,7 @@ import { Hono } from "hono";
52
59
 
53
60
  // src/routes/projects.ts
54
61
  import { readFile } from "fs/promises";
55
- import { join as join3 } from "path";
56
-
57
- // src/helpers/safePath.ts
58
- import { basename, dirname, isAbsolute, join, relative, sep } from "path";
59
- import { readdirSync, realpathSync } from "fs";
60
- import { resolveWithinProject } from "@hyperframes/core";
61
- import { isSafePath, resolveWithinProject as resolveWithinProject2 } from "@hyperframes/core";
62
- function realFilePath(filePath) {
63
- try {
64
- return realpathSync(filePath);
65
- } catch {
66
- const dir = dirname(filePath);
67
- return dir === filePath ? filePath : join(realFilePath(dir), basename(filePath));
68
- }
69
- }
70
- function pinWithinProject(base, relativePath) {
71
- const checked = resolveWithinProject(base, relativePath);
72
- if (!checked) return null;
73
- const inside = relative(realFilePath(base), realFilePath(checked));
74
- const escapes = inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
75
- return escapes ? null : join(base, inside);
76
- }
77
- var IGNORE_DIRS = /* @__PURE__ */ new Set([
78
- ".thumbnails",
79
- ".transcode-cache",
80
- ".waveform-cache",
81
- "node_modules",
82
- ".git"
83
- ]);
84
- function shouldIgnoreDir(rel) {
85
- return rel === ".hyperframes/backup";
86
- }
87
- function isInHiddenOrVendorDir(relPath) {
88
- const segments = relPath.split("/");
89
- return segments.slice(0, -1).some((seg) => seg.startsWith(".") || seg === "node_modules");
90
- }
91
- var UNREADABLE_DIR_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "ENOENT", "ENOTDIR"]);
92
- function walkDir(dir, prefix = "") {
93
- let entries;
94
- try {
95
- entries = readdirSync(dir, { withFileTypes: true });
96
- } catch (err) {
97
- const code = err.code;
98
- if (!prefix || !code || !UNREADABLE_DIR_CODES.has(code)) throw err;
99
- return [];
100
- }
101
- const files = [];
102
- for (const entry of entries) {
103
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
104
- if (IGNORE_DIRS.has(entry.name) || shouldIgnoreDir(rel)) continue;
105
- if (entry.isDirectory()) {
106
- files.push(...walkDir(join(dir, entry.name), rel));
107
- } else {
108
- files.push(rel);
109
- }
110
- }
111
- return files;
112
- }
62
+ import { join as join2 } from "path";
113
63
 
114
64
  // src/helpers/hfIdPersist.ts
115
65
  import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
@@ -123,7 +73,7 @@ import {
123
73
  readFileSync,
124
74
  writeFileSync
125
75
  } from "fs";
126
- import { dirname as dirname2, join as join2 } from "path";
76
+ import { dirname, join } from "path";
127
77
 
128
78
  // src/helpers/atomicFile.ts
129
79
  import * as fs from "fs";
@@ -145,10 +95,10 @@ function replaceFileAtomically(filePath, content, mode, operations = fs) {
145
95
 
146
96
  // src/helpers/hfIdPersist.ts
147
97
  var isCompositionSource = (html) => /data-composition-id\s*=/.test(html);
148
- var STAMP_RECORD = join2(".hyperframes", "hf-ids-stamped.json");
98
+ var STAMP_RECORD = join(".hyperframes", "hf-ids-stamped.json");
149
99
  var contentHash = (text2) => createHash("sha1").update(text2).digest("base64url");
150
100
  function stampProjectHfIds(projectDir) {
151
- const recordPath = join2(projectDir, STAMP_RECORD);
101
+ const recordPath = join(projectDir, STAMP_RECORD);
152
102
  let last = {};
153
103
  try {
154
104
  last = JSON.parse(readFileSync(recordPath, "utf-8"));
@@ -157,7 +107,7 @@ function stampProjectHfIds(projectDir) {
157
107
  const next = {};
158
108
  for (const file of walkDir(projectDir)) {
159
109
  if (!file.endsWith(".html") || isInHiddenOrVendorDir(file)) continue;
160
- const absPath = join2(projectDir, file);
110
+ const absPath = join(projectDir, file);
161
111
  let html;
162
112
  try {
163
113
  html = readFileSync(absPath, "utf-8");
@@ -173,7 +123,7 @@ function stampProjectHfIds(projectDir) {
173
123
  next[file] = hash;
174
124
  }
175
125
  try {
176
- mkdirSync(dirname2(recordPath), { recursive: true });
126
+ mkdirSync(dirname(recordPath), { recursive: true });
177
127
  writeFileSync(recordPath, JSON.stringify(next));
178
128
  } catch {
179
129
  }
@@ -226,8 +176,8 @@ function stampFileHfIds(filePath) {
226
176
 
227
177
  // src/helpers/projectSignature.ts
228
178
  import { createHash as createHash2 } from "crypto";
229
- import { lstatSync, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
230
- import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve, sep as sep2 } from "path";
179
+ import { lstatSync, readFileSync as readFileSync2, readdirSync } from "fs";
180
+ import { extname, isAbsolute, relative, resolve, sep } from "path";
231
181
  var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
232
182
  ".cjs",
233
183
  ".css",
@@ -262,18 +212,18 @@ var STUDIO_SIGNATURE_MANIFEST_PATHS = [
262
212
  ".hyperframes/studio-motion.json"
263
213
  ];
264
214
  function affectsProjectSignature(projectDir, changedPath) {
265
- const relativePath = relative2(resolve(projectDir), resolve(changedPath));
266
- if (relativePath === "" || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
215
+ const relativePath = relative(resolve(projectDir), resolve(changedPath));
216
+ if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath)) {
267
217
  return false;
268
218
  }
269
- const segments = relativePath.split(sep2);
219
+ const segments = relativePath.split(sep);
270
220
  if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
271
221
  return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment));
272
222
  }
273
223
  var projectSignatureCache = /* @__PURE__ */ new Map();
274
224
  function isPathWithin(parentDir, childPath) {
275
- const childRelativePath = relative2(parentDir, childPath);
276
- return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute2(childRelativePath);
225
+ const childRelativePath = relative(parentDir, childPath);
226
+ return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
277
227
  }
278
228
  function isTextContentEligible(file, size) {
279
229
  return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
@@ -281,7 +231,7 @@ function isTextContentEligible(file, size) {
281
231
  function collectProjectSignatureFiles(projectDir, dir, files) {
282
232
  let entries;
283
233
  try {
284
- entries = readdirSync2(dir).sort();
234
+ entries = readdirSync(dir).sort();
285
235
  } catch {
286
236
  return;
287
237
  }
@@ -334,7 +284,7 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
334
284
  function createProjectFingerprint(projectDir, files) {
335
285
  const hash = createHash2("sha256");
336
286
  for (const entry of files) {
337
- hash.update(relative2(projectDir, entry.file));
287
+ hash.update(relative(projectDir, entry.file));
338
288
  hash.update("\0");
339
289
  hash.update(String(entry.size));
340
290
  hash.update("\0");
@@ -356,7 +306,7 @@ function collectProjectFiles(normalizedProjectDir) {
356
306
  function listProjectFiles(projectDir) {
357
307
  const normalizedProjectDir = resolve(projectDir);
358
308
  return collectProjectFiles(normalizedProjectDir).map((entry) => ({
359
- path: relative2(normalizedProjectDir, entry.file).split(sep2).join("/"),
309
+ path: relative(normalizedProjectDir, entry.file).split(sep).join("/"),
360
310
  size: entry.size,
361
311
  mtimeMs: entry.mtimeMs,
362
312
  ctimeMs: entry.ctimeMs
@@ -374,7 +324,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
374
324
  const normalizedProjectDir = resolve(projectDir);
375
325
  const collected = collectProjectFiles(normalizedProjectDir);
376
326
  const files = collected.filter(
377
- (entry) => !excluding.has(relative2(normalizedProjectDir, entry.file).split(sep2).join("/"))
327
+ (entry) => !excluding.has(relative(normalizedProjectDir, entry.file).split(sep).join("/"))
378
328
  );
379
329
  files.sort((a, b) => a.file.localeCompare(b.file));
380
330
  const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
@@ -383,7 +333,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
383
333
  if (cached?.fingerprint === fingerprint) return cached.signature;
384
334
  const hash = createHash2("sha256");
385
335
  for (const entry of files) {
386
- const relativePath = relative2(normalizedProjectDir, entry.file);
336
+ const relativePath = relative(normalizedProjectDir, entry.file);
387
337
  hash.update(relativePath);
388
338
  hash.update("\0");
389
339
  hash.update(String(entry.size));
@@ -410,7 +360,7 @@ async function filterCompositionFiles(projectDir, files) {
410
360
  const checks = await Promise.all(
411
361
  htmlFiles.map(async (f) => {
412
362
  try {
413
- const content = await readFile(join3(projectDir, f), "utf-8");
363
+ const content = await readFile(join2(projectDir, f), "utf-8");
414
364
  return isCompositionSource(content);
415
365
  } catch {
416
366
  return false;
@@ -464,9 +414,9 @@ import {
464
414
  statSync as statSync3,
465
415
  fstatSync as fstatSync2,
466
416
  renameSync as renameSync2,
467
- readdirSync as readdirSync4
417
+ readdirSync as readdirSync3
468
418
  } from "fs";
469
- import { resolve as resolve3, dirname as dirname5, join as join7 } from "path";
419
+ import { resolve as resolve3, dirname as dirname4, join as join6 } from "path";
470
420
 
471
421
  // src/helpers/mime.ts
472
422
  var MIME_TYPES = {
@@ -526,7 +476,7 @@ import {
526
476
  rmSync,
527
477
  statSync
528
478
  } from "fs";
529
- import { dirname as dirname3, join as join4 } from "path";
479
+ import { dirname as dirname2, join as join3 } from "path";
530
480
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
531
481
  var SAMPLE_RATE = 4e3;
532
482
  var PEAK_COUNT = 4e3;
@@ -593,11 +543,11 @@ function decodeAudioPeaks(audioPath) {
593
543
  });
594
544
  }
595
545
  async function generateWaveformCache(projectDir, assetPath) {
596
- const audioPath = join4(projectDir, assetPath);
546
+ const audioPath = join3(projectDir, assetPath);
597
547
  if (!existsSync(audioPath)) return;
598
548
  const stats = statSync(audioPath);
599
- const cacheDir = join4(projectDir, ".waveform-cache");
600
- const cachePath = join4(cacheDir, buildWaveformCacheKey(assetPath, stats));
549
+ const cacheDir = join3(projectDir, ".waveform-cache");
550
+ const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
601
551
  if (isWaveformCacheDirectory(cacheDir) && existsSync(cachePath)) return;
602
552
  const peaks = await decodeAudioPeaks(audioPath);
603
553
  writeWaveformCache(cachePath, peaks);
@@ -606,7 +556,7 @@ function isWaveformCacheDirectory(cacheDir) {
606
556
  return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
607
557
  }
608
558
  function writeWaveformCache(cachePath, peaks) {
609
- const cacheDir = dirname3(cachePath);
559
+ const cacheDir = dirname2(cachePath);
610
560
  try {
611
561
  mkdirSync2(cacheDir, { mode: 448 });
612
562
  } catch (error) {
@@ -615,9 +565,9 @@ function writeWaveformCache(cachePath, peaks) {
615
565
  if (!isWaveformCacheDirectory(cacheDir)) {
616
566
  throw new Error("Waveform cache must be a directory, not a symlink");
617
567
  }
618
- const stagingDir = mkdtempSync(join4(cacheDir, ".waveform-"));
568
+ const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
619
569
  try {
620
- const stagingPath = join4(stagingDir, "peaks.json");
570
+ const stagingPath = join3(stagingDir, "peaks.json");
621
571
  writeFileSync2(stagingPath, JSON.stringify(peaks), { flag: "wx" });
622
572
  renameSync(stagingPath, cachePath);
623
573
  } finally {
@@ -632,7 +582,7 @@ function writeWaveformCache(cachePath, peaks) {
632
582
  import { spawnSync } from "child_process";
633
583
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
634
584
  import { tmpdir } from "os";
635
- import { basename as basename2, join as join5 } from "path";
585
+ import { basename, join as join4 } from "path";
636
586
  var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
637
587
  var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
638
588
  function validateUploadedMedia(filePath, runner = spawnSync) {
@@ -669,8 +619,8 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
669
619
  }
670
620
  }
671
621
  function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
672
- const tempDir = mkdtempSync2(join5(tmpdir(), "hyperframes-upload-"));
673
- const tempPath = join5(tempDir, basename2(fileName));
622
+ const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
623
+ const tempPath = join4(tempDir, basename(fileName));
674
624
  try {
675
625
  writeFileSync3(tempPath, buffer);
676
626
  return validateUploadedMedia(tempPath, runner);
@@ -680,9 +630,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
680
630
  }
681
631
 
682
632
  // src/helpers/backupJournal.ts
683
- import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
633
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
684
634
  import { Buffer as Buffer2 } from "buffer";
685
- import { join as join6, relative as relative3 } from "path";
635
+ import { join as join5, relative as relative2 } from "path";
686
636
  var DEFAULT_KEEP_PER_FILE = 10;
687
637
  function backupKeyForPath(path) {
688
638
  return Buffer2.from(path, "utf-8").toString("base64url");
@@ -692,7 +642,7 @@ function timestampPrefix() {
692
642
  }
693
643
  function backupPathForResponse(projectDir, backupPath) {
694
644
  if (!backupPath) return null;
695
- const rel = relative3(projectDir, backupPath);
645
+ const rel = relative2(projectDir, backupPath);
696
646
  if (!rel || rel.startsWith("..")) return null;
697
647
  return rel.split("\\").join("/");
698
648
  }
@@ -700,8 +650,8 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
700
650
  if (!isSafePath(projectDir, absPath)) return { backupPath: null };
701
651
  try {
702
652
  const content = readFileSync3(absPath);
703
- const relativePath = relative3(projectDir, absPath);
704
- const backupDir = join6(projectDir, ".hyperframes", "backup");
653
+ const relativePath = relative2(projectDir, absPath);
654
+ const backupDir = join5(projectDir, ".hyperframes", "backup");
705
655
  mkdirSync3(backupDir, { recursive: true });
706
656
  const backupKey = backupKeyForPath(relativePath);
707
657
  const backupPath = nextBackupPath(backupDir, backupKey);
@@ -717,7 +667,7 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
717
667
  }
718
668
  function nextBackupPath(backupDir, backupKey) {
719
669
  const base = `${timestampPrefix()}-${backupKey}`;
720
- let candidate = join6(backupDir, base);
670
+ let candidate = join5(backupDir, base);
721
671
  let counter = 2;
722
672
  while (true) {
723
673
  try {
@@ -728,7 +678,7 @@ function nextBackupPath(backupDir, backupKey) {
728
678
  }
729
679
  throw error;
730
680
  }
731
- candidate = join6(backupDir, `${base}-${counter}`);
681
+ candidate = join5(backupDir, `${base}-${counter}`);
732
682
  counter += 1;
733
683
  }
734
684
  }
@@ -736,7 +686,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
736
686
  const keep = Math.max(1, Math.floor(keepPerFile));
737
687
  const suffix = `-${backupKey}`;
738
688
  const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
739
- const matches = readdirSync3(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join6(backupDir, name)).sort((a, b) => {
689
+ const matches = readdirSync2(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
740
690
  return b.localeCompare(a);
741
691
  });
742
692
  for (const file of matches.slice(keep)) {
@@ -937,9 +887,9 @@ import { parseHTML as parseHTML2 } from "linkedom";
937
887
  import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
938
888
 
939
889
  // src/helpers/compositionInsertion.ts
940
- import { existsSync as existsSync2, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "fs";
890
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
941
891
  import { randomUUID as randomUUID3 } from "crypto";
942
- import { dirname as dirname4, relative as relative4, resolve as resolve2, sep as sep3 } from "path";
892
+ import { dirname as dirname3, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
943
893
  import { parseHTML } from "linkedom";
944
894
  var CompositionInsertionError = class extends Error {
945
895
  constructor(message, status) {
@@ -975,8 +925,8 @@ function canonicalProjectPath(projectDir, candidate) {
975
925
  if (!existsSync2(candidate)) {
976
926
  throw new CompositionInsertionError("Composition source was not found", 404);
977
927
  }
978
- const canonical = realpathSync2(candidate);
979
- if (!isSafePath(realpathSync2(projectDir), canonical)) {
928
+ const canonical = realpath(candidate);
929
+ if (!isSafePath(realpath(projectDir), canonical)) {
980
930
  throw new CompositionInsertionError("Composition source escapes the project", 400);
981
931
  }
982
932
  return canonical;
@@ -988,13 +938,13 @@ function validateSourcePath(sourcePath) {
988
938
  }
989
939
  function canonicalProjectFile(projectDir, sourcePath) {
990
940
  validateSourcePath(sourcePath);
991
- return canonicalProjectPath(projectDir, resolveWithinProject2(projectDir, sourcePath));
941
+ return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
992
942
  }
993
943
  function canonicalDependency(projectDir, ownerAbs, sourcePath) {
994
944
  validateSourcePath(sourcePath);
995
945
  return canonicalProjectPath(
996
946
  projectDir,
997
- resolveWithinProject2(projectDir, relative4(projectDir, resolve2(dirname4(ownerAbs), sourcePath)))
947
+ resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname3(ownerAbs), sourcePath)))
998
948
  );
999
949
  }
1000
950
  function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
@@ -1067,7 +1017,7 @@ function uniqueHostId(root, base) {
1067
1017
  return `${base}_${suffix}`;
1068
1018
  }
1069
1019
  function relativeSourcePath(targetAbs, sourceAbs) {
1070
- return relative4(dirname4(targetAbs), sourceAbs).split(sep3).join("/");
1020
+ return relative3(dirname3(targetAbs), sourceAbs).split(sep2).join("/");
1071
1021
  }
1072
1022
  function insertCompositionIntoSource(input) {
1073
1023
  const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
@@ -1199,7 +1149,7 @@ async function loadGsapParser() {
1199
1149
  return import("@hyperframes/parsers/gsap-parser-recast");
1200
1150
  }
1201
1151
  function isDanglingSymlinkInProject(projectDir, lexicalPath) {
1202
- if (!isSafePath(projectDir, dirname5(lexicalPath))) return false;
1152
+ if (!isSafePath(projectDir, dirname4(lexicalPath))) return false;
1203
1153
  let stats;
1204
1154
  try {
1205
1155
  stats = lstatSync3(lexicalPath);
@@ -1207,7 +1157,7 @@ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
1207
1157
  return false;
1208
1158
  }
1209
1159
  if (!stats.isSymbolicLink()) return false;
1210
- const target = resolve3(dirname5(lexicalPath), readlinkSync(lexicalPath));
1160
+ const target = resolve3(dirname4(lexicalPath), readlinkSync(lexicalPath));
1211
1161
  if (!isSafePath(projectDir, target)) return false;
1212
1162
  try {
1213
1163
  statSync3(lexicalPath);
@@ -1231,7 +1181,7 @@ async function resolveProjectPath(c, adapter, route, opts) {
1231
1181
  if (filePath.includes("\0")) {
1232
1182
  return { error: c.json({ error: "forbidden", why: "nul" }, 403) };
1233
1183
  }
1234
- const absPath = (opts?.pin ? pinWithinProject : resolveWithinProject2)(project.dir, filePath);
1184
+ const absPath = (opts?.pin ? pinWithinProject : resolveWithinProject)(project.dir, filePath);
1235
1185
  if (!absPath) {
1236
1186
  if (isDanglingSymlinkInProject(project.dir, resolve3(project.dir, filePath))) {
1237
1187
  return { error: c.json({ error: "not found", why: "dangling_symlink" }, 404) };
@@ -1430,7 +1380,7 @@ async function parseMutationBody(c) {
1430
1380
  return { target: body.target, body };
1431
1381
  }
1432
1382
  function ensureDir(filePath) {
1433
- const dir = dirname5(filePath);
1383
+ const dir = dirname4(filePath);
1434
1384
  if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
1435
1385
  }
1436
1386
  function generateCopyPath(projectDir, originalPath) {
@@ -1448,8 +1398,8 @@ function generateCopyPath(projectDir, originalPath) {
1448
1398
  }
1449
1399
  function walkFiles(dir, filter) {
1450
1400
  const results = [];
1451
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1452
- const full = join7(dir, entry.name);
1401
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1402
+ const full = join6(dir, entry.name);
1453
1403
  if (entry.isDirectory()) {
1454
1404
  if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
1455
1405
  continue;
@@ -2612,7 +2562,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
2612
2562
  if (n >= MAX_COPY_INDEX) skipped.push(name);
2613
2563
  continue;
2614
2564
  }
2615
- const relativePath = subDir ? join7(subDir, finalName) : finalName;
2565
+ const relativePath = subDir ? join6(subDir, finalName) : finalName;
2616
2566
  uploaded.push(relativePath);
2617
2567
  if (isAudioFile(finalName)) {
2618
2568
  generateWaveformCache(projectDir, relativePath).catch(() => {
@@ -3295,7 +3245,7 @@ function registerFileRoutes(api, adapter) {
3295
3245
  if (!body.newPath || body.newPath.includes("\0")) {
3296
3246
  return c.json({ error: "newPath required" }, 400);
3297
3247
  }
3298
- const newAbs = resolveWithinProject2(res.project.dir, body.newPath);
3248
+ const newAbs = resolveWithinProject(res.project.dir, body.newPath);
3299
3249
  if (!newAbs) {
3300
3250
  return c.json({ error: "forbidden" }, 403);
3301
3251
  }
@@ -3314,12 +3264,12 @@ function registerFileRoutes(api, adapter) {
3314
3264
  if (!body.path || body.path.includes("\0")) {
3315
3265
  return c.json({ error: "path required" }, 400);
3316
3266
  }
3317
- const srcAbs = resolveWithinProject2(project.dir, body.path);
3267
+ const srcAbs = resolveWithinProject(project.dir, body.path);
3318
3268
  if (!srcAbs || !existsSync3(srcAbs)) {
3319
3269
  return c.json({ error: "not found" }, 404);
3320
3270
  }
3321
3271
  const copyPath = generateCopyPath(project.dir, body.path);
3322
- const destAbs = resolveWithinProject2(project.dir, copyPath);
3272
+ const destAbs = resolveWithinProject(project.dir, copyPath);
3323
3273
  if (!destAbs) {
3324
3274
  return c.json({ error: "forbidden" }, 403);
3325
3275
  }
@@ -3345,7 +3295,7 @@ function registerFileRoutes(api, adapter) {
3345
3295
  const project = await adapter.resolveProject(c.req.param("id"));
3346
3296
  if (!project) return c.json({ error: "not found" }, 404);
3347
3297
  const subDir = c.req.query("dir") ?? "";
3348
- const targetDir = subDir ? resolveWithinProject2(project.dir, subDir) : project.dir;
3298
+ const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
3349
3299
  if (!targetDir) return c.json({ error: "forbidden" }, 403);
3350
3300
  if (subDir && !existsSync3(targetDir)) mkdirSync4(targetDir, { recursive: true });
3351
3301
  const formData = await c.req.formData();
@@ -3429,7 +3379,7 @@ function registerFileRoutes(api, adapter) {
3429
3379
  // src/routes/preview.ts
3430
3380
  import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
3431
3381
  import { Readable } from "stream";
3432
- import { join as join10, resolve as resolve5 } from "path";
3382
+ import { join as join9, resolve as resolve5 } from "path";
3433
3383
  import { createHash as createHash4 } from "crypto";
3434
3384
  import {
3435
3385
  addScenePartsManifest,
@@ -3441,7 +3391,7 @@ import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3441
3391
 
3442
3392
  // src/helpers/subComposition.ts
3443
3393
  import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
3444
- import { join as join8 } from "path";
3394
+ import { join as join7 } from "path";
3445
3395
  import { parseHTML as parseHTML3 } from "linkedom";
3446
3396
  import {
3447
3397
  rewriteAssetPaths,
@@ -3451,7 +3401,7 @@ import {
3451
3401
  import { stripEmbeddedRuntimeScripts } from "@hyperframes/core/compiler";
3452
3402
  import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
3453
3403
  function rewriteRelativePaths(root, compPath, projectDir) {
3454
- const assetExists = (path) => existsSync4(join8(projectDir, path));
3404
+ const assetExists = (path) => existsSync4(join7(projectDir, path));
3455
3405
  rewriteAssetPaths(
3456
3406
  root.querySelectorAll("[src], [href]"),
3457
3407
  compPath,
@@ -3548,7 +3498,7 @@ function tagRootCompositionFile(bodyHtml, compPath) {
3548
3498
  return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
3549
3499
  }
3550
3500
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
3551
- const compFile = join8(projectDir, compPath);
3501
+ const compFile = join7(projectDir, compPath);
3552
3502
  if (!existsSync4(compFile)) return null;
3553
3503
  const rawComp = rawOverride ?? readFileSync7(compFile, "utf-8");
3554
3504
  let compHeadContent = "";
@@ -3580,7 +3530,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
3580
3530
  }
3581
3531
  rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
3582
3532
  rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
3583
- const indexPath = join8(projectDir, "index.html");
3533
+ const indexPath = join7(projectDir, "index.html");
3584
3534
  let headContent = "";
3585
3535
  if (existsSync4(indexPath)) {
3586
3536
  const indexHtml = readFileSync7(indexPath, "utf-8");
@@ -3624,14 +3574,14 @@ import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
3624
3574
 
3625
3575
  // src/helpers/previewReads.ts
3626
3576
  import { readlinkSync as readlinkSync2 } from "fs";
3627
- import { dirname as dirname6, join as join9, parse, relative as relative5, resolve as resolve4 } from "path";
3577
+ import { dirname as dirname5, join as join8, parse, relative as relative4, resolve as resolve4 } from "path";
3628
3578
  var ALWAYS_AFFECTS = ["hyperframes.json", ...STUDIO_SIGNATURE_MANIFEST_PATHS];
3629
3579
  var REFERENCE = /\b(?:src|href|poster|data-composition-src)\s*=\s*(?:"([^"\n]*)"|'([^'\n]*)')|url\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^"'\s)]+))/gi;
3630
3580
  var pathKey = process.platform === "darwin" || process.platform === "win32" ? (path) => resolve4(path).toLowerCase() : (path) => resolve4(path);
3631
3581
  var readsByProject = /* @__PURE__ */ new Map();
3632
3582
  var builtProjects = /* @__PURE__ */ new Set();
3633
3583
  var projectKey = (projectDir) => pathKey(realFilePath(resolve4(projectDir)));
3634
- var inRealProject = (projectDir, path) => join9(realFilePath(resolve4(projectDir)), relative5(resolve4(projectDir), resolve4(projectDir, path)));
3584
+ var inRealProject = (projectDir, path) => join8(realFilePath(resolve4(projectDir)), relative4(resolve4(projectDir), resolve4(projectDir, path)));
3635
3585
  var readLink = (path) => {
3636
3586
  try {
3637
3587
  return readlinkSync2(path);
@@ -3647,10 +3597,10 @@ function linksOnTheWay(path) {
3647
3597
  const part = rest.shift();
3648
3598
  if (!part || part === ".") continue;
3649
3599
  if (part === "..") {
3650
- at = dirname6(at);
3600
+ at = dirname5(at);
3651
3601
  continue;
3652
3602
  }
3653
- const next = join9(at, part);
3603
+ const next = join8(at, part);
3654
3604
  const target = hops < 40 ? readLink(next) : null;
3655
3605
  if (target === null) {
3656
3606
  at = next;
@@ -3671,7 +3621,7 @@ function recordPreviewRead(projectDir, filePath) {
3671
3621
  if (!reads) readsByProject.set(key, reads = /* @__PURE__ */ new Set());
3672
3622
  const read = inRealProject(projectDir, filePath);
3673
3623
  for (const found of [read, ...linksOnTheWay(read)]) {
3674
- for (let path = pathKey(found); !reads.has(path); path = dirname6(path)) reads.add(path);
3624
+ for (let path = pathKey(found); !reads.has(path); path = dirname5(path)) reads.add(path);
3675
3625
  }
3676
3626
  }
3677
3627
  function recordPreviewReferences(projectDir, html) {
@@ -3740,7 +3690,7 @@ function injectProjectSignature(html, signature) {
3740
3690
  ${html}`;
3741
3691
  }
3742
3692
  function readStudioMotionManifestContent(projectDir) {
3743
- const manifestPath = join10(projectDir, STUDIO_MOTION_PATH);
3693
+ const manifestPath = join9(projectDir, STUDIO_MOTION_PATH);
3744
3694
  if (!existsSync5(manifestPath)) return "";
3745
3695
  try {
3746
3696
  return readFileSync8(manifestPath, "utf-8");
@@ -3885,8 +3835,8 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
3885
3835
  }
3886
3836
  }
3887
3837
  function resolveProjectMainHtml(projectDir, projectId) {
3888
- const indexPath = join10(projectDir, "index.html");
3889
- const blockHtmlPath = join10(projectDir, `${projectId}.html`);
3838
+ const indexPath = join9(projectDir, "index.html");
3839
+ const blockHtmlPath = join9(projectDir, `${projectId}.html`);
3890
3840
  recordPreviewRead(projectDir, indexPath);
3891
3841
  recordPreviewRead(projectDir, blockHtmlPath);
3892
3842
  if (existsSync5(indexPath)) {
@@ -4040,7 +3990,7 @@ ${etag}`;
4040
3990
  if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
4041
3991
  const previewVariables = vars.values;
4042
3992
  const compPath = requestSubPath(c.req.url, "projects/:id/preview/comp");
4043
- const compFile = resolveWithinProject2(project.dir, compPath);
3993
+ const compFile = resolveWithinProject(project.dir, compPath);
4044
3994
  if (compFile) recordPreviewRead(project.dir, compFile);
4045
3995
  recordPreviewRead(project.dir, "index.html");
4046
3996
  if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
@@ -4175,7 +4125,7 @@ ${etag}`;
4175
4125
 
4176
4126
  // src/routes/lint.ts
4177
4127
  import { readFileSync as readFileSync9 } from "fs";
4178
- import { join as join11 } from "path";
4128
+ import { join as join10 } from "path";
4179
4129
  async function collectProjectFindings(adapter, projectDir) {
4180
4130
  const findings = [];
4181
4131
  const coveredFiles = /* @__PURE__ */ new Set();
@@ -4193,7 +4143,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
4193
4143
  const findings = [];
4194
4144
  for (const file of htmlFiles) {
4195
4145
  if (coveredFiles.has(file)) continue;
4196
- const content = readFileSync9(join11(projectDir, file), "utf-8");
4146
+ const content = readFileSync9(join10(projectDir, file), "utf-8");
4197
4147
  const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
4198
4148
  for (const finding of result?.findings ?? []) {
4199
4149
  findings.push({ ...finding, file });
@@ -4226,8 +4176,8 @@ function registerLintRoutes(api, adapter) {
4226
4176
 
4227
4177
  // src/routes/render.ts
4228
4178
  import { streamSSE } from "hono/streaming";
4229
- import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
4230
- import { join as join12 } from "path";
4179
+ import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4180
+ import { join as join11 } from "path";
4231
4181
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
4232
4182
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
4233
4183
  var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
@@ -4278,7 +4228,7 @@ function registerRenderRoutes(api, adapter) {
4278
4228
  const outputResolution = VALID_RESOLUTIONS.has(body.resolution ?? "") ? body.resolution : void 0;
4279
4229
  let composition;
4280
4230
  if (typeof body.composition === "string" && body.composition.length > 0) {
4281
- if (!resolveWithinProject2(project.dir, body.composition)) {
4231
+ if (!resolveWithinProject(project.dir, body.composition)) {
4282
4232
  return c.json({ error: "composition path must be within the project directory" }, 400);
4283
4233
  }
4284
4234
  composition = body.composition;
@@ -4295,7 +4245,7 @@ function registerRenderRoutes(api, adapter) {
4295
4245
  const rendersDir = adapter.rendersDir(project);
4296
4246
  if (!existsSync6(rendersDir)) mkdirSync5(rendersDir, { recursive: true });
4297
4247
  const ext = FORMAT_EXT[format] ?? ".mp4";
4298
- const outputPath = join12(rendersDir, `${jobId}${ext}`);
4248
+ const outputPath = join11(rendersDir, `${jobId}${ext}`);
4299
4249
  const jobState = adapter.startRender({
4300
4250
  project,
4301
4251
  outputPath,
@@ -4396,7 +4346,7 @@ function registerRenderRoutes(api, adapter) {
4396
4346
  if (state.id === jobId && state.outputPath) {
4397
4347
  const dir = state.outputPath.replace(/\/[^/]+$/, "");
4398
4348
  for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
4399
- const fp = join12(dir, `${jobId}${ext}`);
4349
+ const fp = join11(dir, `${jobId}${ext}`);
4400
4350
  if (existsSync6(fp)) unlinkSync3(fp);
4401
4351
  }
4402
4352
  break;
@@ -4411,7 +4361,7 @@ function registerRenderRoutes(api, adapter) {
4411
4361
  const filename = c.req.path.split("/renders/file/")[1];
4412
4362
  if (!filename) return c.json({ error: "missing filename" }, 400);
4413
4363
  const rendersDir = adapter.rendersDir(project);
4414
- const fp = resolveWithinProject2(rendersDir, filename);
4364
+ const fp = resolveWithinProject(rendersDir, filename);
4415
4365
  if (!fp) return c.json({ error: "forbidden" }, 403);
4416
4366
  if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
4417
4367
  const contentType = renderContentType(fp);
@@ -4430,11 +4380,11 @@ function registerRenderRoutes(api, adapter) {
4430
4380
  if (!project) return c.json({ error: "not found" }, 404);
4431
4381
  const rendersDir = adapter.rendersDir(project);
4432
4382
  if (!existsSync6(rendersDir)) return c.json({ renders: [] });
4433
- const files = readdirSync5(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
4434
- const fp = join12(rendersDir, f);
4383
+ const files = readdirSync4(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
4384
+ const fp = join11(rendersDir, f);
4435
4385
  const stat3 = statSync5(fp);
4436
4386
  const rid = f.replace(/\.(mp4|webm|mov)$/, "");
4437
- const metaPath = join12(rendersDir, `${rid}.meta.json`);
4387
+ const metaPath = join11(rendersDir, `${rid}.meta.json`);
4438
4388
  let status = "complete";
4439
4389
  let durationMs;
4440
4390
  if (existsSync6(metaPath)) {
@@ -4460,7 +4410,7 @@ function registerRenderRoutes(api, adapter) {
4460
4410
  id: file.id,
4461
4411
  status: file.status,
4462
4412
  progress: 100,
4463
- outputPath: join12(rendersDir, file.filename),
4413
+ outputPath: join11(rendersDir, file.filename),
4464
4414
  createdAt: file.createdAt
4465
4415
  });
4466
4416
  }
@@ -4608,14 +4558,14 @@ async function generateThumbnail(path, signal) {
4608
4558
  async function resolveSource(adapter, id, url) {
4609
4559
  const project = await adapter.resolveProject(id);
4610
4560
  if (!project) throw new HTTPException(404);
4611
- let relative7;
4561
+ let relative6;
4612
4562
  try {
4613
- relative7 = requestSubPath(url, "projects/:id/image-thumbnail");
4563
+ relative6 = requestSubPath(url, "projects/:id/image-thumbnail");
4614
4564
  } catch {
4615
4565
  throw new HTTPException(400);
4616
4566
  }
4617
- if (!/\.jpe?g$/i.test(relative7)) throw new HTTPException(415);
4618
- const path = pinWithinProject(project.dir, relative7);
4567
+ if (!/\.jpe?g$/i.test(relative6)) throw new HTTPException(415);
4568
+ const path = pinWithinProject(project.dir, relative6);
4619
4569
  if (!path) throw new HTTPException(404);
4620
4570
  const info = await stat(path).catch(() => {
4621
4571
  throw new HTTPException(404);
@@ -4676,14 +4626,14 @@ import {
4676
4626
  openSync as openSync3,
4677
4627
  mkdirSync as mkdirSync6,
4678
4628
  readFileSync as readFileSync12,
4679
- readdirSync as readdirSync6,
4629
+ readdirSync as readdirSync5,
4680
4630
  renameSync as renameSync3,
4681
4631
  rmSync as rmSync4,
4682
4632
  statSync as statSync6,
4683
4633
  unlinkSync as unlinkSync4,
4684
4634
  writeFileSync as writeFileSync6
4685
4635
  } from "fs";
4686
- import { join as join13 } from "path";
4636
+ import { join as join12 } from "path";
4687
4637
  import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
4688
4638
 
4689
4639
  // src/helpers/compositionInputs.ts
@@ -4694,7 +4644,7 @@ function projectReader(projectDir) {
4694
4644
  const sources = /* @__PURE__ */ new Map();
4695
4645
  return (compPath) => {
4696
4646
  if (!sources.has(compPath)) {
4697
- const file = resolveWithinProject2(projectDir, compPath);
4647
+ const file = resolveWithinProject(projectDir, compPath);
4698
4648
  let source = null;
4699
4649
  try {
4700
4650
  source = file ? readFileSync11(file, "utf-8") : null;
@@ -4753,14 +4703,14 @@ var prunedCacheDirs = /* @__PURE__ */ new Set();
4753
4703
  function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
4754
4704
  let entries;
4755
4705
  try {
4756
- entries = readdirSync6(cacheDir, { withFileTypes: true });
4706
+ entries = readdirSync5(cacheDir, { withFileTypes: true });
4757
4707
  } catch (err) {
4758
4708
  if (err.code === "ENOENT") return;
4759
4709
  throw err;
4760
4710
  }
4761
4711
  const files = entries.flatMap((entry) => {
4762
4712
  if (!entry.isFile()) return [];
4763
- const path = join13(cacheDir, entry.name);
4713
+ const path = join12(cacheDir, entry.name);
4764
4714
  try {
4765
4715
  const stats = statSync6(path);
4766
4716
  return [{ path, bytes: stats.size, mtimeMs: stats.mtimeMs }];
@@ -4830,7 +4780,7 @@ function registerThumbnailRoutes(api, adapter) {
4830
4780
  const { project, signature: projectSignature } = resolved;
4831
4781
  let compPath = requestSubPath(c.req.url, "projects/:id/thumbnail");
4832
4782
  if (compPath && !compPath.includes(".")) compPath += ".html";
4833
- const htmlFile = resolveWithinProject2(project.dir, compPath);
4783
+ const htmlFile = resolveWithinProject(project.dir, compPath);
4834
4784
  const source = htmlFile ? readFileOnce(htmlFile) : "not-a-file";
4835
4785
  if (source === "not-a-file") return c.json({ error: "not found" }, 404);
4836
4786
  const inputSignature = compositionInputSignature(project.dir, compPath, projectSignature);
@@ -4863,12 +4813,12 @@ function registerThumbnailRoutes(api, adapter) {
4863
4813
  if (hMatch?.[1]) compH = parseInt(hMatch[1]);
4864
4814
  }
4865
4815
  }
4866
- const manualEdits = manifestKey(join13(project.dir, STUDIO_MANUAL_EDITS_PATH));
4867
- const motion = manifestKey(join13(project.dir, STUDIO_MOTION_PATH));
4816
+ const manualEdits = manifestKey(join12(project.dir, STUDIO_MANUAL_EDITS_PATH));
4817
+ const motion = manifestKey(join12(project.dir, STUDIO_MOTION_PATH));
4868
4818
  sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
4869
4819
  const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
4870
4820
  const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4871
- const cacheDir = join13(project.dir, ".thumbnails");
4821
+ const cacheDir = join12(project.dir, ".thumbnails");
4872
4822
  const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4873
4823
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
4874
4824
  const inputSignatureKey = `_${createHash6("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
@@ -4876,7 +4826,7 @@ function registerThumbnailRoutes(api, adapter) {
4876
4826
  const outputWidth = Math.max(1, Math.round(compW * outputScale));
4877
4827
  const outputHeight = Math.max(1, Math.round(compH * outputScale));
4878
4828
  const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${manualEdits.key}${motion.key}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
4879
- const cachePath = join13(cacheDir, cacheKey);
4829
+ const cachePath = join12(cacheDir, cacheKey);
4880
4830
  if (!prunedCacheDirs.has(cacheDir)) {
4881
4831
  prunedCacheDirs.add(cacheDir);
4882
4832
  pruneThumbnailCache(
@@ -4943,7 +4893,7 @@ function registerThumbnailRoutes(api, adapter) {
4943
4893
 
4944
4894
  // src/routes/waveform.ts
4945
4895
  import { existsSync as existsSync7, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
4946
- import { join as join14, resolve as resolve6 } from "path";
4896
+ import { join as join13, resolve as resolve6 } from "path";
4947
4897
  import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
4948
4898
  function registerWaveformRoutes(api, adapter) {
4949
4899
  api.get("/projects/:id/waveform/*", async (c) => {
@@ -4956,8 +4906,8 @@ function registerWaveformRoutes(api, adapter) {
4956
4906
  }
4957
4907
  const stats = statSync7(audioPath, { throwIfNoEntry: false });
4958
4908
  if (!stats) return c.json({ error: "file not found" }, 404);
4959
- const cacheDir = join14(project.dir, ".waveform-cache");
4960
- const cachePath = join14(cacheDir, buildWaveformCacheKey(assetPath, stats));
4909
+ const cacheDir = join13(project.dir, ".waveform-cache");
4910
+ const cachePath = join13(cacheDir, buildWaveformCacheKey(assetPath, stats));
4961
4911
  try {
4962
4912
  if (isWaveformCacheDirectory(cacheDir) && existsSync7(cachePath)) {
4963
4913
  const peaks2 = JSON.parse(readFileSync13(cachePath, "utf-8"));
@@ -5244,7 +5194,7 @@ function registerSelectionRoutes(api, adapter) {
5244
5194
  // src/routes/media.ts
5245
5195
  import { streamSSE as streamSSE2 } from "hono/streaming";
5246
5196
  import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
5247
- import { basename as basename3, dirname as dirname7, extname as extname2, join as join15 } from "path";
5197
+ import { basename as basename2, dirname as dirname6, extname as extname2, join as join14 } from "path";
5248
5198
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5249
5199
  ".mp4",
5250
5200
  ".mov",
@@ -5274,14 +5224,14 @@ function containsNullByte(path) {
5274
5224
  return path.includes("\0");
5275
5225
  }
5276
5226
  function slugFileBase(path) {
5277
- const name = basename3(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
5227
+ const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
5278
5228
  return name || "media";
5279
5229
  }
5280
5230
  function uniqueAssetPath(projectDir, assetPath) {
5281
5231
  const ext = extname2(assetPath);
5282
5232
  const withoutExt = assetPath.slice(0, -ext.length);
5283
5233
  let candidate = assetPath;
5284
- for (let index = 2; existsSync8(join15(projectDir, candidate)); index++) {
5234
+ for (let index = 2; existsSync8(join14(projectDir, candidate)); index++) {
5285
5235
  candidate = `${withoutExt}-${index}${ext}`;
5286
5236
  }
5287
5237
  return candidate;
@@ -5330,7 +5280,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
5330
5280
  if (/^(?:https?:|data:|blob:)/i.test(assetPath)) {
5331
5281
  return c.json({ error: "media metadata requires a project-local asset" }, 400);
5332
5282
  }
5333
- const filePath = resolveWithinProject2(project.dir, assetPath);
5283
+ const filePath = resolveWithinProject(project.dir, assetPath);
5334
5284
  if (!filePath) return c.json({ error: "forbidden" }, 403);
5335
5285
  if (!existsSync8(filePath)) return c.json({ error: "media not found" }, 404);
5336
5286
  return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
@@ -5352,7 +5302,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
5352
5302
  if (/^(?:https?:|data:|blob:)/i.test(inputAssetPath)) {
5353
5303
  return c.json({ error: "background removal requires a project-local media asset" }, 400);
5354
5304
  }
5355
- const inputPath = resolveWithinProject2(project.dir, inputAssetPath);
5305
+ const inputPath = resolveWithinProject(project.dir, inputAssetPath);
5356
5306
  if (!inputPath) return c.json({ error: "forbidden" }, 403);
5357
5307
  if (!existsSync8(inputPath)) return c.json({ error: "input media not found" }, 404);
5358
5308
  const inputIsVideo = isVideoPath(inputAssetPath);
@@ -5364,11 +5314,11 @@ function registerMediaRoutes(api, adapter, options = {}) {
5364
5314
  if (requestedOutput && containsNullByte(requestedOutput)) {
5365
5315
  return c.json({ error: "forbidden" }, 403);
5366
5316
  }
5367
- if (requestedOutput && !resolveWithinProject2(project.dir, requestedOutput)) {
5317
+ if (requestedOutput && !resolveWithinProject(project.dir, requestedOutput)) {
5368
5318
  return c.json({ error: "forbidden" }, 403);
5369
5319
  }
5370
5320
  const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
5371
- const outputPath = resolveWithinProject2(project.dir, outputAssetPath);
5321
+ const outputPath = resolveWithinProject(project.dir, outputAssetPath);
5372
5322
  if (!outputPath) return c.json({ error: "forbidden" }, 403);
5373
5323
  if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
5374
5324
  return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
@@ -5383,13 +5333,13 @@ function registerMediaRoutes(api, adapter, options = {}) {
5383
5333
  return c.json({ error: "background plates are only supported for video inputs" }, 400);
5384
5334
  }
5385
5335
  backgroundOutputAssetPath = defaultPlatePath(project.dir, inputAssetPath);
5386
- backgroundOutputPath = resolveWithinProject2(project.dir, backgroundOutputAssetPath) ?? void 0;
5336
+ backgroundOutputPath = resolveWithinProject(project.dir, backgroundOutputAssetPath) ?? void 0;
5387
5337
  if (!backgroundOutputPath) {
5388
5338
  return c.json({ error: "forbidden" }, 403);
5389
5339
  }
5390
5340
  }
5391
- mkdirSync7(dirname7(outputPath), { recursive: true });
5392
- if (backgroundOutputPath) mkdirSync7(dirname7(backgroundOutputPath), { recursive: true });
5341
+ mkdirSync7(dirname6(outputPath), { recursive: true });
5342
+ if (backgroundOutputPath) mkdirSync7(dirname6(backgroundOutputPath), { recursive: true });
5393
5343
  const jobId = makeJobId(project.id, mediaJobs);
5394
5344
  const state = adapter.startBackgroundRemoval({
5395
5345
  project,
@@ -5448,9 +5398,9 @@ function registerMediaRoutes(api, adapter, options = {}) {
5448
5398
  // src/routes/globalAssets.ts
5449
5399
  import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
5450
5400
  import { homedir } from "os";
5451
- import { join as join16 } from "path";
5401
+ import { join as join15 } from "path";
5452
5402
  function readGlobalAssets(home = homedir()) {
5453
- const manifestPath = join16(home, ".media", "manifest.jsonl");
5403
+ const manifestPath = join15(home, ".media", "manifest.jsonl");
5454
5404
  if (!existsSync9(manifestPath)) return [];
5455
5405
  const out = [];
5456
5406
  for (const line of readFileSync14(manifestPath, "utf8").split("\n")) {
@@ -5478,19 +5428,19 @@ import {
5478
5428
  lstatSync as lstatSync4,
5479
5429
  statSync as statSync9,
5480
5430
  mkdirSync as mkdirSync11,
5481
- readdirSync as readdirSync7,
5431
+ readdirSync as readdirSync6,
5482
5432
  readFileSync as readFileSync18,
5483
- realpathSync as realpathSync3,
5433
+ realpathSync,
5484
5434
  rmSync as rmSync6
5485
5435
  } from "fs";
5486
5436
  import { homedir as homedir2 } from "os";
5487
- import { dirname as dirname10, isAbsolute as isAbsolute3, join as join20, relative as relative6, resolve as resolve8, sep as sep4 } from "path";
5437
+ import { dirname as dirname9, isAbsolute as isAbsolute2, join as join19, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
5488
5438
 
5489
5439
  // src/history/blobStore.ts
5490
5440
  import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
5491
5441
  import { constants as constants3, createReadStream as createReadStream2, renameSync as renameSync4 } from "fs";
5492
5442
  import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat as stat2 } from "fs/promises";
5493
- import { dirname as dirname8, join as join17 } from "path";
5443
+ import { dirname as dirname7, join as join16 } from "path";
5494
5444
  var BLOB_HASH = /^[0-9a-f]{64}$/;
5495
5445
  async function hashFile(path) {
5496
5446
  const hash = createHash7("sha256");
@@ -5498,7 +5448,7 @@ async function hashFile(path) {
5498
5448
  return hash.digest("hex");
5499
5449
  }
5500
5450
  async function cloneOrCopy(from, to, beforeReplace) {
5501
- await mkdir(dirname8(to), { recursive: true });
5451
+ await mkdir(dirname7(to), { recursive: true });
5502
5452
  const temp = `${to}.${randomUUID5()}.tmp`;
5503
5453
  try {
5504
5454
  await copyFile(from, temp, constants3.COPYFILE_FICLONE);
@@ -5513,16 +5463,16 @@ async function openBlobStore(dir) {
5513
5463
  const sizes = /* @__PURE__ */ new Map();
5514
5464
  await mkdir(dir, { recursive: true });
5515
5465
  for (const shard of await readdir(dir))
5516
- for (const hash of await readdir(join17(dir, shard)).catch(() => []))
5517
- sizes.set(hash, (await stat2(join17(dir, shard, hash))).size);
5466
+ for (const hash of await readdir(join16(dir, shard)).catch(() => []))
5467
+ sizes.set(hash, (await stat2(join16(dir, shard, hash))).size);
5518
5468
  const pathOf = (hash) => {
5519
5469
  if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
5520
- return join17(dir, hash.slice(0, 2), hash);
5470
+ return join16(dir, hash.slice(0, 2), hash);
5521
5471
  };
5522
5472
  let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
5523
5473
  return {
5524
5474
  async put(absPath) {
5525
- const temp = join17(dir, `incoming-${randomUUID5()}`);
5475
+ const temp = join16(dir, `incoming-${randomUUID5()}`);
5526
5476
  await mkdir(dir, { recursive: true });
5527
5477
  await copyFile(absPath, temp, constants3.COPYFILE_FICLONE);
5528
5478
  const hash = await hashFile(temp);
@@ -5530,7 +5480,7 @@ async function openBlobStore(dir) {
5530
5480
  await rm(temp, { force: true });
5531
5481
  return hash;
5532
5482
  }
5533
- await mkdir(dirname8(pathOf(hash)), { recursive: true });
5483
+ await mkdir(dirname7(pathOf(hash)), { recursive: true });
5534
5484
  const size = (await stat2(temp)).size;
5535
5485
  await rename(temp, pathOf(hash));
5536
5486
  sizes.set(hash, size);
@@ -5556,12 +5506,12 @@ async function openBlobStore(dir) {
5556
5506
  // src/history/historyId.ts
5557
5507
  import { randomUUID as randomUUID6 } from "crypto";
5558
5508
  import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
5559
- import { join as join18, resolve as resolve7 } from "path";
5560
- var ID_PATH = join18(".hyperframes", "history-id");
5509
+ import { basename as basename3, join as join17, resolve as resolve7 } from "path";
5510
+ var ID_PATH = join17(".hyperframes", "history-id");
5561
5511
  var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
5562
5512
  function readId(projectDir) {
5563
5513
  try {
5564
- const id = readFileSync15(join18(projectDir, ID_PATH), "utf-8").trim();
5514
+ const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
5565
5515
  return ID_SHAPE.test(id) ? id : null;
5566
5516
  } catch {
5567
5517
  return null;
@@ -5569,36 +5519,62 @@ function readId(projectDir) {
5569
5519
  }
5570
5520
  var sameFolder = (a, b) => a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
5571
5521
  function isRecordedFolder(historyDir, folder) {
5522
+ const was = readRecord(historyDir);
5523
+ if (typeof was?.ino === "number")
5524
+ return sameFolder({ ino: was.ino, birthtimeMs: was.born ?? NaN }, folder);
5525
+ const unreadableSo0878KeptTheId = typeof was?.dir !== "string";
5526
+ if (unreadableSo0878KeptTheId) return true;
5527
+ return !isCopyOf0878Folder(was.dir, basename3(historyDir), folder);
5528
+ }
5529
+ function readRecord(historyDir) {
5572
5530
  try {
5573
- const was = JSON.parse(readFileSync15(join18(historyDir, "project.json"), "utf-8"));
5574
- return sameFolder({ ino: was.ino, birthtimeMs: was.born }, folder);
5531
+ return JSON.parse(readFileSync15(join17(historyDir, "project.json"), "utf-8"));
5532
+ } catch {
5533
+ return null;
5534
+ }
5535
+ }
5536
+ function isCopyOf0878Folder(recordedDir, id, folder) {
5537
+ let there;
5538
+ try {
5539
+ there = statSync8(recordedDir, { throwIfNoEntry: false });
5575
5540
  } catch {
5576
5541
  return false;
5577
5542
  }
5543
+ return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
5578
5544
  }
5579
5545
  function projectHistoryId(projectDir, historyRoot) {
5580
5546
  const dir = resolve7(projectDir);
5581
5547
  const folder = statSync8(dir);
5582
5548
  let id = readId(dir);
5583
- if (!id || existsSync10(join18(historyRoot, id)) && !isRecordedFolder(join18(historyRoot, id), folder)) {
5549
+ if (!id && existsSync10(join17(dir, ID_PATH)))
5550
+ throw new HistoryIdError(
5551
+ `${join17(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
5552
+ );
5553
+ if (!id || existsSync10(join17(historyRoot, id)) && !isRecordedFolder(join17(historyRoot, id), folder)) {
5584
5554
  id = randomUUID6();
5585
- mkdirSync8(join18(dir, ".hyperframes"), { recursive: true });
5586
- writeFileSync7(join18(dir, ID_PATH), `${id}
5555
+ mkdirSync8(join17(dir, ".hyperframes"), { recursive: true });
5556
+ writeFileSync7(join17(dir, ID_PATH), `${id}
5587
5557
  `);
5588
5558
  }
5589
- recordProject(join18(historyRoot, id), dir, folder);
5559
+ recordProject(join17(historyRoot, id), dir, folder);
5590
5560
  return id;
5591
5561
  }
5562
+ var HistoryIdError = class extends Error {
5563
+ constructor(message) {
5564
+ super(message);
5565
+ this.name = "HistoryIdError";
5566
+ }
5567
+ };
5592
5568
  function recordProject(historyDir, dir, folder) {
5593
5569
  const record = { dir, ino: folder.ino, born: folder.birthtimeMs };
5594
5570
  mkdirSync8(historyDir, { recursive: true });
5595
- replaceFileAtomically(join18(historyDir, "project.json"), JSON.stringify(record), 420);
5571
+ replaceFileAtomically(join17(historyDir, "project.json"), JSON.stringify(record), 420);
5596
5572
  }
5597
5573
 
5598
5574
  // src/history/ownerLock.ts
5599
5575
  import { randomUUID as randomUUID7 } from "crypto";
5600
5576
  import { linkSync, mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
5601
- import { join as join19 } from "path";
5577
+ import { join as join18 } from "path";
5602
5578
  var HistoryBusyError = class extends Error {
5603
5579
  constructor(pid) {
5604
5580
  super(`This project's history is open in another process (pid ${pid}).`);
@@ -5657,7 +5633,7 @@ function evictDeadOwner(file) {
5657
5633
  }
5658
5634
  }
5659
5635
  async function takeHistoryOwnership(home, waitMs) {
5660
- const file = join19(home, "owner.pid");
5636
+ const file = join18(home, "owner.pid");
5661
5637
  const deadline = Date.now() + waitMs;
5662
5638
  mkdirSync9(home, { recursive: true });
5663
5639
  for (; ; ) {
@@ -5678,7 +5654,7 @@ async function takeHistoryOwnership(home, waitMs) {
5678
5654
 
5679
5655
  // src/history/historyLog.ts
5680
5656
  import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync17, writeSync as writeSync2 } from "fs";
5681
- import { dirname as dirname9 } from "path";
5657
+ import { dirname as dirname8 } from "path";
5682
5658
  var START = "start";
5683
5659
  function parseRecord(line) {
5684
5660
  try {
@@ -5730,7 +5706,7 @@ function writeLog(file, log) {
5730
5706
  ...log.entries.map((entry) => ({ type: "entry", entry })),
5731
5707
  ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
5732
5708
  ];
5733
- mkdirSync10(dirname9(file), { recursive: true });
5709
+ mkdirSync10(dirname8(file), { recursive: true });
5734
5710
  replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
5735
5711
  `).join(""), 420);
5736
5712
  }
@@ -5803,7 +5779,7 @@ function referencedHashes(log, current) {
5803
5779
  }
5804
5780
 
5805
5781
  // src/history/projectHistory.ts
5806
- var DEFAULT_HISTORY_ROOT = join20(homedir2(), ".cache", "hyperframes", "history");
5782
+ var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
5807
5783
  var MAX_WINDOW_IDLE_MS = 10 * 6e4;
5808
5784
  var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
5809
5785
  var OUTSIDE = { kind: "outside", name: "Outside" };
@@ -5829,20 +5805,20 @@ async function removeEmptyFolders(dir) {
5829
5805
  throw error;
5830
5806
  }
5831
5807
  for (const entry of entries)
5832
- if (entry.isDirectory()) await removeEmptyFolders(join20(dir, entry.name));
5808
+ if (entry.isDirectory()) await removeEmptyFolders(join19(dir, entry.name));
5833
5809
  await rmdir(dir);
5834
5810
  }
5835
5811
  function fileLeftIn(dir, at, deleted) {
5836
- for (const entry of readdirSync7(dir, { withFileTypes: true })) {
5812
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
5837
5813
  const path = `${at}/${entry.name}`;
5838
- const left = entry.isDirectory() ? fileLeftIn(join20(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
5814
+ const left = entry.isDirectory() ? fileLeftIn(join19(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
5839
5815
  if (left) return left;
5840
5816
  }
5841
5817
  return void 0;
5842
5818
  }
5843
5819
  function ignoresCase(dir) {
5844
- const self = statSync9(join20(dir, ID_PATH), { throwIfNoEntry: false });
5845
- const other = statSync9(join20(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
5820
+ const self = statSync9(join19(dir, ID_PATH), { throwIfNoEntry: false });
5821
+ const other = statSync9(join19(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
5846
5822
  return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
5847
5823
  }
5848
5824
  var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
@@ -5859,7 +5835,7 @@ var Engine = class {
5859
5835
  this.projectId = projectId;
5860
5836
  this.blobs = blobs;
5861
5837
  this.dir = resolve8(options.projectDir);
5862
- this.home = join20(options.historyRoot, projectId);
5838
+ this.home = join19(options.historyRoot, projectId);
5863
5839
  this.foldsCase = ignoresCase(this.dir);
5864
5840
  const folder = statSync9(this.dir, { throwIfNoEntry: false });
5865
5841
  if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
@@ -5892,7 +5868,7 @@ var Engine = class {
5892
5868
  return this.foldsCase ? path.toLowerCase() : path;
5893
5869
  }
5894
5870
  get logFile() {
5895
- return join20(this.home, "log.jsonl");
5871
+ return join19(this.home, "log.jsonl");
5896
5872
  }
5897
5873
  now() {
5898
5874
  return (this.options.now ?? Date.now)();
@@ -5920,7 +5896,7 @@ var Engine = class {
5920
5896
  persistLog(record) {
5921
5897
  if (record) saveRecord(this.logFile, this.log, record);
5922
5898
  else writeLog(this.logFile, this.log);
5923
- if (!existsSync11(join20(this.home, "project.json")))
5899
+ if (!existsSync11(join19(this.home, "project.json")))
5924
5900
  recordProject(this.home, this.dir, this.folder);
5925
5901
  }
5926
5902
  replaced() {
@@ -5970,7 +5946,7 @@ var Engine = class {
5970
5946
  }
5971
5947
  readStatCache() {
5972
5948
  try {
5973
- const files = JSON.parse(readFileSync18(join20(this.home, "stat.json"), "utf-8"));
5949
+ const files = JSON.parse(readFileSync18(join19(this.home, "stat.json"), "utf-8"));
5974
5950
  return new Map(Object.entries(files));
5975
5951
  } catch {
5976
5952
  return /* @__PURE__ */ new Map();
@@ -5979,7 +5955,7 @@ var Engine = class {
5979
5955
  saveStatCache() {
5980
5956
  const files = JSON.stringify(Object.fromEntries(this.tracked));
5981
5957
  mkdirSync11(this.home, { recursive: true });
5982
- replaceFileAtomically(join20(this.home, "stat.json"), files, 420);
5958
+ replaceFileAtomically(join19(this.home, "stat.json"), files, 420);
5983
5959
  }
5984
5960
  /**
5985
5961
  * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
@@ -6019,7 +5995,7 @@ var Engine = class {
6019
5995
  /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
6020
5996
  async storeIfPresent(path) {
6021
5997
  try {
6022
- return await this.blobs.put(join20(this.dir, path));
5998
+ return await this.blobs.put(join19(this.dir, path));
6023
5999
  } catch (error) {
6024
6000
  if (error.code === "ENOENT") return null;
6025
6001
  throw error;
@@ -6082,7 +6058,7 @@ var Engine = class {
6082
6058
  return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
6083
6059
  }
6084
6060
  logPath(path) {
6085
- return relative6(this.dir, resolve8(this.dir, path)).split(sep4).join("/");
6061
+ return relative5(this.dir, resolve8(this.dir, path)).split(sep3).join("/");
6086
6062
  }
6087
6063
  /** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
6088
6064
  async takeClaimed(who, paths, overwrote) {
@@ -6106,7 +6082,7 @@ var Engine = class {
6106
6082
  }
6107
6083
  hits.forEach(
6108
6084
  ({ change }, i) => forgetOverwrittenBytes(
6109
- join20(this.dir, change.path),
6085
+ join19(this.dir, change.path),
6110
6086
  new Set([...walked[i]].map(hashVersion))
6111
6087
  )
6112
6088
  );
@@ -6114,7 +6090,7 @@ var Engine = class {
6114
6090
  }
6115
6091
  /** Walks the server's writes back from `change.after` to the bytes they replaced, stored so an undo restores them. */
6116
6092
  async overwrittenBy(change, told, seen) {
6117
- const absPath = join20(this.dir, change.path);
6093
+ const absPath = join19(this.dir, change.path);
6118
6094
  let bytes;
6119
6095
  for (let hash = change.after; hash; ) {
6120
6096
  if (seen.has(hash)) return void 0;
@@ -6126,7 +6102,7 @@ var Engine = class {
6126
6102
  if (hash === change.before || hash === told) return hash;
6127
6103
  }
6128
6104
  if (bytes === void 0) return void 0;
6129
- const staged = join20(this.home, `overwrote-${randomUUID8()}`);
6105
+ const staged = join19(this.home, `overwrote-${randomUUID8()}`);
6130
6106
  await writeFile(staged, bytes);
6131
6107
  try {
6132
6108
  return await this.blobs.put(staged);
@@ -6148,6 +6124,7 @@ var Engine = class {
6148
6124
  part.changes.set(path, window.changes.get(path));
6149
6125
  window.changes.delete(path);
6150
6126
  }
6127
+ (window.parts ??= /* @__PURE__ */ new Set()).add(part.id);
6151
6128
  window.entry = await this.commit(part);
6152
6129
  }
6153
6130
  holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
@@ -6297,7 +6274,7 @@ var Engine = class {
6297
6274
  const parts = path.split("/");
6298
6275
  for (let depth = 1; depth <= parts.length; depth += 1) {
6299
6276
  const at = parts.slice(0, depth).join("/");
6300
- const stat3 = lstatSync4(join20(this.dir, at), { throwIfNoEntry: false });
6277
+ const stat3 = lstatSync4(join19(this.dir, at), { throwIfNoEntry: false });
6301
6278
  if (!stat3) return void 0;
6302
6279
  if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
6303
6280
  }
@@ -6309,8 +6286,8 @@ var Engine = class {
6309
6286
  if (!standing) return void 0;
6310
6287
  if (standing.stat.isSymbolicLink()) return standing.at;
6311
6288
  if (standing.at === path && !standing.stat.isDirectory()) return void 0;
6312
- const real = realpathSync3.native(join20(this.dir, standing.at));
6313
- const at = relative6(realpathSync3.native(this.dir), real).split(sep4).join("/");
6289
+ const real = realpathSync.native(join19(this.dir, standing.at));
6290
+ const at = relative5(realpathSync.native(this.dir), real).split(sep3).join("/");
6314
6291
  if (standing.at !== path) return deleted.has(at) ? void 0 : at;
6315
6292
  return fileLeftIn(real, at, deleted);
6316
6293
  }
@@ -6334,7 +6311,7 @@ var Engine = class {
6334
6311
  /** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
6335
6312
  async writeProjectFile(path, hash) {
6336
6313
  this.assertWritable();
6337
- const absPath = join20(this.dir, path);
6314
+ const absPath = join19(this.dir, path);
6338
6315
  const { writeToken } = this;
6339
6316
  if (writeToken) {
6340
6317
  const version = hash === null ? DELETED_VERSION : hashVersion(hash);
@@ -6387,7 +6364,13 @@ var Engine = class {
6387
6364
  (group) => group?.changes.size && mine(group.who)
6388
6365
  );
6389
6366
  if (pending) return direction === "back" ? this.pendingEntry(pending) : void 0;
6390
- return stepTarget(this.log.entries, direction, (entry) => mine(entry.who));
6367
+ const everyone = this.options.undoScope === "everyone";
6368
+ const ofOpenTurn = (entry) => this.windows.some((open2) => open2.parts?.has(entry.id));
6369
+ return stepTarget(
6370
+ this.log.entries,
6371
+ direction,
6372
+ (entry) => mine(entry.who) || everyone && !ofOpenTurn(entry)
6373
+ );
6391
6374
  }
6392
6375
  /** A pending group as the entry it becomes once committed (a window's part gets a fresh id). */
6393
6376
  pendingEntry(group) {
@@ -6466,10 +6449,10 @@ var Engine = class {
6466
6449
  throw new Error(`Checkout writes into an empty folder only: ${dest}`);
6467
6450
  const files = manifestAround(this.log, entryId, side);
6468
6451
  try {
6469
- for (const [path, hash] of files) await this.blobs.writeTo(hash, join20(dest, path));
6452
+ for (const [path, hash] of files) await this.blobs.writeTo(hash, join19(dest, path));
6470
6453
  } catch (error) {
6471
6454
  for (const name of await readdir2(dest).catch(() => []))
6472
- await rm2(join20(dest, name), { recursive: true, force: true });
6455
+ await rm2(join19(dest, name), { recursive: true, force: true });
6473
6456
  throw error;
6474
6457
  }
6475
6458
  }),
@@ -6505,9 +6488,9 @@ var Engine = class {
6505
6488
  };
6506
6489
  function isWithin(dir, path) {
6507
6490
  let probe = resolve8(path);
6508
- while (!existsSync11(probe) && dirname10(probe) !== probe) probe = dirname10(probe);
6509
- const rel = relative6(realpathSync3.native(dir), realpathSync3.native(probe));
6510
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute3(rel);
6491
+ while (!existsSync11(probe) && dirname9(probe) !== probe) probe = dirname9(probe);
6492
+ const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
6493
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
6511
6494
  }
6512
6495
  function missingIsEmpty(error) {
6513
6496
  if (error.code === "ENOENT") return [];
@@ -6524,11 +6507,11 @@ async function openProjectHistory(options) {
6524
6507
  throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
6525
6508
  const projectId = projectHistoryId(options.projectDir, options.historyRoot);
6526
6509
  const release = await takeHistoryOwnership(
6527
- join20(options.historyRoot, projectId),
6510
+ join19(options.historyRoot, projectId),
6528
6511
  options.ownerWaitMs ?? 5e3
6529
6512
  );
6530
6513
  try {
6531
- const blobs = await openBlobStore(join20(options.historyRoot, projectId, "blobs"));
6514
+ const blobs = await openBlobStore(join19(options.historyRoot, projectId, "blobs"));
6532
6515
  const engine = new Engine(options, projectId, blobs);
6533
6516
  await engine.queue(() => engine.start());
6534
6517
  const api = engine.api();
@@ -6777,10 +6760,10 @@ function numericAttribute(element, name) {
6777
6760
 
6778
6761
  // src/helpers/previewDocumentStore.ts
6779
6762
  import { createHash as createHash8 } from "crypto";
6780
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
6781
- import { join as join21 } from "path";
6763
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync7 } from "fs";
6764
+ import { join as join20 } from "path";
6782
6765
  function createPreviewDocumentStore(dir, salt) {
6783
- const fileFor = (key) => join21(dir, `${createHash8("sha256").update(`${salt}
6766
+ const fileFor = (key) => join20(dir, `${createHash8("sha256").update(`${salt}
6784
6767
  ${key}`).digest("hex").slice(0, 32)}.html`);
6785
6768
  return {
6786
6769
  read(key) {
@@ -6795,8 +6778,8 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
6795
6778
  mkdirSync12(dir, { recursive: true });
6796
6779
  const file = fileFor(key);
6797
6780
  replaceFileAtomically(file, html, 420);
6798
- for (const name of readdirSync8(dir)) {
6799
- if (join21(dir, name) !== file) rmSync7(join21(dir, name), { force: true });
6781
+ for (const name of readdirSync7(dir)) {
6782
+ if (join20(dir, name) !== file) rmSync7(join20(dir, name), { force: true });
6800
6783
  }
6801
6784
  } catch {
6802
6785
  }
@@ -6877,6 +6860,7 @@ export {
6877
6860
  START as HISTORY_START,
6878
6861
  HistoryBusyError,
6879
6862
  HistoryClosedError,
6863
+ HistoryIdError,
6880
6864
  MAX_WINDOW_IDLE_MS,
6881
6865
  MIME_TYPES,
6882
6866
  PREVIEW_BUNDLE_OPTIONS,