@hyperframes/studio-server 0.7.61 → 0.7.63

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
@@ -10,7 +10,7 @@ import {
10
10
  splitElementInHtml,
11
11
  unwrapElementsFromHtml,
12
12
  wrapElementsInHtml
13
- } from "./chunk-XVQX2JHE.js";
13
+ } from "./chunk-VPA335OG.js";
14
14
  import {
15
15
  getElementScreenshotClip
16
16
  } from "./chunk-W2SBTCO2.js";
@@ -19,12 +19,12 @@ import {
19
19
  isAutoProxyEnabled,
20
20
  proxyEtagSalt,
21
21
  resolvePreviewMediaCodecProbeCache
22
- } from "./chunk-ZUW4PULZ.js";
22
+ } from "./chunk-OK7FKBKI.js";
23
23
  import {
24
24
  ProxyCapacityError,
25
25
  ProxyTranscodeError,
26
26
  resolveProxy
27
- } from "./chunk-YBR7MXIO.js";
27
+ } from "./chunk-IVOJZ24X.js";
28
28
  import {
29
29
  PROXY_VARIANT_CONFIG,
30
30
  decideMediaProxyEligibility,
@@ -32,7 +32,7 @@ import {
32
32
  probeAssetCodec,
33
33
  probeMediaMetadata,
34
34
  resolveProxyVariantRequest
35
- } from "./chunk-LVXVG4V6.js";
35
+ } from "./chunk-7Q5AFHU6.js";
36
36
  import {
37
37
  STUDIO_MANUAL_EDITS_PATH,
38
38
  createStudioManualEditsRenderBodyScript,
@@ -334,10 +334,10 @@ function registerStoryboardRoutes(api, adapter) {
334
334
  import { bodyLimit } from "hono/body-limit";
335
335
  import {
336
336
  closeSync,
337
- existsSync as existsSync3,
337
+ existsSync as existsSync4,
338
338
  ftruncateSync,
339
339
  openSync,
340
- readFileSync as readFileSync4,
340
+ readFileSync as readFileSync5,
341
341
  writeFileSync as writeFileSync4,
342
342
  writeSync,
343
343
  mkdirSync as mkdirSync3,
@@ -347,7 +347,7 @@ import {
347
347
  renameSync,
348
348
  readdirSync as readdirSync4
349
349
  } from "fs";
350
- import { resolve as resolve2, dirname, join as join6 } from "path";
350
+ import { resolve as resolve3, dirname as dirname2, join as join6 } from "path";
351
351
 
352
352
  // src/helpers/mime.ts
353
353
  var MIME_TYPES = {
@@ -654,7 +654,196 @@ import {
654
654
  scalePositionsInScript,
655
655
  dedupePositionWritesInScript
656
656
  } from "@hyperframes/parsers/gsap-writer-acorn";
657
+ import { parseHTML as parseHTML2 } from "linkedom";
658
+
659
+ // src/helpers/compositionInsertion.ts
660
+ import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
661
+ import { randomUUID as randomUUID2 } from "crypto";
662
+ import { dirname, relative as relative3, resolve as resolve2, sep } from "path";
657
663
  import { parseHTML } from "linkedom";
664
+ var CompositionInsertionError = class extends Error {
665
+ constructor(message, status) {
666
+ super(message);
667
+ this.status = status;
668
+ }
669
+ status;
670
+ };
671
+ function descendants(root, selector) {
672
+ const found = Array.from(root.querySelectorAll(selector));
673
+ for (const template of root.querySelectorAll("template")) {
674
+ found.push(...descendants(template, selector));
675
+ }
676
+ return [...new Set(found)];
677
+ }
678
+ function compositionRoot(source) {
679
+ const document = parseHTML(source).document;
680
+ const root = descendants(document, "[data-composition-id]")[0];
681
+ if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
682
+ return { document, root };
683
+ }
684
+ function positiveAttribute(root, ...names) {
685
+ for (const name of names) {
686
+ const value = Number.parseFloat(root.getAttribute(name) ?? "");
687
+ if (Number.isFinite(value) && value > 0) return value;
688
+ }
689
+ throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
690
+ }
691
+ function canonicalProjectPath(projectDir, candidate) {
692
+ if (!candidate) {
693
+ throw new CompositionInsertionError("Composition source escapes the project", 400);
694
+ }
695
+ if (!existsSync3(candidate)) {
696
+ throw new CompositionInsertionError("Composition source was not found", 404);
697
+ }
698
+ const canonical = realpathSync(candidate);
699
+ if (!isSafePath(realpathSync(projectDir), canonical)) {
700
+ throw new CompositionInsertionError("Composition source escapes the project", 400);
701
+ }
702
+ return canonical;
703
+ }
704
+ function validateSourcePath(sourcePath) {
705
+ if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
706
+ throw new CompositionInsertionError("Invalid composition source path", 400);
707
+ }
708
+ }
709
+ function canonicalProjectFile(projectDir, sourcePath) {
710
+ validateSourcePath(sourcePath);
711
+ return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
712
+ }
713
+ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
714
+ validateSourcePath(sourcePath);
715
+ return canonicalProjectPath(
716
+ projectDir,
717
+ resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname(ownerAbs), sourcePath)))
718
+ );
719
+ }
720
+ function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
721
+ const visited = /* @__PURE__ */ new Set();
722
+ const visiting = /* @__PURE__ */ new Set();
723
+ const visit = (file) => {
724
+ if (file === targetAbs) {
725
+ throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
726
+ }
727
+ if (visiting.has(file)) {
728
+ throw new CompositionInsertionError("Composition dependency cycle detected", 400);
729
+ }
730
+ if (visited.has(file)) return;
731
+ visiting.add(file);
732
+ const source = readFileSync4(file, "utf-8");
733
+ const { document } = compositionRoot(source);
734
+ for (const host of descendants(document, "[data-composition-src]")) {
735
+ const dependency = host.getAttribute("data-composition-src");
736
+ if (dependency) {
737
+ visit(canonicalDependency(projectDir, file, dependency));
738
+ }
739
+ }
740
+ visiting.delete(file);
741
+ visited.add(file);
742
+ };
743
+ visit(sourceAbs);
744
+ }
745
+ function numberAttribute(element, name, fallback = 0) {
746
+ const value = Number.parseFloat(element.getAttribute(name) ?? "");
747
+ return Number.isFinite(value) ? value : fallback;
748
+ }
749
+ function rangesOverlap(start, duration, other) {
750
+ const otherStart = numberAttribute(other, "data-start");
751
+ const otherDuration = numberAttribute(other, "data-duration");
752
+ return start < otherStart + otherDuration && otherStart < start + duration;
753
+ }
754
+ function resolveTrack(root, desiredTrack, start, duration) {
755
+ const clips = descendants(root, "[data-start][data-duration]").filter(
756
+ (element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
757
+ );
758
+ const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
759
+ (a, b) => a - b
760
+ );
761
+ const isFree = (track) => !clips.some(
762
+ (clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
763
+ );
764
+ if (isFree(desiredTrack)) return desiredTrack;
765
+ const row = tracks.indexOf(desiredTrack);
766
+ for (let index = row - 1; index >= 0; index--) {
767
+ const track = tracks[index];
768
+ if (track !== void 0 && isFree(track)) return track;
769
+ }
770
+ for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
771
+ const track = tracks[index];
772
+ if (track !== void 0 && isFree(track)) return track;
773
+ }
774
+ return Math.max(desiredTrack, ...tracks, -1) + 1;
775
+ }
776
+ function uniqueHostId(root, base) {
777
+ const ids = /* @__PURE__ */ new Set([
778
+ ...descendants(root, "[id]").map((element) => element.id),
779
+ ...descendants(root, "[data-composition-id]").flatMap((element) => {
780
+ const id = element.getAttribute("data-composition-id");
781
+ return id ? [id] : [];
782
+ })
783
+ ]);
784
+ if (!ids.has(base)) return base;
785
+ let suffix = 2;
786
+ while (ids.has(`${base}_${suffix}`)) suffix += 1;
787
+ return `${base}_${suffix}`;
788
+ }
789
+ function relativeSourcePath(targetAbs, sourceAbs) {
790
+ return relative3(dirname(targetAbs), sourceAbs).split(sep).join("/");
791
+ }
792
+ function insertCompositionIntoSource(input) {
793
+ const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
794
+ const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
795
+ validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
796
+ const source = readFileSync4(sourceAbs, "utf-8");
797
+ const sourceComposition = compositionRoot(source).root;
798
+ const duration = positiveAttribute(
799
+ sourceComposition,
800
+ "data-composition-duration",
801
+ "data-duration"
802
+ );
803
+ const width = positiveAttribute(sourceComposition, "data-width");
804
+ const height = positiveAttribute(sourceComposition, "data-height");
805
+ const { document, root } = compositionRoot(input.parentSource);
806
+ const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
807
+ const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
808
+ const hostId = uniqueHostId(root, base);
809
+ const track = resolveTrack(
810
+ root,
811
+ Math.max(0, Math.round(input.desiredTrack)),
812
+ input.start,
813
+ duration
814
+ );
815
+ const zIndex = Math.max(
816
+ 0,
817
+ ...descendants(root, "[style]").map((element) => {
818
+ const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
819
+ return match?.[1] ? Number.parseInt(match[1], 10) : 0;
820
+ })
821
+ ) + 1;
822
+ const host = document.createElement("div");
823
+ host.id = hostId;
824
+ host.className = "clip";
825
+ host.setAttribute("data-hf-id", `hf-${randomUUID2()}`);
826
+ host.setAttribute("data-composition-id", hostId);
827
+ host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
828
+ host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
829
+ host.setAttribute("data-duration", String(duration));
830
+ host.setAttribute("data-playback-start", "0");
831
+ host.setAttribute("data-track-index", String(track));
832
+ host.setAttribute("data-width", String(width));
833
+ host.setAttribute("data-height", String(height));
834
+ host.setAttribute(
835
+ "style",
836
+ `position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
837
+ );
838
+ root.appendChild(host);
839
+ if (input.start + duration > parentDuration) {
840
+ const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
841
+ root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
842
+ }
843
+ return { html: document.toString(), hostId, track, duration };
844
+ }
845
+
846
+ // src/routes/files.ts
658
847
  function isAcornGsapWriterEnabled() {
659
848
  const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
660
849
  return val === "true" || val === "1";
@@ -676,7 +865,7 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
676
865
  if (!absPath) {
677
866
  return { error: c.json({ error: "forbidden" }, 403) };
678
867
  }
679
- if (opts?.mustExist && !existsSync3(absPath)) {
868
+ if (opts?.mustExist && !existsSync4(absPath)) {
680
869
  return { error: c.json({ error: "not found" }, 404) };
681
870
  }
682
871
  return { project, filePath, absPath };
@@ -687,6 +876,25 @@ function resolveProjectFile(c, adapter, opts) {
687
876
  function resolveFileMutationContext(c, adapter, operation) {
688
877
  return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
689
878
  }
879
+ function isAtomicCutTarget(value) {
880
+ if (!value || typeof value !== "object") return false;
881
+ const target = value;
882
+ return !!target.target && typeof target.target === "object" && Number.isFinite(target.splitTime) && Number.isFinite(target.elementStart) && Number.isFinite(target.elementDuration) && Number(target.elementDuration) > 0;
883
+ }
884
+ function isAtomicCutFileRequest(value) {
885
+ if (!value || typeof value !== "object") return false;
886
+ const file = value;
887
+ return typeof file.path === "string" && file.path.length > 0 && typeof file.expectedVersion === "string" && Array.isArray(file.targets) && file.targets.length > 0 && file.targets.every(isAtomicCutTarget);
888
+ }
889
+ var atomicCutTail = Promise.resolve();
890
+ function serializeAtomicCut(task) {
891
+ const next = atomicCutTail.then(task, task);
892
+ atomicCutTail = next.then(
893
+ () => void 0,
894
+ () => void 0
895
+ );
896
+ return next;
897
+ }
690
898
  function isElementPatchRequest(value) {
691
899
  if (typeof value !== "object" || value === null) return false;
692
900
  if (!("target" in value) || typeof value.target !== "object" || value.target === null) {
@@ -722,7 +930,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
722
930
  resolvedPaths.add(absPath);
723
931
  let before;
724
932
  try {
725
- before = readFileSync4(absPath, "utf-8");
933
+ before = readFileSync5(absPath, "utf-8");
726
934
  } catch {
727
935
  return { error: "not-found", sourceFile: batch.sourceFile };
728
936
  }
@@ -834,8 +1042,8 @@ async function parseMutationBody(c) {
834
1042
  return { target: body.target, body };
835
1043
  }
836
1044
  function ensureDir(filePath) {
837
- const dir = dirname(filePath);
838
- if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
1045
+ const dir = dirname2(filePath);
1046
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
839
1047
  }
840
1048
  function generateCopyPath(projectDir, originalPath) {
841
1049
  const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
@@ -844,7 +1052,7 @@ function generateCopyPath(projectDir, originalPath) {
844
1052
  const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
845
1053
  let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
846
1054
  let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
847
- while (existsSync3(resolve2(projectDir, candidate))) {
1055
+ while (existsSync4(resolve3(projectDir, candidate))) {
848
1056
  num++;
849
1057
  candidate = `${cleanBase} (copy ${num})${ext}`;
850
1058
  }
@@ -871,7 +1079,7 @@ function updateReferences(projectDir, oldPath, newPath) {
871
1079
  );
872
1080
  let updatedCount = 0;
873
1081
  for (const file of textFiles) {
874
- const content = readFileSync4(file, "utf-8");
1082
+ const content = readFileSync5(file, "utf-8");
875
1083
  if (!content.includes(oldPath)) continue;
876
1084
  const updated = content.split(oldPath).join(newPath);
877
1085
  if (updated !== content) {
@@ -882,7 +1090,7 @@ function updateReferences(projectDir, oldPath, newPath) {
882
1090
  return updatedCount;
883
1091
  }
884
1092
  function extractGsapScriptBlock(html) {
885
- const { document } = parseHTML(html);
1093
+ const { document } = parseHTML2(html);
886
1094
  const scripts = [
887
1095
  ...document.querySelectorAll("script:not([src])"),
888
1096
  ...Array.from(document.querySelectorAll("template")).flatMap(
@@ -1128,7 +1336,7 @@ function validateGsapMutationRequest(c, body) {
1128
1336
  return null;
1129
1337
  }
1130
1338
  async function prepareGsapMutationScript(c, res, firstMutation) {
1131
- const beforeHtml = readFileSync4(res.absPath, "utf-8");
1339
+ const beforeHtml = readFileSync5(res.absPath, "utf-8");
1132
1340
  let html = beforeHtml;
1133
1341
  let block = extractGsapScriptBlock(html);
1134
1342
  if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
@@ -1188,7 +1396,7 @@ async function applyGsapMutations(c, res, mutations) {
1188
1396
  const changed = block.scriptText !== initialScript;
1189
1397
  const newHtml = changed ? block.replaceScript(block.scriptText) : html;
1190
1398
  let backupPath = null;
1191
- if (readFileSync4(res.absPath, "utf-8") !== beforeHtml) {
1399
+ if (readFileSync5(res.absPath, "utf-8") !== beforeHtml) {
1192
1400
  return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
1193
1401
  }
1194
1402
  if (changed) {
@@ -1811,6 +2019,74 @@ async function executeGsapMutationRecast(body, block, respond) {
1811
2019
  return respond({ error: `unknown mutation type: ${body.type}` }, 400);
1812
2020
  }
1813
2021
  }
2022
+ async function foldAtomicCutFile(c, file, absPath, before) {
2023
+ let after = before;
2024
+ let splitCount = 0;
2025
+ const skippedSelectors = /* @__PURE__ */ new Set();
2026
+ const respond = (data, status) => status ? c.json(data, status) : c.json(data);
2027
+ const orderedTargets = file.targets.map((cut, index) => ({ cut, index })).sort((left, right) => {
2028
+ const locatorKey = (entry) => !entry.target.id && !entry.target.hfId && entry.target.selector ? entry.target.selector : null;
2029
+ const leftKey = locatorKey(left.cut);
2030
+ const rightKey = locatorKey(right.cut);
2031
+ if (leftKey && rightKey) {
2032
+ return leftKey.localeCompare(rightKey) || (right.cut.target.selectorIndex ?? 0) - (left.cut.target.selectorIndex ?? 0);
2033
+ }
2034
+ if (leftKey) return -1;
2035
+ if (rightKey) return 1;
2036
+ return left.index - right.index;
2037
+ }).map(({ cut }) => cut);
2038
+ for (const cut of orderedTargets) {
2039
+ const baseId = cut.originalId || cut.target.id || "clip";
2040
+ const split = splitElementInHtml(after, cut.target, cut.splitTime, `${baseId}-split`, {
2041
+ start: cut.elementStart,
2042
+ duration: cut.elementDuration,
2043
+ playbackStart: cut.playbackStart,
2044
+ playbackRate: cut.playbackRate,
2045
+ stampPlaybackStart: cut.isComposition
2046
+ });
2047
+ if (!split.matched || !split.newId) {
2048
+ return c.json(
2049
+ { error: `Cut target was not found or was outside its authored bounds in ${file.path}` },
2050
+ 400
2051
+ );
2052
+ }
2053
+ after = split.html;
2054
+ splitCount++;
2055
+ if (!cut.originalId) continue;
2056
+ const block = extractGsapScriptBlock(after);
2057
+ if (!block) continue;
2058
+ const result = await executeGsapMutation(
2059
+ {
2060
+ type: "split-animations",
2061
+ originalId: cut.originalId,
2062
+ newId: split.newId,
2063
+ splitTime: cut.splitTime,
2064
+ elementStart: cut.elementStart,
2065
+ elementDuration: cut.elementDuration
2066
+ },
2067
+ block,
2068
+ respond
2069
+ );
2070
+ if (result instanceof Response) return result;
2071
+ let script = typeof result === "string" ? result : result.script;
2072
+ if (typeof result !== "string") {
2073
+ for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
2074
+ }
2075
+ if (script !== block.scriptText) {
2076
+ const parser = await loadGsapParser();
2077
+ script = parser.syncPositionHoldsBeforeKeyframes(script);
2078
+ after = block.replaceScript(script);
2079
+ }
2080
+ }
2081
+ return {
2082
+ path: file.path,
2083
+ absPath,
2084
+ before,
2085
+ after,
2086
+ splitCount,
2087
+ skippedSelectors: [...skippedSelectors]
2088
+ };
2089
+ }
1814
2090
  async function processUploadedFiles(formData, targetDir, projectDir) {
1815
2091
  const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
1816
2092
  const uploaded = [];
@@ -1826,23 +2102,23 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
1826
2102
  skipped.push(name);
1827
2103
  continue;
1828
2104
  }
1829
- const destPath = resolve2(targetDir, name);
2105
+ const destPath = resolve3(targetDir, name);
1830
2106
  if (!isSafePath(projectDir, destPath)) continue;
1831
2107
  let finalPath = destPath;
1832
2108
  let finalName = name;
1833
- if (existsSync3(finalPath)) {
2109
+ if (existsSync4(finalPath)) {
1834
2110
  const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
1835
2111
  const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
1836
2112
  const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
1837
2113
  let n = 2;
1838
2114
  const MAX_COPY_INDEX = 1e4;
1839
- while (n < MAX_COPY_INDEX && existsSync3(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
2115
+ while (n < MAX_COPY_INDEX && existsSync4(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
1840
2116
  if (n >= MAX_COPY_INDEX) {
1841
2117
  skipped.push(name);
1842
2118
  continue;
1843
2119
  }
1844
2120
  finalName = `${base} (${n})${ext}`;
1845
- finalPath = resolve2(targetDir, finalName);
2121
+ finalPath = resolve3(targetDir, finalName);
1846
2122
  }
1847
2123
  const buffer = Buffer.from(await value.arrayBuffer());
1848
2124
  const validation = validateUploadedMediaBuffer(finalName, buffer);
@@ -1864,13 +2140,13 @@ function registerFileRoutes(api, adapter) {
1864
2140
  api.get("/projects/:id/files/*", async (c) => {
1865
2141
  const res = await resolveProjectFile(c, adapter);
1866
2142
  if ("error" in res) return res.error;
1867
- if (!existsSync3(res.absPath)) {
2143
+ if (!existsSync4(res.absPath)) {
1868
2144
  if (c.req.query("optional") === "1") {
1869
2145
  return c.json({ filename: res.filePath, content: "" });
1870
2146
  }
1871
2147
  return c.json({ error: "not found" }, 404);
1872
2148
  }
1873
- const content = readFileSync4(res.absPath, "utf-8");
2149
+ const content = readFileSync5(res.absPath, "utf-8");
1874
2150
  const version = fileContentVersion(content);
1875
2151
  c.header("ETag", version);
1876
2152
  return c.json({ filename: res.filePath, content, version });
@@ -1884,7 +2160,7 @@ function registerFileRoutes(api, adapter) {
1884
2160
  if (expectedVersion === null && !createOnly) {
1885
2161
  let currentContent = null;
1886
2162
  try {
1887
- currentContent = readFileSync4(res.absPath, "utf-8");
2163
+ currentContent = readFileSync5(res.absPath, "utf-8");
1888
2164
  } catch (error) {
1889
2165
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
1890
2166
  throw error;
@@ -1910,7 +2186,7 @@ function registerFileRoutes(api, adapter) {
1910
2186
  if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
1911
2187
  throw error;
1912
2188
  }
1913
- const currentContent = readFileSync4(res.absPath, "utf-8");
2189
+ const currentContent = readFileSync5(res.absPath, "utf-8");
1914
2190
  return c.json(
1915
2191
  {
1916
2192
  error: "file conflict",
@@ -1945,7 +2221,7 @@ function registerFileRoutes(api, adapter) {
1945
2221
  );
1946
2222
  }
1947
2223
  try {
1948
- const currentContent = readFileSync4(fd, "utf-8");
2224
+ const currentContent = readFileSync5(fd, "utf-8");
1949
2225
  const currentVersion = fileContentVersion(currentContent);
1950
2226
  if (expectedVersion !== currentVersion) {
1951
2227
  return c.json(
@@ -1982,7 +2258,7 @@ function registerFileRoutes(api, adapter) {
1982
2258
  api.post("/projects/:id/files/*", async (c) => {
1983
2259
  const res = await resolveProjectFile(c, adapter);
1984
2260
  if ("error" in res) return res.error;
1985
- if (existsSync3(res.absPath)) {
2261
+ if (existsSync4(res.absPath)) {
1986
2262
  return c.json({ error: "already exists" }, 409);
1987
2263
  }
1988
2264
  ensureDir(res.absPath);
@@ -2006,15 +2282,71 @@ function registerFileRoutes(api, adapter) {
2006
2282
  backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
2007
2283
  });
2008
2284
  });
2285
+ api.post("/projects/:id/file-mutations/insert-composition/*", async (c) => {
2286
+ const ctx = await resolveFileMutationContext(c, adapter, "insert-composition");
2287
+ if ("error" in ctx) return ctx.error;
2288
+ const body = await c.req.json().catch(() => null);
2289
+ if (!body || typeof body.sourcePath !== "string" || typeof body.start !== "number" || !Number.isFinite(body.start) || body.start < 0 || typeof body.track !== "number" || !Number.isFinite(body.track) || typeof body.expectedVersion !== "string") {
2290
+ return c.json({ error: "sourcePath, finite placement, and expectedVersion required" }, 400);
2291
+ }
2292
+ let before;
2293
+ try {
2294
+ before = readFileSync5(ctx.absPath, "utf-8");
2295
+ } catch (error) {
2296
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2297
+ throw error;
2298
+ }
2299
+ return c.json({ error: "not found" }, 404);
2300
+ }
2301
+ const currentVersion = fileContentVersion(before);
2302
+ if (body.expectedVersion !== currentVersion) {
2303
+ return c.json({ error: "file conflict", currentVersion, currentContent: before }, 409);
2304
+ }
2305
+ let insertion;
2306
+ try {
2307
+ insertion = insertCompositionIntoSource({
2308
+ projectDir: ctx.project.dir,
2309
+ targetPath: ctx.filePath,
2310
+ sourcePath: body.sourcePath,
2311
+ parentSource: before,
2312
+ start: body.start,
2313
+ desiredTrack: body.track
2314
+ });
2315
+ } catch (error) {
2316
+ if (error instanceof CompositionInsertionError) {
2317
+ return c.json({ error: error.message }, error.status);
2318
+ }
2319
+ throw error;
2320
+ }
2321
+ const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
2322
+ if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
2323
+ writeFileSync4(ctx.absPath, insertion.html, "utf-8");
2324
+ const version = fileContentVersion(insertion.html);
2325
+ const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
2326
+ recordFileWriteReceipt(ctx.absPath, { path: ctx.filePath, version, writeToken });
2327
+ c.header("ETag", version);
2328
+ return c.json({
2329
+ ok: true,
2330
+ path: ctx.filePath,
2331
+ hostId: insertion.hostId,
2332
+ track: insertion.track,
2333
+ duration: insertion.duration,
2334
+ before,
2335
+ after: insertion.html,
2336
+ version,
2337
+ writeToken,
2338
+ backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
2339
+ });
2340
+ });
2009
2341
  api.post("/projects/:id/file-mutations/remove-element/*", async (c) => {
2010
2342
  const ctx = await resolveFileMutationContext(c, adapter, "remove-element");
2011
2343
  if ("error" in ctx) return ctx.error;
2012
- if (!existsSync3(ctx.absPath)) {
2344
+ if (!existsSync4(ctx.absPath)) {
2013
2345
  return c.json({ error: "not found" }, 404);
2014
2346
  }
2015
2347
  const parsed = await parseMutationBody(c);
2016
2348
  if ("error" in parsed) return parsed.error;
2017
- const originalContent = readFileSync4(ctx.absPath, "utf-8");
2349
+ const originalContent = readFileSync5(ctx.absPath, "utf-8");
2018
2350
  return writeIfChanged(
2019
2351
  c,
2020
2352
  ctx.project.dir,
@@ -2024,6 +2356,130 @@ function registerFileRoutes(api, adapter) {
2024
2356
  removeElementFromHtml(originalContent, parsed.target)
2025
2357
  );
2026
2358
  });
2359
+ api.post("/projects/:id/file-mutations/split-batch", async (c) => {
2360
+ const body = await c.req.json().catch(() => null);
2361
+ if (!Array.isArray(body?.files) || body.files.length === 0 || !body.files.every(isAtomicCutFileRequest)) {
2362
+ return c.json({ error: "files with path, expectedVersion, and cut targets required" }, 400);
2363
+ }
2364
+ const files = body.files;
2365
+ const project = await adapter.resolveProject(c.req.param("id"));
2366
+ if (!project) return c.json({ error: "not found" }, 404);
2367
+ return serializeAtomicCut(async () => {
2368
+ const seen = /* @__PURE__ */ new Set();
2369
+ const prepared = [];
2370
+ for (const file of files) {
2371
+ const absPath = resolveWithinProject(project.dir, file.path);
2372
+ if (!absPath) return c.json({ error: `forbidden path: ${file.path}` }, 403);
2373
+ if (seen.has(absPath)) return c.json({ error: `duplicate path: ${file.path}` }, 400);
2374
+ seen.add(absPath);
2375
+ let before;
2376
+ try {
2377
+ before = readFileSync5(absPath, "utf-8");
2378
+ } catch {
2379
+ return c.json({ error: `not found: ${file.path}` }, 404);
2380
+ }
2381
+ const currentVersion = fileContentVersion(before);
2382
+ if (currentVersion !== file.expectedVersion) {
2383
+ return c.json(
2384
+ {
2385
+ error: `file conflict: ${file.path}`,
2386
+ path: file.path,
2387
+ currentVersion,
2388
+ currentContent: before
2389
+ },
2390
+ 409
2391
+ );
2392
+ }
2393
+ let folded;
2394
+ try {
2395
+ folded = await foldAtomicCutFile(c, file, absPath, before);
2396
+ } catch (error) {
2397
+ const message = error instanceof Error ? error.message : "Cut transform failed";
2398
+ return c.json({ error: message }, 400);
2399
+ }
2400
+ if (folded instanceof Response) return folded;
2401
+ prepared.push(folded);
2402
+ }
2403
+ for (const file of prepared) {
2404
+ const current = readFileSync5(file.absPath, "utf-8");
2405
+ if (current !== file.before) {
2406
+ return c.json(
2407
+ {
2408
+ error: `file conflict: ${file.path}`,
2409
+ path: file.path,
2410
+ currentVersion: fileContentVersion(current),
2411
+ currentContent: current
2412
+ },
2413
+ 409
2414
+ );
2415
+ }
2416
+ }
2417
+ const backups = /* @__PURE__ */ new Map();
2418
+ for (const file of prepared) {
2419
+ const backup = snapshotBeforeWrite(project.dir, file.absPath);
2420
+ if (backup.error) {
2421
+ return c.json(
2422
+ { error: `Failed to create backup for ${file.path}: ${backup.error}` },
2423
+ 500
2424
+ );
2425
+ }
2426
+ backups.set(file.path, backupPathForResponse(project.dir, backup.backupPath));
2427
+ }
2428
+ const writeToken = createWriteToken(
2429
+ typeof body.transactionToken === "string" ? body.transactionToken : c.req.header("X-Hyperframes-Write-Token")
2430
+ );
2431
+ const written = [];
2432
+ try {
2433
+ for (const file of prepared) {
2434
+ writeFileSync4(file.absPath, file.after, "utf-8");
2435
+ written.push(file);
2436
+ recordFileWriteReceipt(file.absPath, {
2437
+ path: file.path,
2438
+ version: fileContentVersion(file.after),
2439
+ writeToken
2440
+ });
2441
+ }
2442
+ } catch (error) {
2443
+ const conflicts = [];
2444
+ for (const file of written.reverse()) {
2445
+ try {
2446
+ const current = readFileSync5(file.absPath, "utf-8");
2447
+ if (current !== file.after) {
2448
+ conflicts.push(file.path);
2449
+ continue;
2450
+ }
2451
+ writeFileSync4(file.absPath, file.before, "utf-8");
2452
+ recordFileWriteReceipt(file.absPath, {
2453
+ path: file.path,
2454
+ version: fileContentVersion(file.before),
2455
+ writeToken
2456
+ });
2457
+ } catch {
2458
+ conflicts.push(file.path);
2459
+ }
2460
+ }
2461
+ return c.json(
2462
+ {
2463
+ error: error instanceof Error ? error.message : "Cut write failed",
2464
+ outcome: conflicts.length ? "aborted-with-conflicts" : "aborted-restored",
2465
+ conflicts
2466
+ },
2467
+ conflicts.length ? 409 : 500
2468
+ );
2469
+ }
2470
+ const result = prepared.map((file) => ({
2471
+ path: file.path,
2472
+ before: file.before,
2473
+ after: file.after,
2474
+ version: fileContentVersion(file.after),
2475
+ writeToken,
2476
+ backupPath: backups.get(file.path) ?? null,
2477
+ splitCount: file.splitCount,
2478
+ skippedSelectors: file.skippedSelectors
2479
+ }));
2480
+ return c.json({ ok: true, outcome: "committed", files: result });
2481
+ });
2482
+ });
2027
2483
  api.post("/projects/:id/file-mutations/split-element/*", async (c) => {
2028
2484
  const ctx = await resolveFileMutationContext(c, adapter, "split-element");
2029
2485
  if ("error" in ctx) return ctx.error;
@@ -2035,7 +2491,7 @@ function registerFileRoutes(api, adapter) {
2035
2491
  const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
2036
2492
  let originalContent;
2037
2493
  try {
2038
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2494
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2039
2495
  } catch {
2040
2496
  return c.json({ error: "not found" }, 404);
2041
2497
  }
@@ -2086,7 +2542,7 @@ function registerFileRoutes(api, adapter) {
2086
2542
  }
2087
2543
  let originalContent;
2088
2544
  try {
2089
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2545
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2090
2546
  } catch {
2091
2547
  return c.json({ error: "not found" }, 404);
2092
2548
  }
@@ -2176,7 +2632,7 @@ function registerFileRoutes(api, adapter) {
2176
2632
  }
2177
2633
  let originalContent;
2178
2634
  try {
2179
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2635
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2180
2636
  } catch {
2181
2637
  return c.json({ error: "not found" }, 404);
2182
2638
  }
@@ -2218,7 +2674,7 @@ function registerFileRoutes(api, adapter) {
2218
2674
  if ("error" in parsed) return parsed.error;
2219
2675
  let originalContent;
2220
2676
  try {
2221
- originalContent = readFileSync4(ctx.absPath, "utf-8");
2677
+ originalContent = readFileSync5(ctx.absPath, "utf-8");
2222
2678
  } catch {
2223
2679
  return c.json({ error: "not found" }, 404);
2224
2680
  }
@@ -2247,7 +2703,7 @@ function registerFileRoutes(api, adapter) {
2247
2703
  if ("error" in parsed) return parsed.error;
2248
2704
  let content;
2249
2705
  try {
2250
- content = readFileSync4(ctx.absPath, "utf-8");
2706
+ content = readFileSync5(ctx.absPath, "utf-8");
2251
2707
  } catch {
2252
2708
  return c.json({ exists: false });
2253
2709
  }
@@ -2265,7 +2721,7 @@ function registerFileRoutes(api, adapter) {
2265
2721
  if (!newAbs) {
2266
2722
  return c.json({ error: "forbidden" }, 403);
2267
2723
  }
2268
- if (existsSync3(newAbs)) {
2724
+ if (existsSync4(newAbs)) {
2269
2725
  return c.json({ error: "already exists" }, 409);
2270
2726
  }
2271
2727
  ensureDir(newAbs);
@@ -2281,7 +2737,7 @@ function registerFileRoutes(api, adapter) {
2281
2737
  return c.json({ error: "path required" }, 400);
2282
2738
  }
2283
2739
  const srcAbs = resolveWithinProject(project.dir, body.path);
2284
- if (!srcAbs || !existsSync3(srcAbs)) {
2740
+ if (!srcAbs || !existsSync4(srcAbs)) {
2285
2741
  return c.json({ error: "not found" }, 404);
2286
2742
  }
2287
2743
  const copyPath = generateCopyPath(project.dir, body.path);
@@ -2290,7 +2746,7 @@ function registerFileRoutes(api, adapter) {
2290
2746
  return c.json({ error: "forbidden" }, 403);
2291
2747
  }
2292
2748
  ensureDir(destAbs);
2293
- writeFileSync4(destAbs, readFileSync4(srcAbs));
2749
+ writeFileSync4(destAbs, readFileSync5(srcAbs));
2294
2750
  return c.json({ ok: true, path: copyPath }, 201);
2295
2751
  });
2296
2752
  const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
@@ -2306,7 +2762,7 @@ function registerFileRoutes(api, adapter) {
2306
2762
  const subDir = c.req.query("dir") ?? "";
2307
2763
  const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
2308
2764
  if (!targetDir) return c.json({ error: "forbidden" }, 403);
2309
- if (subDir && !existsSync3(targetDir)) mkdirSync3(targetDir, { recursive: true });
2765
+ if (subDir && !existsSync4(targetDir)) mkdirSync3(targetDir, { recursive: true });
2310
2766
  const formData = await c.req.formData();
2311
2767
  const result = await processUploadedFiles(formData, targetDir, project.dir);
2312
2768
  return c.json(
@@ -2320,7 +2776,7 @@ function registerFileRoutes(api, adapter) {
2320
2776
  mustExist: true
2321
2777
  });
2322
2778
  if ("error" in res) return res.error;
2323
- const html = readFileSync4(res.absPath, "utf-8");
2779
+ const html = readFileSync5(res.absPath, "utf-8");
2324
2780
  const block = extractGsapScriptBlock(html);
2325
2781
  if (!block) {
2326
2782
  return c.json({
@@ -2379,7 +2835,7 @@ function registerFileRoutes(api, adapter) {
2379
2835
  if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
2380
2836
  return c.json({ error: "expected and restore contents required" }, 400);
2381
2837
  }
2382
- const current = readFileSync4(res.absPath, "utf-8");
2838
+ const current = readFileSync5(res.absPath, "utf-8");
2383
2839
  if (current !== body.expected) {
2384
2840
  return c.json({ ok: true, restored: false, conflict: true });
2385
2841
  }
@@ -2389,15 +2845,15 @@ function registerFileRoutes(api, adapter) {
2389
2845
  }
2390
2846
 
2391
2847
  // src/routes/preview.ts
2392
- import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
2848
+ import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
2393
2849
  import { join as join8 } from "path";
2394
2850
  import { createHash as createHash3 } from "crypto";
2395
2851
  import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
2396
2852
 
2397
2853
  // src/helpers/subComposition.ts
2398
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2854
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2399
2855
  import { join as join7 } from "path";
2400
- import { parseHTML as parseHTML2 } from "linkedom";
2856
+ import { parseHTML as parseHTML3 } from "linkedom";
2401
2857
  import {
2402
2858
  rewriteAssetPaths,
2403
2859
  rewriteCssAssetUrls,
@@ -2445,7 +2901,7 @@ function fixDigitLeadingIdSelectors(root) {
2445
2901
  }
2446
2902
  }
2447
2903
  function extractFullDocumentParts(rawHtml, compPath) {
2448
- const { document: doc } = parseHTML2(rawHtml);
2904
+ const { document: doc } = parseHTML3(rawHtml);
2449
2905
  const rewriteTargets = [doc.head, doc.body].filter(Boolean);
2450
2906
  for (const target of rewriteTargets) {
2451
2907
  rewriteRelativePaths(target, compPath);
@@ -2459,7 +2915,7 @@ function extractFullDocumentParts(rawHtml, compPath) {
2459
2915
  return { headContent, bodyContent, htmlAttrs, bodyAttrs };
2460
2916
  }
2461
2917
  function extractTemplateInnerHtml(rawComp) {
2462
- const { document: doc } = parseHTML2(rawComp);
2918
+ const { document: doc } = parseHTML3(rawComp);
2463
2919
  const template = doc.querySelector("template");
2464
2920
  return template ? template.innerHTML : null;
2465
2921
  }
@@ -2503,15 +2959,15 @@ function tagRootCompositionFile(bodyHtml, compPath) {
2503
2959
  }
2504
2960
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
2505
2961
  const compFile = join7(projectDir, compPath);
2506
- if (!existsSync4(compFile)) return null;
2507
- const rawComp = rawOverride ?? readFileSync5(compFile, "utf-8");
2962
+ if (!existsSync5(compFile)) return null;
2963
+ const rawComp = rawOverride ?? readFileSync6(compFile, "utf-8");
2508
2964
  let compHeadContent = "";
2509
2965
  let rewrittenContent;
2510
2966
  let htmlAttrs = "";
2511
2967
  let bodyAttrs = "";
2512
2968
  const templateInner = extractTemplateInnerHtml(rawComp);
2513
2969
  if (templateInner != null) {
2514
- const { document: contentDoc } = parseHTML2(
2970
+ const { document: contentDoc } = parseHTML3(
2515
2971
  `<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
2516
2972
  );
2517
2973
  rewriteRelativePaths(contentDoc, compPath);
@@ -2525,7 +2981,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
2525
2981
  htmlAttrs = parts.htmlAttrs;
2526
2982
  bodyAttrs = parts.bodyAttrs;
2527
2983
  } else {
2528
- const { document: contentDoc } = parseHTML2(
2984
+ const { document: contentDoc } = parseHTML3(
2529
2985
  `<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
2530
2986
  );
2531
2987
  rewriteRelativePaths(contentDoc, compPath);
@@ -2536,8 +2992,8 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
2536
2992
  rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
2537
2993
  const indexPath = join7(projectDir, "index.html");
2538
2994
  let headContent = "";
2539
- if (existsSync4(indexPath)) {
2540
- const indexHtml = readFileSync5(indexPath, "utf-8");
2995
+ if (existsSync5(indexPath)) {
2996
+ const indexHtml = readFileSync6(indexPath, "utf-8");
2541
2997
  const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
2542
2998
  headContent = headMatch?.[1] ?? "";
2543
2999
  }
@@ -2581,7 +3037,7 @@ import {
2581
3037
  fstatSync,
2582
3038
  ftruncateSync as ftruncateSync2,
2583
3039
  openSync as openSync2,
2584
- readFileSync as readFileSync6,
3040
+ readFileSync as readFileSync7,
2585
3041
  writeFileSync as writeFileSync5,
2586
3042
  writeSync as writeSync2
2587
3043
  } from "fs";
@@ -2591,7 +3047,7 @@ function persistHfIdsIfNeeded(filePath, html) {
2591
3047
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
2592
3048
  if (idsAfter > idsBefore) {
2593
3049
  try {
2594
- const current = readFileSync6(filePath, "utf-8");
3050
+ const current = readFileSync7(filePath, "utf-8");
2595
3051
  if (current === html) {
2596
3052
  writeFileSync5(filePath, normalized, "utf-8");
2597
3053
  }
@@ -2619,7 +3075,7 @@ function stampFileHfIds(filePath) {
2619
3075
  if (fd === null) return null;
2620
3076
  try {
2621
3077
  if (!fstatSync(fd).isFile()) return null;
2622
- const html = readFileSync6(fd, "utf-8");
3078
+ const html = readFileSync7(fd, "utf-8");
2623
3079
  const normalized = ensureHfIds(html);
2624
3080
  const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
2625
3081
  const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
@@ -2663,9 +3119,9 @@ ${html}`;
2663
3119
  }
2664
3120
  function readStudioMotionManifestContent(projectDir) {
2665
3121
  const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
2666
- if (!existsSync5(manifestPath)) return "";
3122
+ if (!existsSync6(manifestPath)) return "";
2667
3123
  try {
2668
- return readFileSync7(manifestPath, "utf-8");
3124
+ return readFileSync8(manifestPath, "utf-8");
2669
3125
  } catch {
2670
3126
  return "";
2671
3127
  }
@@ -2821,16 +3277,16 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
2821
3277
  }
2822
3278
  function resolveProjectMainHtml(projectDir, projectId) {
2823
3279
  const indexPath = join8(projectDir, "index.html");
2824
- if (existsSync5(indexPath)) {
3280
+ if (existsSync6(indexPath)) {
2825
3281
  return {
2826
- html: readFileSync7(indexPath, "utf-8"),
3282
+ html: readFileSync8(indexPath, "utf-8"),
2827
3283
  compositionPath: "index.html"
2828
3284
  };
2829
3285
  }
2830
3286
  const blockHtmlPath = join8(projectDir, `${projectId}.html`);
2831
- if (existsSync5(blockHtmlPath)) {
3287
+ if (existsSync6(blockHtmlPath)) {
2832
3288
  return {
2833
- html: readFileSync7(blockHtmlPath, "utf-8"),
3289
+ html: readFileSync8(blockHtmlPath, "utf-8"),
2834
3290
  compositionPath: `${projectId}.html`
2835
3291
  };
2836
3292
  }
@@ -2935,7 +3391,7 @@ ${runtimeTag}`;
2935
3391
  c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
2936
3392
  );
2937
3393
  const compFile = resolveWithinProject(project.dir, compPath);
2938
- if (!compFile || !existsSync5(compFile) || !statSync2(compFile).isFile()) {
3394
+ if (!compFile || !existsSync6(compFile) || !statSync2(compFile).isFile()) {
2939
3395
  return c.text("not found", 404);
2940
3396
  }
2941
3397
  const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
@@ -2973,7 +3429,7 @@ ${runtimeTag}`;
2973
3429
  if (!file) {
2974
3430
  return c.text("not found", 404);
2975
3431
  }
2976
- const stat = existsSync5(file) ? statSync2(file) : null;
3432
+ const stat = existsSync6(file) ? statSync2(file) : null;
2977
3433
  if (!stat?.isFile()) {
2978
3434
  return c.text("not found", 404);
2979
3435
  }
@@ -3021,7 +3477,7 @@ ${runtimeTag}`;
3021
3477
  }
3022
3478
  servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
3023
3479
  }
3024
- const buffer = isText ? Buffer.from(readFileSync7(file, "utf-8"), "utf-8") : readFileSync7(servedPath);
3480
+ const buffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : readFileSync8(servedPath);
3025
3481
  const totalSize = buffer.length;
3026
3482
  const rangeHeader = c.req.header("Range");
3027
3483
  if (rangeHeader) {
@@ -3055,7 +3511,7 @@ ${runtimeTag}`;
3055
3511
  }
3056
3512
 
3057
3513
  // src/routes/lint.ts
3058
- import { readFileSync as readFileSync8 } from "fs";
3514
+ import { readFileSync as readFileSync9 } from "fs";
3059
3515
  import { join as join9 } from "path";
3060
3516
  function registerLintRoutes(api, adapter) {
3061
3517
  api.get("/projects/:id/lint", async (c) => {
@@ -3067,7 +3523,7 @@ function registerLintRoutes(api, adapter) {
3067
3523
  );
3068
3524
  const allFindings = [];
3069
3525
  for (const file of htmlFiles) {
3070
- const content = readFileSync8(join9(project.dir, file), "utf-8");
3526
+ const content = readFileSync9(join9(project.dir, file), "utf-8");
3071
3527
  const result = await adapter.lint(content, { filePath: file });
3072
3528
  if (result?.findings) {
3073
3529
  for (const f of result.findings) {
@@ -3085,7 +3541,7 @@ function registerLintRoutes(api, adapter) {
3085
3541
 
3086
3542
  // src/routes/render.ts
3087
3543
  import { streamSSE } from "hono/streaming";
3088
- import { existsSync as existsSync6, readFileSync as readFileSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3544
+ import { existsSync as existsSync7, readFileSync as readFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3089
3545
  import { join as join10 } from "path";
3090
3546
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
3091
3547
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
@@ -3144,7 +3600,7 @@ function registerRenderRoutes(api, adapter) {
3144
3600
  const now = /* @__PURE__ */ new Date();
3145
3601
  const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
3146
3602
  const rendersDir = adapter.rendersDir(project);
3147
- if (!existsSync6(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
3603
+ if (!existsSync7(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
3148
3604
  const ext = FORMAT_EXT[format] ?? ".mp4";
3149
3605
  const outputPath = join10(rendersDir, `${jobId}${ext}`);
3150
3606
  const jobState = adapter.startRender({
@@ -3209,12 +3665,12 @@ function registerRenderRoutes(api, adapter) {
3209
3665
  api.get("/render/:jobId/view", (c) => {
3210
3666
  const { jobId } = c.req.param();
3211
3667
  const job = renderJobs.get(jobId);
3212
- if (!job?.outputPath || !existsSync6(job.outputPath)) {
3668
+ if (!job?.outputPath || !existsSync7(job.outputPath)) {
3213
3669
  return c.json({ error: "not found" }, 404);
3214
3670
  }
3215
3671
  const contentType = renderContentType(job.outputPath);
3216
3672
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
3217
- const content = readFileSync9(job.outputPath);
3673
+ const content = readFileSync10(job.outputPath);
3218
3674
  return new Response(content, {
3219
3675
  headers: {
3220
3676
  "Content-Type": contentType,
@@ -3227,12 +3683,12 @@ function registerRenderRoutes(api, adapter) {
3227
3683
  api.get("/render/:jobId/download", (c) => {
3228
3684
  const { jobId } = c.req.param();
3229
3685
  const job = renderJobs.get(jobId);
3230
- if (!job?.outputPath || !existsSync6(job.outputPath)) {
3686
+ if (!job?.outputPath || !existsSync7(job.outputPath)) {
3231
3687
  return c.json({ error: "not found" }, 404);
3232
3688
  }
3233
3689
  const contentType = renderContentType(job.outputPath);
3234
3690
  const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
3235
- const content = readFileSync9(job.outputPath);
3691
+ const content = readFileSync10(job.outputPath);
3236
3692
  return new Response(content, {
3237
3693
  headers: {
3238
3694
  "Content-Type": contentType,
@@ -3247,7 +3703,7 @@ function registerRenderRoutes(api, adapter) {
3247
3703
  const dir = state.outputPath.replace(/\/[^/]+$/, "");
3248
3704
  for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
3249
3705
  const fp = join10(dir, `${jobId}${ext}`);
3250
- if (existsSync6(fp)) unlinkSync3(fp);
3706
+ if (existsSync7(fp)) unlinkSync3(fp);
3251
3707
  }
3252
3708
  break;
3253
3709
  }
@@ -3263,9 +3719,9 @@ function registerRenderRoutes(api, adapter) {
3263
3719
  const rendersDir = adapter.rendersDir(project);
3264
3720
  const fp = resolveWithinProject(rendersDir, filename);
3265
3721
  if (!fp) return c.json({ error: "forbidden" }, 403);
3266
- if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
3722
+ if (!existsSync7(fp)) return c.json({ error: "not found" }, 404);
3267
3723
  const contentType = renderContentType(fp);
3268
- const content = readFileSync9(fp);
3724
+ const content = readFileSync10(fp);
3269
3725
  return new Response(content, {
3270
3726
  headers: {
3271
3727
  "Content-Type": contentType,
@@ -3279,7 +3735,7 @@ function registerRenderRoutes(api, adapter) {
3279
3735
  const project = await adapter.resolveProject(c.req.param("id"));
3280
3736
  if (!project) return c.json({ error: "not found" }, 404);
3281
3737
  const rendersDir = adapter.rendersDir(project);
3282
- if (!existsSync6(rendersDir)) return c.json({ renders: [] });
3738
+ if (!existsSync7(rendersDir)) return c.json({ renders: [] });
3283
3739
  const files = readdirSync5(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
3284
3740
  const fp = join10(rendersDir, f);
3285
3741
  const stat = statSync3(fp);
@@ -3287,10 +3743,10 @@ function registerRenderRoutes(api, adapter) {
3287
3743
  const metaPath = join10(rendersDir, `${rid}.meta.json`);
3288
3744
  let status = "complete";
3289
3745
  let durationMs;
3290
- if (existsSync6(metaPath)) {
3746
+ if (existsSync7(metaPath)) {
3291
3747
  try {
3292
- const meta = JSON.parse(readFileSync9(metaPath, "utf-8"));
3293
- if (meta.status === "failed") status = "failed";
3748
+ const meta = JSON.parse(readFileSync10(metaPath, "utf-8"));
3749
+ if (meta.status === "failed" && !existsSync7(fp)) status = "failed";
3294
3750
  if (meta.durationMs) durationMs = meta.durationMs;
3295
3751
  } catch {
3296
3752
  }
@@ -3320,7 +3776,7 @@ function registerRenderRoutes(api, adapter) {
3320
3776
  }
3321
3777
 
3322
3778
  // src/routes/thumbnail.ts
3323
- import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
3779
+ import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
3324
3780
  import { join as join11 } from "path";
3325
3781
  import { createHash as createHash4 } from "crypto";
3326
3782
  var THUMBNAIL_CACHE_VERSION = "v4";
@@ -3352,8 +3808,8 @@ function registerThumbnailRoutes(api, adapter) {
3352
3808
  let sourceMtime = 0;
3353
3809
  let sourceKey = "";
3354
3810
  const htmlFile = join11(project.dir, compPath);
3355
- if (existsSync7(htmlFile)) {
3356
- const html = readFileSync10(htmlFile, "utf-8");
3811
+ if (existsSync8(htmlFile)) {
3812
+ const html = readFileSync11(htmlFile, "utf-8");
3357
3813
  sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
3358
3814
  sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
3359
3815
  if (!vpWidth) {
@@ -3365,15 +3821,15 @@ function registerThumbnailRoutes(api, adapter) {
3365
3821
  }
3366
3822
  const manualEditsFile = join11(project.dir, STUDIO_MANUAL_EDITS_PATH);
3367
3823
  let manualEditsKey = "";
3368
- if (existsSync7(manualEditsFile)) {
3369
- const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
3824
+ if (existsSync8(manualEditsFile)) {
3825
+ const manualEditsContent = readFileSync11(manualEditsFile, "utf-8");
3370
3826
  manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
3371
3827
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
3372
3828
  }
3373
3829
  const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
3374
3830
  let motionKey = "";
3375
- if (existsSync7(motionFile)) {
3376
- const motionContent = readFileSync10(motionFile, "utf-8");
3831
+ if (existsSync8(motionFile)) {
3832
+ const motionContent = readFileSync11(motionFile, "utf-8");
3377
3833
  motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
3378
3834
  sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
3379
3835
  }
@@ -3383,8 +3839,8 @@ function registerThumbnailRoutes(api, adapter) {
3383
3839
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
3384
3840
  const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
3385
3841
  const cachePath = join11(cacheDir, cacheKey);
3386
- if (existsSync7(cachePath)) {
3387
- return new Response(new Uint8Array(readFileSync10(cachePath)), {
3842
+ if (existsSync8(cachePath)) {
3843
+ return new Response(new Uint8Array(readFileSync11(cachePath)), {
3388
3844
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
3389
3845
  });
3390
3846
  }
@@ -3406,7 +3862,7 @@ function registerThumbnailRoutes(api, adapter) {
3406
3862
  500
3407
3863
  );
3408
3864
  }
3409
- if (!existsSync7(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
3865
+ if (!existsSync8(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
3410
3866
  writeFileSync6(cachePath, buffer);
3411
3867
  return new Response(new Uint8Array(buffer), {
3412
3868
  headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
@@ -3419,7 +3875,7 @@ function registerThumbnailRoutes(api, adapter) {
3419
3875
  }
3420
3876
 
3421
3877
  // src/routes/waveform.ts
3422
- import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
3878
+ import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
3423
3879
  import { join as join12 } from "path";
3424
3880
  function registerWaveformRoutes(api, adapter) {
3425
3881
  api.get("/projects/:id/waveform/*", async (c) => {
@@ -3429,12 +3885,12 @@ function registerWaveformRoutes(api, adapter) {
3429
3885
  c.req.path.replace(`/projects/${project.id}/waveform/`, "").split("?")[0] ?? ""
3430
3886
  );
3431
3887
  const audioPath = join12(project.dir, assetPath);
3432
- if (!existsSync8(audioPath)) return c.json({ error: "file not found" }, 404);
3888
+ if (!existsSync9(audioPath)) return c.json({ error: "file not found" }, 404);
3433
3889
  const cacheDir = join12(project.dir, ".waveform-cache");
3434
3890
  const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath));
3435
- if (existsSync8(cachePath)) {
3891
+ if (existsSync9(cachePath)) {
3436
3892
  try {
3437
- const peaks2 = JSON.parse(readFileSync11(cachePath, "utf-8"));
3893
+ const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
3438
3894
  return c.json({ peaks: peaks2 });
3439
3895
  } catch {
3440
3896
  }
@@ -3718,8 +4174,8 @@ function registerSelectionRoutes(api, adapter) {
3718
4174
 
3719
4175
  // src/routes/media.ts
3720
4176
  import { streamSSE as streamSSE2 } from "hono/streaming";
3721
- import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
3722
- import { basename as basename2, dirname as dirname2, extname as extname2, join as join13 } from "path";
4177
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
4178
+ import { basename as basename2, dirname as dirname3, extname as extname2, join as join13 } from "path";
3723
4179
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
3724
4180
  ".mp4",
3725
4181
  ".mov",
@@ -3756,7 +4212,7 @@ function uniqueAssetPath(projectDir, assetPath) {
3756
4212
  const ext = extname2(assetPath);
3757
4213
  const withoutExt = assetPath.slice(0, -ext.length);
3758
4214
  let candidate = assetPath;
3759
- for (let index = 2; existsSync9(join13(projectDir, candidate)); index++) {
4215
+ for (let index = 2; existsSync10(join13(projectDir, candidate)); index++) {
3760
4216
  candidate = `${withoutExt}-${index}${ext}`;
3761
4217
  }
3762
4218
  return candidate;
@@ -3807,7 +4263,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
3807
4263
  }
3808
4264
  const filePath = resolveWithinProject(project.dir, assetPath);
3809
4265
  if (!filePath) return c.json({ error: "forbidden" }, 403);
3810
- if (!existsSync9(filePath)) return c.json({ error: "media not found" }, 404);
4266
+ if (!existsSync10(filePath)) return c.json({ error: "media not found" }, 404);
3811
4267
  return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
3812
4268
  });
3813
4269
  api.post(
@@ -3829,7 +4285,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
3829
4285
  }
3830
4286
  const inputPath = resolveWithinProject(project.dir, inputAssetPath);
3831
4287
  if (!inputPath) return c.json({ error: "forbidden" }, 403);
3832
- if (!existsSync9(inputPath)) return c.json({ error: "input media not found" }, 404);
4288
+ if (!existsSync10(inputPath)) return c.json({ error: "input media not found" }, 404);
3833
4289
  const inputIsVideo = isVideoPath(inputAssetPath);
3834
4290
  const inputIsImage = isImagePath(inputAssetPath);
3835
4291
  if (!inputIsVideo && !inputIsImage) {
@@ -3863,8 +4319,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
3863
4319
  return c.json({ error: "forbidden" }, 403);
3864
4320
  }
3865
4321
  }
3866
- mkdirSync7(dirname2(outputPath), { recursive: true });
3867
- if (backgroundOutputPath) mkdirSync7(dirname2(backgroundOutputPath), { recursive: true });
4322
+ mkdirSync7(dirname3(outputPath), { recursive: true });
4323
+ if (backgroundOutputPath) mkdirSync7(dirname3(backgroundOutputPath), { recursive: true });
3868
4324
  const jobId = makeJobId(project.id, mediaJobs);
3869
4325
  const state = adapter.startBackgroundRemoval({
3870
4326
  project,
@@ -3921,14 +4377,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
3921
4377
  }
3922
4378
 
3923
4379
  // src/routes/globalAssets.ts
3924
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
4380
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
3925
4381
  import { homedir } from "os";
3926
4382
  import { join as join14 } from "path";
3927
4383
  function readGlobalAssets(home = homedir()) {
3928
4384
  const manifestPath = join14(home, ".media", "manifest.jsonl");
3929
- if (!existsSync10(manifestPath)) return [];
4385
+ if (!existsSync11(manifestPath)) return [];
3930
4386
  const out = [];
3931
- for (const line of readFileSync12(manifestPath, "utf8").split("\n")) {
4387
+ for (const line of readFileSync13(manifestPath, "utf8").split("\n")) {
3932
4388
  if (!line.trim()) continue;
3933
4389
  try {
3934
4390
  const rec = JSON.parse(line);