@hyperframes/studio-server 0.8.82 → 0.8.83

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,18 +22,21 @@ import {
22
22
  isAutoProxyEnabled,
23
23
  proxyEtagSalt,
24
24
  resolvePreviewMediaCodecProbeCache
25
- } from "./chunk-FTDTO3QP.js";
25
+ } from "./chunk-HWBORC2U.js";
26
26
  import {
27
27
  ProxyCapacityError,
28
28
  ProxyTranscodeError,
29
29
  resolveProxy
30
- } from "./chunk-G6IIFKXY.js";
30
+ } from "./chunk-EL3UQI46.js";
31
31
  import {
32
32
  PROXY_VARIANT_CONFIG,
33
33
  decideMediaProxyEligibility,
34
+ folderGone,
34
35
  isInHiddenOrVendorDir,
36
+ isProjectRootMissing,
35
37
  isProxyVariantRequest,
36
38
  isSafePath,
39
+ mkdirWithinProject,
37
40
  pinWithinProject,
38
41
  probeAssetCodec,
39
42
  probeMediaMetadata,
@@ -43,7 +46,7 @@ import {
43
46
  resolveProxyVariantRequest,
44
47
  resolveWithinProject,
45
48
  walkDir
46
- } from "./chunk-TJQ76IEA.js";
49
+ } from "./chunk-WQ2WF2YJ.js";
47
50
  import {
48
51
  STUDIO_MANUAL_EDITS_PATH,
49
52
  createStudioManualEditsRenderBodyScript,
@@ -64,15 +67,7 @@ import { join as join2 } from "path";
64
67
  // src/helpers/hfIdPersist.ts
65
68
  import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
66
69
  import { createHash } from "crypto";
67
- import {
68
- closeSync,
69
- constants,
70
- fstatSync,
71
- mkdirSync,
72
- openSync,
73
- readFileSync,
74
- writeFileSync
75
- } from "fs";
70
+ import { closeSync, constants, fstatSync, openSync, readFileSync, writeFileSync } from "fs";
76
71
  import { dirname, join } from "path";
77
72
 
78
73
  // src/helpers/atomicFile.ts
@@ -123,7 +118,7 @@ function stampProjectHfIds(projectDir) {
123
118
  next[file] = hash;
124
119
  }
125
120
  try {
126
- mkdirSync(dirname(recordPath), { recursive: true });
121
+ mkdirWithinProject(projectDir, dirname(recordPath));
127
122
  writeFileSync(recordPath, JSON.stringify(next));
128
123
  } catch {
129
124
  }
@@ -408,7 +403,6 @@ import {
408
403
  readlinkSync,
409
404
  writeFileSync as writeFileSync5,
410
405
  writeSync,
411
- mkdirSync as mkdirSync4,
412
406
  unlinkSync as unlinkSync2,
413
407
  rmSync as rmSync3,
414
408
  statSync as statSync3,
@@ -469,7 +463,7 @@ import { spawn } from "child_process";
469
463
  import {
470
464
  existsSync,
471
465
  writeFileSync as writeFileSync2,
472
- mkdirSync as mkdirSync2,
466
+ mkdirSync,
473
467
  lstatSync as lstatSync2,
474
468
  mkdtempSync,
475
469
  renameSync,
@@ -558,7 +552,7 @@ function isWaveformCacheDirectory(cacheDir) {
558
552
  function writeWaveformCache(cachePath, peaks) {
559
553
  const cacheDir = dirname2(cachePath);
560
554
  try {
561
- mkdirSync2(cacheDir, { mode: 448 });
555
+ mkdirSync(cacheDir, { mode: 448 });
562
556
  } catch (error) {
563
557
  if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
564
558
  }
@@ -585,7 +579,7 @@ import { tmpdir } from "os";
585
579
  import { basename, join as join4 } from "path";
586
580
  import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
587
581
  var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
588
- var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
582
+ var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
589
583
  var defaultRunner = spawnSync;
590
584
  var FFPROBE_MISSING = "not checked: ffprobe was not found. Install FFmpeg or set HYPERFRAMES_FFPROBE_PATH.";
591
585
  function validateUploadedMedia(filePath, runner = defaultRunner) {
@@ -644,7 +638,7 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
644
638
  }
645
639
 
646
640
  // src/helpers/backupJournal.ts
647
- import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
641
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
648
642
  import { Buffer as Buffer2 } from "buffer";
649
643
  import { join as join5, relative as relative2 } from "path";
650
644
  var DEFAULT_KEEP_PER_FILE = 10;
@@ -666,13 +660,14 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
666
660
  const content = readFileSync3(absPath);
667
661
  const relativePath = relative2(projectDir, absPath);
668
662
  const backupDir = join5(projectDir, ".hyperframes", "backup");
669
- mkdirSync3(backupDir, { recursive: true });
663
+ mkdirWithinProject(projectDir, backupDir);
670
664
  const backupKey = backupKeyForPath(relativePath);
671
665
  const backupPath = nextBackupPath(backupDir, backupKey);
672
666
  writeFileSync4(backupPath, content);
673
667
  pruneBackups(backupDir, backupKey, options.keepPerFile ?? DEFAULT_KEEP_PER_FILE);
674
668
  return { backupPath };
675
669
  } catch (error) {
670
+ if (isProjectRootMissing(error)) throw error;
676
671
  if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "EISDIR")) {
677
672
  return { backupPath: null };
678
673
  }
@@ -711,6 +706,15 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
711
706
  }
712
707
  }
713
708
 
709
+ // src/helpers/projectDirMissing.ts
710
+ var PROJECT_DIR_MISSING = { error: "not found", why: "project_dir_missing" };
711
+ var projectDirMissing = (c) => c.json(PROJECT_DIR_MISSING, 404);
712
+ function replaceWithProjectDirMissing(c, hostHeaders) {
713
+ hostHeaders.delete("content-type");
714
+ c.res = void 0;
715
+ c.res = Response.json(PROJECT_DIR_MISSING, { status: 404, headers: hostHeaders });
716
+ }
717
+
714
718
  // src/helpers/fileVersion.ts
715
719
  import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
716
720
  var RECEIPT_TTL_MS = 1e4;
@@ -905,6 +909,7 @@ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
905
909
  import { randomUUID as randomUUID3 } from "crypto";
906
910
  import { dirname as dirname3, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
907
911
  import { parseHTML } from "linkedom";
912
+ import { ProjectRootMissingError } from "@hyperframes/core";
908
913
  var CompositionInsertionError = class extends Error {
909
914
  constructor(message, status) {
910
915
  super(message);
@@ -934,6 +939,7 @@ function positiveAttribute(root, ...names) {
934
939
  }
935
940
  function canonicalProjectPath(projectDir, candidate) {
936
941
  if (!candidate) {
942
+ if (folderGone(projectDir)) throw new ProjectRootMissingError(projectDir);
937
943
  throw new CompositionInsertionError("Composition source escapes the project", 400);
938
944
  }
939
945
  if (!existsSync2(candidate)) {
@@ -1186,10 +1192,8 @@ async function resolveProjectPath(c, adapter, route, opts) {
1186
1192
  if (!project) {
1187
1193
  return { error: c.json({ error: "not found" }, 404) };
1188
1194
  }
1189
- if (!existsSync3(project.dir)) {
1190
- return {
1191
- error: c.json({ error: "not found", why: "project_dir_missing" }, 404)
1192
- };
1195
+ if (folderGone(project.dir)) {
1196
+ return { error: projectDirMissing(c) };
1193
1197
  }
1194
1198
  const filePath = requestSubPath(c.req.url, `projects/:id/${route}`);
1195
1199
  if (filePath.includes("\0")) {
@@ -1393,9 +1397,8 @@ async function parseMutationBody(c) {
1393
1397
  }
1394
1398
  return { target: body.target, body };
1395
1399
  }
1396
- function ensureDir(filePath) {
1397
- const dir = dirname4(filePath);
1398
- if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
1400
+ function ensureDir(projectDir, filePath) {
1401
+ mkdirWithinProject(projectDir, dirname4(filePath));
1399
1402
  }
1400
1403
  function generateCopyPath(projectDir, originalPath) {
1401
1404
  const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
@@ -2645,7 +2648,7 @@ function registerFileRoutes(api, adapter) {
2645
2648
  let backup = { backupPath: null };
2646
2649
  let overwrote;
2647
2650
  if (createOnly) {
2648
- ensureDir(res.absPath);
2651
+ ensureDir(res.project.dir, res.absPath);
2649
2652
  let fd;
2650
2653
  try {
2651
2654
  fd = openSync2(res.absPath, "wx");
@@ -2727,7 +2730,7 @@ function registerFileRoutes(api, adapter) {
2727
2730
  api.post("/projects/:id/files/*", async (c) => {
2728
2731
  const res = await resolveProjectFile(c, adapter);
2729
2732
  if ("error" in res) return res.error;
2730
- ensureDir(res.absPath);
2733
+ ensureDir(res.project.dir, res.absPath);
2731
2734
  const body = Buffer.from(await c.req.arrayBuffer());
2732
2735
  try {
2733
2736
  writeFileSync5(res.absPath, body, { flag: "wx" });
@@ -3268,7 +3271,7 @@ function registerFileRoutes(api, adapter) {
3268
3271
  if (existsSync3(newAbs)) {
3269
3272
  return c.json({ error: "already exists" }, 409);
3270
3273
  }
3271
- ensureDir(newAbs);
3274
+ ensureDir(res.project.dir, newAbs);
3272
3275
  renameSync2(res.absPath, newAbs);
3273
3276
  const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
3274
3277
  return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
@@ -3289,7 +3292,7 @@ function registerFileRoutes(api, adapter) {
3289
3292
  if (!destAbs) {
3290
3293
  return c.json({ error: "forbidden" }, 403);
3291
3294
  }
3292
- ensureDir(destAbs);
3295
+ ensureDir(project.dir, destAbs);
3293
3296
  try {
3294
3297
  writeFileSync5(destAbs, readFileSync6(srcAbs), { flag: "wx" });
3295
3298
  } catch (error) {
@@ -3310,12 +3313,14 @@ function registerFileRoutes(api, adapter) {
3310
3313
  async (c) => {
3311
3314
  const project = await adapter.resolveProject(c.req.param("id"));
3312
3315
  if (!project) return c.json({ error: "not found" }, 404);
3316
+ if (folderGone(project.dir)) return projectDirMissing(c);
3313
3317
  const subDir = c.req.query("dir") ?? "";
3314
3318
  const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
3315
3319
  if (!targetDir) return c.json({ error: "forbidden" }, 403);
3316
- if (subDir && !existsSync3(targetDir)) mkdirSync4(targetDir, { recursive: true });
3317
3320
  const formData = await c.req.formData();
3321
+ mkdirWithinProject(project.dir, targetDir);
3318
3322
  const result = await processUploadedFiles(formData, targetDir, project.dir);
3323
+ if (folderGone(project.dir)) return projectDirMissing(c);
3319
3324
  return c.json(
3320
3325
  {
3321
3326
  ok: true,
@@ -4134,6 +4139,7 @@ ${etag}`;
4134
4139
  if (err instanceof ProxyCapacityError) {
4135
4140
  return c.text(err.message, 503, { "Retry-After": "5" });
4136
4141
  }
4142
+ if (isProjectRootMissing(err)) throw err;
4137
4143
  const message = err instanceof ProxyTranscodeError ? err.message : "proxy transcode failed";
4138
4144
  return c.text(message, 502);
4139
4145
  }
@@ -4235,7 +4241,7 @@ function registerLintRoutes(api, adapter) {
4235
4241
 
4236
4242
  // src/routes/render.ts
4237
4243
  import { streamSSE } from "hono/streaming";
4238
- import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4244
+ import { existsSync as existsSync6, readFileSync as readFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
4239
4245
  import { join as join11 } from "path";
4240
4246
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
4241
4247
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
@@ -4288,6 +4294,7 @@ function registerRenderRoutes(api, adapter) {
4288
4294
  let composition;
4289
4295
  if (typeof body.composition === "string" && body.composition.length > 0) {
4290
4296
  if (!resolveWithinProject(project.dir, body.composition)) {
4297
+ if (folderGone(project.dir)) return projectDirMissing(c);
4291
4298
  return c.json({ error: "composition path must be within the project directory" }, 400);
4292
4299
  }
4293
4300
  composition = body.composition;
@@ -4302,7 +4309,7 @@ function registerRenderRoutes(api, adapter) {
4302
4309
  const now = /* @__PURE__ */ new Date();
4303
4310
  const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
4304
4311
  const rendersDir = adapter.rendersDir(project);
4305
- if (!existsSync6(rendersDir)) mkdirSync5(rendersDir, { recursive: true });
4312
+ mkdirWithinProject(project.dir, rendersDir);
4306
4313
  const ext = FORMAT_EXT[format] ?? ".mp4";
4307
4314
  const outputPath = join11(rendersDir, `${jobId}${ext}`);
4308
4315
  const jobState = adapter.startRender({
@@ -4683,7 +4690,6 @@ import {
4683
4690
  closeSync as closeSync3,
4684
4691
  fstatSync as fstatSync3,
4685
4692
  openSync as openSync3,
4686
- mkdirSync as mkdirSync6,
4687
4693
  readFileSync as readFileSync12,
4688
4694
  readdirSync as readdirSync5,
4689
4695
  renameSync as renameSync3,
@@ -4900,7 +4906,8 @@ function registerThumbnailRoutes(api, adapter) {
4900
4906
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
4901
4907
  });
4902
4908
  }
4903
- if (url.searchParams.get("cached") === "1") return c.body(null, 404);
4909
+ if (url.searchParams.get("cached") === "1")
4910
+ return c.body(null, 204, { "Cache-Control": "no-cache" });
4904
4911
  try {
4905
4912
  const buffer = await thumbnailGenerationCoordinator.acquire(
4906
4913
  cachePath,
@@ -4926,7 +4933,7 @@ function registerThumbnailRoutes(api, adapter) {
4926
4933
  if (afterGeneration?.project.dir !== project.dir || !inputsUnchanged(afterGeneration.signature) || !inputsUnchanged(createProjectSignature(project.dir))) {
4927
4934
  return generated;
4928
4935
  }
4929
- mkdirSync6(cacheDir, { recursive: true });
4936
+ mkdirWithinProject(project.dir, cacheDir);
4930
4937
  writeThumbnailAtomically(cachePath, generated);
4931
4938
  return generated;
4932
4939
  }
@@ -4945,6 +4952,7 @@ function registerThumbnailRoutes(api, adapter) {
4945
4952
  if (err instanceof DOMException && err.name === "AbortError") {
4946
4953
  return new Response(null, { status: 499 });
4947
4954
  }
4955
+ if (isProjectRootMissing(err)) throw err;
4948
4956
  const msg = err instanceof Error ? err.message : String(err);
4949
4957
  return c.json({ error: `Thumbnail generation failed: ${msg}` }, 500);
4950
4958
  }
@@ -5139,6 +5147,7 @@ function registerRegistryRoutes(api, adapter) {
5139
5147
  }
5140
5148
  const project = await adapter.resolveProject(c.req.param("id"));
5141
5149
  if (!project) return c.json({ error: "Project not found" }, 404);
5150
+ if (folderGone(project.dir)) return projectDirMissing(c);
5142
5151
  const body = await c.req.json().catch(() => null);
5143
5152
  if (!body?.blockName) {
5144
5153
  return c.json({ error: "blockName is required" }, 400);
@@ -5147,6 +5156,7 @@ function registerRegistryRoutes(api, adapter) {
5147
5156
  const result = await adapter.installRegistryBlock({ project, blockName: body.blockName });
5148
5157
  return c.json(result);
5149
5158
  } catch (err) {
5159
+ if (isProjectRootMissing(err)) return projectDirMissing(c);
5150
5160
  const message = err instanceof Error ? err.message : "Install failed";
5151
5161
  return c.json({ error: message }, 500);
5152
5162
  }
@@ -5253,7 +5263,7 @@ function registerSelectionRoutes(api, adapter) {
5253
5263
 
5254
5264
  // src/routes/media.ts
5255
5265
  import { streamSSE as streamSSE2 } from "hono/streaming";
5256
- import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
5266
+ import { existsSync as existsSync8 } from "fs";
5257
5267
  import { basename as basename2, dirname as dirname6, extname as extname2, join as join14, posix } from "path";
5258
5268
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5259
5269
  ".mp4",
@@ -5410,8 +5420,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
5410
5420
  return c.json({ error: "forbidden" }, 403);
5411
5421
  }
5412
5422
  }
5413
- mkdirSync7(dirname6(outputPath), { recursive: true });
5414
- if (backgroundOutputPath) mkdirSync7(dirname6(backgroundOutputPath), { recursive: true });
5423
+ mkdirWithinProject(project.dir, dirname6(outputPath));
5424
+ if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname6(backgroundOutputPath));
5415
5425
  const jobId = makeJobId(project.id, mediaJobs);
5416
5426
  const state = adapter.startBackgroundRemoval({
5417
5427
  project,
@@ -5499,7 +5509,7 @@ import {
5499
5509
  existsSync as existsSync11,
5500
5510
  lstatSync as lstatSync4,
5501
5511
  statSync as statSync10,
5502
- mkdirSync as mkdirSync11,
5512
+ mkdirSync as mkdirSync5,
5503
5513
  readdirSync as readdirSync7,
5504
5514
  readFileSync as readFileSync19,
5505
5515
  realpathSync,
@@ -5584,7 +5594,7 @@ import { dirname as dirname8, join as join19 } from "path";
5584
5594
 
5585
5595
  // src/history/historyId.ts
5586
5596
  import { randomUUID as randomUUID6 } from "crypto";
5587
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
5597
+ import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
5588
5598
  import { basename as basename3, join as join17, resolve as resolve7 } from "path";
5589
5599
  var ID_PATH = join17(".hyperframes", "history-id");
5590
5600
  var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
@@ -5632,7 +5642,7 @@ function projectHistoryId(projectDir, historyRoot) {
5632
5642
  );
5633
5643
  if (!id || existsSync10(join17(historyRoot, id)) && !isRecordedFolder(join17(historyRoot, id), folder)) {
5634
5644
  id = randomUUID6();
5635
- mkdirSync8(join17(dir, ".hyperframes"), { recursive: true });
5645
+ mkdirWithinProject(dir, join17(dir, ".hyperframes"));
5636
5646
  writeFileSync7(join17(dir, ID_PATH), `${id}
5637
5647
  `);
5638
5648
  }
@@ -5647,13 +5657,13 @@ var HistoryIdError = class extends Error {
5647
5657
  };
5648
5658
  function recordProject(historyDir, dir, folder) {
5649
5659
  const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
5650
- mkdirSync8(historyDir, { recursive: true });
5660
+ mkdirSync2(historyDir, { recursive: true });
5651
5661
  replaceFileAtomically(join17(historyDir, "project.json"), JSON.stringify(record), 420);
5652
5662
  }
5653
5663
 
5654
5664
  // src/history/ownerLock.ts
5655
5665
  import { randomUUID as randomUUID7 } from "crypto";
5656
- import { linkSync, mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
5666
+ import { linkSync, mkdirSync as mkdirSync3, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
5657
5667
  import { join as join18 } from "path";
5658
5668
  var HistoryBusyError = class extends Error {
5659
5669
  constructor(pid) {
@@ -5715,7 +5725,7 @@ function evictDeadOwner(file) {
5715
5725
  async function takeHistoryOwnership(home, waitMs) {
5716
5726
  const file = join18(home, "owner.pid");
5717
5727
  const deadline = Date.now() + waitMs;
5718
- mkdirSync9(home, { recursive: true });
5728
+ mkdirSync3(home, { recursive: true });
5719
5729
  for (; ; ) {
5720
5730
  if (claim(file)) {
5721
5731
  let held = true;
@@ -5737,6 +5747,22 @@ var DAY_MS = 24 * 60 * 60 * 1e3;
5737
5747
  var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
5738
5748
  var PRUNED_PREFIX = ".pruned-";
5739
5749
  var LAST_PRUNE_FILE = ".last-prune";
5750
+ function listProjectHistories(historyRoot) {
5751
+ return namesIn(historyRoot).filter(isHistoryId).flatMap((id) => {
5752
+ const home = join19(historyRoot, id);
5753
+ const projectDir = readRecord(home)?.dir;
5754
+ if (typeof projectDir !== "string" || readId(projectDir) !== id) return [];
5755
+ return [{ id, projectDir, lastUsedMs: lastUsed(home) }];
5756
+ });
5757
+ }
5758
+ function namesIn(historyRoot) {
5759
+ try {
5760
+ return readdirSync6(historyRoot);
5761
+ } catch (error) {
5762
+ if (error.code === "ENOENT") return [];
5763
+ throw error;
5764
+ }
5765
+ }
5740
5766
  async function pruneGoneProjectHistories(historyRoot, options = {}) {
5741
5767
  return (await pruneWithin(historyRoot, options)).pruned;
5742
5768
  }
@@ -5749,13 +5775,7 @@ async function pruneWithin(historyRoot, {
5749
5775
  measure = true
5750
5776
  }) {
5751
5777
  const started = performance.now();
5752
- let names;
5753
- try {
5754
- names = readdirSync6(historyRoot);
5755
- } catch (error) {
5756
- if (error.code === "ENOENT") return { pruned: [], finished: true };
5757
- throw error;
5758
- }
5778
+ const names = namesIn(historyRoot);
5759
5779
  const pruned = [];
5760
5780
  for (const name of names) {
5761
5781
  if (performance.now() - started >= budgetMs) return { pruned, finished: false };
@@ -5869,7 +5889,7 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
5869
5889
  }
5870
5890
 
5871
5891
  // src/history/historyLog.ts
5872
- import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync18, writeSync as writeSync2 } from "fs";
5892
+ import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync4, openSync as openSync5, readFileSync as readFileSync18, writeSync as writeSync2 } from "fs";
5873
5893
  import { dirname as dirname9 } from "path";
5874
5894
  var START = "start";
5875
5895
  function parseRecord(line) {
@@ -5922,7 +5942,7 @@ function writeLog(file, log) {
5922
5942
  ...log.entries.map((entry) => ({ type: "entry", entry })),
5923
5943
  ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
5924
5944
  ];
5925
- mkdirSync10(dirname9(file), { recursive: true });
5945
+ mkdirSync4(dirname9(file), { recursive: true });
5926
5946
  replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
5927
5947
  `).join(""), 420);
5928
5948
  }
@@ -6170,7 +6190,7 @@ var Engine = class {
6170
6190
  }
6171
6191
  saveStatCache() {
6172
6192
  const files = JSON.stringify(Object.fromEntries(this.tracked));
6173
- mkdirSync11(this.home, { recursive: true });
6193
+ mkdirSync5(this.home, { recursive: true });
6174
6194
  replaceFileAtomically(join20(this.home, "stat.json"), files, 420);
6175
6195
  }
6176
6196
  /**
@@ -6884,6 +6904,21 @@ function registerHistoryRoutes(api, adapter) {
6884
6904
  // src/createStudioApi.ts
6885
6905
  function createStudioApi(adapter) {
6886
6906
  const api = new Hono();
6907
+ api.use(async function answerProjectDirMissingAfterErrorHandlers(c, next) {
6908
+ const hostHeaders = new Headers(c.res.headers);
6909
+ await next();
6910
+ if (isProjectRootMissing(c.error)) replaceWithProjectDirMissing(c, hostHeaders);
6911
+ });
6912
+ api.use("/projects/:id/*", async function answerProjectDirMissingForVanishedFolder(c, next) {
6913
+ const hostHeaders = new Headers(c.res.headers);
6914
+ const dirBeforeRoute = await Promise.resolve().then(() => adapter.resolveProject(c.req.param("id"))).then(
6915
+ (project) => project?.dir,
6916
+ () => void 0
6917
+ );
6918
+ await next();
6919
+ if (c.res.status >= 403 && dirBeforeRoute && folderGone(dirBeforeRoute))
6920
+ replaceWithProjectDirMissing(c, hostHeaders);
6921
+ });
6887
6922
  registerProjectRoutes(api, adapter);
6888
6923
  registerFileRoutes(api, adapter);
6889
6924
  registerPreviewRoutes(api, adapter);
@@ -6980,9 +7015,10 @@ function numericAttribute(element, name) {
6980
7015
 
6981
7016
  // src/helpers/previewDocumentStore.ts
6982
7017
  import { createHash as createHash8 } from "crypto";
6983
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
7018
+ import { readFileSync as readFileSync20, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
6984
7019
  import { join as join21 } from "path";
6985
- function createPreviewDocumentStore(dir, salt) {
7020
+ function createPreviewDocumentStore(projectDir, salt) {
7021
+ const dir = join21(projectDir, ".hyperframes", "preview");
6986
7022
  const fileFor = (key) => join21(dir, `${createHash8("sha256").update(`${salt}
6987
7023
  ${key}`).digest("hex").slice(0, 32)}.html`);
6988
7024
  return {
@@ -6995,7 +7031,7 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
6995
7031
  },
6996
7032
  write(key, html) {
6997
7033
  try {
6998
- mkdirSync12(dir, { recursive: true });
7034
+ mkdirWithinProject(projectDir, dir);
6999
7035
  const file = fileFor(key);
7000
7036
  replaceFileAtomically(file, html, 420);
7001
7037
  for (const name of readdirSync8(dir)) {
@@ -7109,6 +7145,7 @@ export {
7109
7145
  historyCache,
7110
7146
  identifyFileWrite,
7111
7147
  isSafePath,
7148
+ listProjectHistories,
7112
7149
  openProjectHistory,
7113
7150
  parseSourceDocument,
7114
7151
  patchElementInHtml,