hyperframes 0.8.151 → 0.8.152

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.
Files changed (165) hide show
  1. package/dist/{add-QKZZG7A2.js → add-SA33REHW.js} +14 -14
  2. package/dist/{animatedGifPrep-YQDPYGOD.js → animatedGifPrep-3YXJQZ7L.js} +9 -9
  3. package/dist/api.js +15 -15
  4. package/dist/{asset-3Q737Y4H.js → asset-PUVPTWPD.js} +2 -2
  5. package/dist/{assetCataloger-BTHEBJJB.js → assetCataloger-VWE2PLBX.js} +4 -4
  6. package/dist/{auth-XL7EU3SO.js → auth-PS3M6AI7.js} +1 -1
  7. package/dist/{autoUpdate-PJFLTQ3U.js → autoUpdate-CMO4SHWU.js} +2 -2
  8. package/dist/{backgroundChecks-T5TIR5WK.js → backgroundChecks-EDZ2A55J.js} +3 -3
  9. package/dist/backgroundChecksWorker.js +3 -3
  10. package/dist/{batchRender-NFLVIDB4.js → batchRender-7PLJISCN.js} +5 -5
  11. package/dist/{beats-JU4GZ26R.js → beats-2MDSIOE7.js} +14 -14
  12. package/dist/{benchmark-7U2YWLTW.js → benchmark-KBBXAPI4.js} +10 -10
  13. package/dist/{browser-D57UPYDA.js → browser-OMWKZRSW.js} +3 -3
  14. package/dist/{bundleWithLocalizedFonts-26MXQOZG.js → bundleWithLocalizedFonts-76DMCRIW.js} +2 -2
  15. package/dist/{capture-47BV25G3.js → capture-LRUUXH5Q.js} +2 -2
  16. package/dist/{capture-LT252COQ.js → capture-YGDC7GN3.js} +12 -12
  17. package/dist/{catalog-BEZXOV4O.js → catalog-SMEBKEVO.js} +14 -14
  18. package/dist/{catch-up-KVV5X6BF.js → catch-up-OMILX3WE.js} +4 -4
  19. package/dist/{check-6KQ433E6.js → check-NUZ62LIW.js} +131 -43
  20. package/dist/{checkBrowser-PRDR6XBY.js → checkBrowser-MSTVEL4V.js} +39 -38
  21. package/dist/{chunk-L6RIMCMG.js → chunk-3DZBQ7MH.js} +3 -3
  22. package/dist/{chunk-POEEDPJI.js → chunk-3E4OCOC5.js} +1 -1
  23. package/dist/{chunk-YZTVHRJ2.js → chunk-3HPJM5IQ.js} +1 -1
  24. package/dist/{chunk-OZ4K46OV.js → chunk-3LO7BBJX.js} +2 -2
  25. package/dist/{chunk-APDLWWFG.js → chunk-3VGU2SH7.js} +1 -1
  26. package/dist/{chunk-RRBKEHST.js → chunk-4UMKAUZI.js} +1 -1
  27. package/dist/{chunk-DHPFXAUF.js → chunk-57GN6KWT.js} +17 -3
  28. package/dist/{chunk-374QQGBQ.js → chunk-5CS4T6C6.js} +63 -12
  29. package/dist/{chunk-XQKVEQNY.js → chunk-5ERVRJMV.js} +2 -2
  30. package/dist/{chunk-S22FKF6G.js → chunk-5VMSUW4Z.js} +2 -2
  31. package/dist/{chunk-N4IXP3MY.js → chunk-6Z4YJ5Q3.js} +3 -3
  32. package/dist/{chunk-FJWEYVEM.js → chunk-7FYFEXD2.js} +658 -198
  33. package/dist/{chunk-4LE27ST4.js → chunk-7NPSXBIN.js} +2 -2
  34. package/dist/chunk-7TASSBN2.js +215 -0
  35. package/dist/{chunk-IP73NMQT.js → chunk-ANS56TPS.js} +10 -10
  36. package/dist/{chunk-PUIELIFA.js → chunk-BQK3J7XO.js} +1 -1
  37. package/dist/{chunk-RJRGR5KF.js → chunk-BYJYQWEX.js} +9 -9
  38. package/dist/{chunk-XXPYZBZ3.js → chunk-C6TPGUWD.js} +2 -2
  39. package/dist/{chunk-FAIYEIZ4.js → chunk-D6RJXQLD.js} +1 -1
  40. package/dist/{chunk-N3I6RAOR.js → chunk-DFXK7KUO.js} +11 -11
  41. package/dist/{chunk-DIOBKUBV.js → chunk-E6EYZ33P.js} +2 -2
  42. package/dist/{chunk-ZGCRVM5M.js → chunk-EVTHG34H.js} +2 -2
  43. package/dist/{chunk-IZPN423R.js → chunk-EXH6L2XZ.js} +2 -2
  44. package/dist/{chunk-AIJS74LG.js → chunk-FFDC5OWV.js} +8 -2
  45. package/dist/{chunk-KQGWP4KH.js → chunk-FNN5QINS.js} +1 -1
  46. package/dist/{chunk-DMKI64EZ.js → chunk-FSZAFCU7.js} +1 -1
  47. package/dist/{chunk-L3N7RJHS.js → chunk-GQETH4PE.js} +3 -3
  48. package/dist/{chunk-ZD3RDNZI.js → chunk-H2SGM2V2.js} +1 -1
  49. package/dist/{chunk-OSAH7S5M.js → chunk-H7FXBK3Q.js} +1 -1
  50. package/dist/{chunk-BRF3O35X.js → chunk-HBCRRCTX.js} +109 -31
  51. package/dist/{chunk-KXX6NLOT.js → chunk-IBYC3VUA.js} +1 -1
  52. package/dist/{chunk-IPDS2SWI.js → chunk-IEDT4EXE.js} +1 -1
  53. package/dist/{chunk-TCWMDCBX.js → chunk-IZIRIDJH.js} +2 -2
  54. package/dist/{chunk-4PKQEKAI.js → chunk-IZKXUYZE.js} +2 -2
  55. package/dist/chunk-KCFF4ENE.js +24 -0
  56. package/dist/{chunk-IZ7L6V76.js → chunk-KZ3L3W4Q.js} +1 -1
  57. package/dist/{chunk-YEBKEWKB.js → chunk-LECTCGGS.js} +1 -1
  58. package/dist/{chunk-O3DUX6P2.js → chunk-LVPSWUS6.js} +4 -4
  59. package/dist/{chunk-LKQS72D5.js → chunk-MNHPG2UO.js} +1 -1
  60. package/dist/{chunk-XKKBIFE5.js → chunk-NHAG2KCE.js} +7 -7
  61. package/dist/{chunk-HVXRYMGQ.js → chunk-PKO2EWO7.js} +1 -1
  62. package/dist/{chunk-XNDXENJQ.js → chunk-PRXAOZWZ.js} +3 -3
  63. package/dist/{chunk-4YZYYP7V.js → chunk-PTXZDSW5.js} +3 -3
  64. package/dist/{chunk-QF7AE3DB.js → chunk-QECEOV6W.js} +1 -1
  65. package/dist/{chunk-XGLNQDLN.js → chunk-QESSGUKX.js} +1 -1
  66. package/dist/{chunk-3SVC7SW4.js → chunk-RN5I2DP5.js} +3 -3
  67. package/dist/{chunk-QBYHNRHU.js → chunk-S3RCK56N.js} +1 -1
  68. package/dist/{chunk-RYL6ZIJP.js → chunk-SWCSL6WS.js} +5 -5
  69. package/dist/{chunk-2PSPARWP.js → chunk-TYXH3EKP.js} +4 -0
  70. package/dist/{chunk-FMNVTKX2.js → chunk-U3BCDVZO.js} +1 -1
  71. package/dist/{chunk-FLVI7FHU.js → chunk-VS732BUG.js} +1 -1
  72. package/dist/{chunk-LFNQDTSH.js → chunk-VYKPBRCB.js} +63 -12
  73. package/dist/{chunk-QLGBNGXC.js → chunk-XEFFJRYX.js} +2 -2
  74. package/dist/{chunk-3ZPFRDEJ.js → chunk-XINGD6MS.js} +4 -4
  75. package/dist/chunk-XSEGMQYA.js +30 -0
  76. package/dist/{chunk-GNTVYF7E.js → chunk-YOEGUBBY.js} +5 -5
  77. package/dist/{chunk-35PWWBGZ.js → chunk-YYEYOFHW.js} +1 -1
  78. package/dist/{chunk-XYHBVSWD.js → chunk-ZTM3RMAD.js} +2 -2
  79. package/dist/{clean-RLTHFTQA.js → clean-F75G45BI.js} +52 -31
  80. package/dist/cli.js +53 -53
  81. package/dist/{cloud-Q5TGBTW3.js → cloud-KVJAOUWH.js} +4 -4
  82. package/dist/{cloudrun-4UZF74MV.js → cloudrun-Q7Q7O4GY.js} +6 -6
  83. package/dist/{compare-Q5UNR2V4.js → compare-DGUJ4QWU.js} +30 -30
  84. package/dist/{compiler-ZXT27Z62.js → compiler-NCN6PTVH.js} +6 -6
  85. package/dist/{component-H734P32V.js → component-PSYGT3JV.js} +3 -3
  86. package/dist/{compositions-YEHDSTDR.js → compositions-J3GAETIC.js} +13 -13
  87. package/dist/{delete-LDBMPWRT.js → delete-MR7LRGRP.js} +2 -2
  88. package/dist/{deterministicFonts-C5JQLCTS.js → deterministicFonts-EOMDR6ZF.js} +3 -3
  89. package/dist/{dist-RGE4QASU.js → dist-2YYJYYPY.js} +3 -3
  90. package/dist/{dist-7JXLFD6U.js → dist-3MHXG6EJ.js} +4 -4
  91. package/dist/{doctor-XNJFIWJ7.js → doctor-JOUGR5UK.js} +11 -11
  92. package/dist/{events-UKCPUWMH.js → events-63BKDWVO.js} +3 -3
  93. package/dist/{events-G6UFLNXR.js → events-FZFBJCIH.js} +2 -2
  94. package/dist/{feedback-CK3KZ7DE.js → feedback-QLKWUDXR.js} +8 -8
  95. package/dist/{figma-3D5C737B.js → figma-YJ7HGVQC.js} +3 -3
  96. package/dist/fontLocalizeCli.js +12 -12
  97. package/dist/{get-QGTQUWFT.js → get-54MUUXMR.js} +2 -2
  98. package/dist/{gpuPolicy-G5QAQDYF.js → gpuPolicy-5OVNR75D.js} +9 -9
  99. package/dist/{grade-compare-XIHOM62P.js → grade-compare-7IDUF673.js} +30 -30
  100. package/dist/{gsapParserAcorn-VXZ3ZIF2.js → gsapParserAcorn-7NCK2LVT.js} +2 -2
  101. package/dist/{help-U5UBXT52.js → help-GQFYO5LU.js} +1 -1
  102. package/dist/{history-5J73QZSA.js → history-UI3O7CSO.js} +77 -205
  103. package/dist/hyperframes-player.global.js +1 -1
  104. package/dist/{info-RJHQQQUP.js → info-VQV3APEX.js} +9 -9
  105. package/dist/{init-M6UQOYYC.js → init-2UBFV5D7.js} +24 -24
  106. package/dist/{inspect-PRQWDBTP.js → inspect-36ACJ3CO.js} +31 -31
  107. package/dist/{installer-ZL7UKYK5.js → installer-GDUZDJG4.js} +7 -7
  108. package/dist/{keyframes-VMUJYTCQ.js → keyframes-GPBU34BG.js} +227 -82
  109. package/dist/{lambda-ON42DRQD.js → lambda-CBJVLTHY.js} +7 -7
  110. package/dist/{layout-QKBFVDVY.js → layout-2IHFV6EI.js} +31 -31
  111. package/dist/{lint-ZQHHL2SV.js → lint-IR6IYCEO.js} +9 -9
  112. package/dist/{lintProject-3Z6OJCSO.js → lintProject-XQWXJYSC.js} +3 -3
  113. package/dist/{list-YBRKIWGF.js → list-UAGKWARH.js} +2 -2
  114. package/dist/{login-4B2YXGLB.js → login-2GYCNCZF.js} +4 -4
  115. package/dist/{media-treatment-5C5EJAAP.js → media-treatment-Z553F7XI.js} +12 -12
  116. package/dist/{models-R7ZDWBJ6.js → models-6AZTNNSO.js} +12 -12
  117. package/dist/{motionShot-ZHESZNUL.js → motionShot-ARRL7WXT.js} +28 -15
  118. package/dist/{normalize-audio-EVWZVTBM.js → normalize-audio-CME4NUB6.js} +6 -6
  119. package/dist/{open-TV6HT4UT.js → open-HU2VXUFD.js} +4 -4
  120. package/dist/{parakeetRunner-YVVF4CG6.js → parakeetRunner-X5MKFMCB.js} +12 -12
  121. package/dist/{pipeline-ETR5SEBS.js → pipeline-MNFTEBJU.js} +9 -9
  122. package/dist/{play-C6WONRZV.js → play-2BBJDDAZ.js} +24 -24
  123. package/dist/{present-RANXTVPA.js → present-OE2WL2NY.js} +20 -20
  124. package/dist/{preview-KF6UMBT2.js → preview-RYSCCAWJ.js} +19 -19
  125. package/dist/{producer-CY2GXBWN.js → producer-SQWNRO66.js} +1 -1
  126. package/dist/{projectConfig-KPXCNMBM.js → projectConfig-B55Z7YKQ.js} +9 -9
  127. package/dist/{publication-Y36N4A7E.js → publication-MDZCOBJP.js} +4 -4
  128. package/dist/{publish-RCQJO35C.js → publish-AILV36BO.js} +12 -12
  129. package/dist/{remove-background-3ZMXVWLB.js → remove-background-6F72LKHG.js} +10 -10
  130. package/dist/{render-PTNEFO5U.js → render-2MZKU3L5.js} +10 -10
  131. package/dist/{render-FKVRCDT2.js → render-5EREH6YD.js} +57 -22
  132. package/dist/{render-WUSTRMUD.js → render-6GH5FSXU.js} +5 -5
  133. package/dist/{render-batch-AQHU2JWZ.js → render-batch-UMBUQWWS.js} +5 -5
  134. package/dist/renderSetupWorker.js +3 -3
  135. package/dist/{resolver-L5BDZJKU.js → resolver-KO6WWO5P.js} +6 -6
  136. package/dist/{sherpa-3S3ISG7G.js → sherpa-DADGYWNR.js} +11 -11
  137. package/dist/skills/hyperframes-cli/SKILL.md +3 -0
  138. package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +5 -1
  139. package/dist/{skills-X4WQEKNF.js → skills-QLKSNU4Y.js} +5 -5
  140. package/dist/{skillsUpdateCheck-UYCVHDZ6.js → skillsUpdateCheck-Z2QQGF2W.js} +3 -3
  141. package/dist/{snapshot-YEYHQEZQ.js → snapshot-D5BBFXOS.js} +118 -41
  142. package/dist/{src-2NN2WB6X.js → src-AVQBAECC.js} +8 -8
  143. package/dist/{src-OLBDPEG6.js → src-V7263E47.js} +11 -11
  144. package/dist/{staticProjectServer-7H35NMWB.js → staticProjectServer-HNWSVEWN.js} +20 -20
  145. package/dist/studio/assets/{hyperframes-player-DJt8E7k4.js → hyperframes-player-0BY7-PK7.js} +1 -1
  146. package/dist/studio/assets/{index-DWIHrAY2.js → index-COrWdzlj.js} +1 -1
  147. package/dist/studio/assets/{index-BGks9KLI.js → index-p-VXwuJ2.js} +132 -132
  148. package/dist/studio/assets/{index-DgbLblGR.js → index-rmD5k_ES.js} +1 -1
  149. package/dist/studio/index.html +1 -1
  150. package/dist/{studio-api-HGM2A5I3.js → studio-api-UXKTDWLT.js} +30 -16
  151. package/dist/{studioServer-M6XFL7N6.js → studioServer-JOVR24O3.js} +43 -43
  152. package/dist/{synthesize-22B7L5GX.js → synthesize-MBDVUSH3.js} +2 -1
  153. package/dist/{telemetry-KVCZKVTE.js → telemetry-UW7XAM5W.js} +3 -3
  154. package/dist/{timeline-VYYKZR4V.js → timeline-JSGTZ3AN.js} +20 -20
  155. package/dist/{tokens-B5NFFL55.js → tokens-ADDBGTQ7.js} +2 -2
  156. package/dist/{transcribe-SBETOIWI.js → transcribe-FEHOK626.js} +10 -10
  157. package/dist/{transcribe-WNFRYZAC.js → transcribe-P6MJGSZJ.js} +16 -16
  158. package/dist/{tts-QSAQZVEZ.js → tts-POI553RG.js} +1 -1
  159. package/dist/{updateCheck-5MM2DFUC.js → updateCheck-IBWMLXQ4.js} +2 -2
  160. package/dist/{upgrade-LWID54R3.js → upgrade-NHZ7AYPO.js} +2 -2
  161. package/dist/{urlDownloader-CVLNXFDG.js → urlDownloader-ULDRBWS5.js} +8 -8
  162. package/dist/{validate-V4CGQ6OF.js → validate-NVWNXNBV.js} +25 -25
  163. package/dist/{video-VJB2KRSS.js → video-74DRD2DN.js} +9 -9
  164. package/package.json +1 -1
  165. package/dist/chunk-WBLXWOYK.js +0 -15
@@ -5,8 +5,9 @@ var require = __hf_createRequire(import.meta.url);
5
5
  var __filename = __hf_fileURLToPath(import.meta.url);
6
6
  var __dirname = __hf_dirname(__filename);
7
7
  import {
8
+ pinProjectRecord,
8
9
  realFilePath
9
- } from "./chunk-AIJS74LG.js";
10
+ } from "./chunk-FFDC5OWV.js";
10
11
  import {
11
12
  atomicTempPath,
12
13
  isAtomicTempPath,
@@ -17,41 +18,57 @@ import {
17
18
  mkdirWithinProject
18
19
  } from "./chunk-OFQWTKPW.js";
19
20
 
20
- // ../studio-server/dist/chunk-Y7V6FJHS.js
21
- import { randomUUID as randomUUID6 } from "crypto";
21
+ // ../studio-server/dist/chunk-CW5G6XXR.js
22
+ import { randomUUID as randomUUID7 } from "crypto";
22
23
  import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
23
24
  import {
24
- existsSync as existsSync2,
25
- lstatSync as lstatSync2,
26
- statSync as statSync3,
25
+ existsSync as existsSync3,
26
+ lstatSync as lstatSync3,
27
+ statSync as statSync4,
27
28
  mkdirSync as mkdirSync4,
28
29
  readdirSync as readdirSync3,
29
- readFileSync as readFileSync6,
30
+ readFileSync as readFileSync9,
30
31
  realpathSync,
31
32
  rmSync as rmSync2
32
33
  } from "fs";
33
34
  import { homedir } from "os";
34
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
35
+ import { dirname as dirname4, isAbsolute as isAbsolute5, join as join7, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
35
36
  import { createHash, randomUUID } from "crypto";
37
+ import { readFileSync as readFileSync2 } from "fs";
38
+ import { isAbsolute, relative, resolve, sep } from "path";
39
+ import { randomUUID as randomUUID2 } from "crypto";
40
+ import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
41
+ import { join } from "path";
36
42
  import { createHash as createHash2 } from "crypto";
37
- import { lstatSync, readFileSync, readdirSync } from "fs";
38
- import { extname, isAbsolute, relative, resolve, sep } from "path";
39
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
43
+ import { lstatSync, readFileSync as readFileSync3, readdirSync } from "fs";
44
+ import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
45
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
40
46
  import { constants, createReadStream, renameSync } from "fs";
41
47
  import { copyFile, mkdir, readdir, readFile, rename, rm, stat } from "fs/promises";
42
- import { dirname, join } from "path";
48
+ import { dirname, join as join2 } from "path";
43
49
  import { randomUUID as randomUUID5 } from "crypto";
44
- import { readdirSync as readdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
50
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
45
51
  import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
46
52
  import { tmpdir } from "os";
47
53
  import { dirname as dirname2, join as join4 } from "path";
48
- import { randomUUID as randomUUID3 } from "crypto";
49
- import { existsSync, mkdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
50
- import { basename, join as join2, resolve as resolve2 } from "path";
51
54
  import { randomUUID as randomUUID4 } from "crypto";
52
- import { linkSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
53
- import { join as join3 } from "path";
54
- import { closeSync, constants as constants2, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync5, writeSync } from "fs";
55
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2 } from "fs";
56
+ import { basename, join as join3, resolve as resolve3 } from "path";
57
+ import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
58
+ import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync7, renameSync as renameSync3, statSync as statSync3 } from "fs";
59
+ import { isAbsolute as isAbsolute4, join as join6, resolve as resolve5 } from "path";
60
+ import { createHash as createHash4 } from "crypto";
61
+ import {
62
+ closeSync,
63
+ constants as constants2,
64
+ fstatSync,
65
+ lstatSync as lstatSync2,
66
+ openSync,
67
+ readFileSync as readFileSync6,
68
+ unlinkSync
69
+ } from "fs";
70
+ import { isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
71
+ import { closeSync as closeSync2, constants as constants3, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync8, writeSync } from "fs";
55
72
  import { dirname as dirname3 } from "path";
56
73
  var RECEIPT_TTL_MS = 1e4;
57
74
  var receipts = /* @__PURE__ */ new Map();
@@ -128,6 +145,127 @@ function newestReceipt(absPath, expectedVersion) {
128
145
  return receipt;
129
146
  }
130
147
  var consumeFileWriteReceipt = identifyFileWrite;
148
+ var HistoryBusyError = class extends Error {
149
+ constructor(pid) {
150
+ super(`This project's history is open in another process (pid ${pid}).`);
151
+ this.pid = pid;
152
+ this.name = "HistoryBusyError";
153
+ }
154
+ pid;
155
+ };
156
+ function alive(pid) {
157
+ if (!Number.isInteger(pid) || pid <= 0) return false;
158
+ try {
159
+ process.kill(pid, 0);
160
+ return true;
161
+ } catch (error) {
162
+ return error.code === "EPERM";
163
+ }
164
+ }
165
+ function ownerOf(file) {
166
+ try {
167
+ const text = readFileSync(file, "utf-8");
168
+ return /^\d+$/.test(text) ? Number(text) : Number.NaN;
169
+ } catch (error) {
170
+ if (error.code === "ENOENT") return null;
171
+ throw error;
172
+ }
173
+ }
174
+ function claim(file) {
175
+ const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
176
+ writeFileSync(draft, String(process.pid));
177
+ try {
178
+ linkSync(draft, file);
179
+ return true;
180
+ } catch (error) {
181
+ if (error.code !== "EEXIST") throw error;
182
+ return false;
183
+ } finally {
184
+ rmSync(draft, { force: true });
185
+ }
186
+ }
187
+ function releaseOwn(file) {
188
+ if (ownerOf(file) === process.pid) rmSync(file, { force: true });
189
+ }
190
+ function evictDeadOwner(file) {
191
+ const evictor = `${file}.evict`;
192
+ if (!claim(evictor)) {
193
+ const holder = ownerOf(evictor);
194
+ if (holder !== null && !alive(holder)) rmSync(evictor, { force: true });
195
+ return false;
196
+ }
197
+ try {
198
+ const pid = ownerOf(file);
199
+ if (pid !== null && !alive(pid)) rmSync(file, { force: true });
200
+ return true;
201
+ } finally {
202
+ releaseOwn(evictor);
203
+ }
204
+ }
205
+ async function takeHistoryOwnership(home, waitMs) {
206
+ const file = join(home, "owner.pid");
207
+ const deadline = Date.now() + waitMs;
208
+ mkdirSync(home, { recursive: true });
209
+ for (; ; ) {
210
+ if (claim(file)) {
211
+ let held = true;
212
+ return () => {
213
+ if (held) releaseOwn(file);
214
+ held = false;
215
+ };
216
+ }
217
+ const pid = ownerOf(file);
218
+ if (pid === null) continue;
219
+ if (!alive(pid) && evictDeadOwner(file)) continue;
220
+ if (Date.now() >= deadline) throw new HistoryBusyError(pid);
221
+ await new Promise((settle) => setTimeout(settle, 50));
222
+ }
223
+ }
224
+ var manifest = ".hyperframes/render-outputs.json";
225
+ function readRenderOutputs(projectDir) {
226
+ const path = pinProjectRecord(projectDir, manifest);
227
+ if (!path) return [];
228
+ let text;
229
+ try {
230
+ text = readFileSync2(path, "utf8");
231
+ } catch (error) {
232
+ const code = error.code;
233
+ if (code === "ENOENT" || code === "ENOTDIR") return [];
234
+ throw error;
235
+ }
236
+ const value = JSON.parse(text);
237
+ if (value.version !== 1 || !Array.isArray(value.paths) || !value.paths.every(
238
+ (p) => typeof p === "string" && p !== "" && !isAbsolute(p) && !p.split(/[\\/]/).some((part) => part === ".." || part === ".")
239
+ ))
240
+ throw new Error("Invalid render output registry.");
241
+ return value.paths;
242
+ }
243
+ function readRenderOutputsOrNone(projectDir) {
244
+ try {
245
+ return readRenderOutputs(projectDir);
246
+ } catch {
247
+ return [];
248
+ }
249
+ }
250
+ function isRenderOutput(projectDir, path, outputs) {
251
+ const candidate = relative(resolve(projectDir), resolve(path)).split(sep).join("/");
252
+ return outputs.some((output) => candidate === output || candidate.startsWith(`${output}/`));
253
+ }
254
+ async function registerRenderOutput(projectDir, output) {
255
+ const path = relative(resolve(projectDir), resolve(output)).split(sep).join("/");
256
+ if (path === "") throw new Error("Render output cannot replace the project directory.");
257
+ if (isAbsolute(path) || path === ".." || path.startsWith("../")) return;
258
+ const registry = pinProjectRecord(projectDir, manifest);
259
+ const lock = pinProjectRecord(projectDir, ".hyperframes/render-outputs-lock");
260
+ if (!registry || !lock) throw new Error("Render output registry must stay inside the project.");
261
+ const release = await takeHistoryOwnership(lock, 5e3);
262
+ try {
263
+ const paths = [.../* @__PURE__ */ new Set([...readRenderOutputs(projectDir), path])].sort();
264
+ replaceFileAtomically(registry, JSON.stringify({ version: 1, paths }) + "\n", 420);
265
+ } finally {
266
+ release();
267
+ }
268
+ }
131
269
  var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
132
270
  ".cjs",
133
271
  ".css",
@@ -162,25 +300,36 @@ var STUDIO_SIGNATURE_MANIFEST_PATHS = [
162
300
  ".hyperframes/studio-motion.json"
163
301
  ];
164
302
  function affectsProjectSignature(projectDir, changedPath) {
165
- const relativePath = relative(resolve(projectDir), resolve(changedPath));
166
- if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath) || isAtomicTempPath(relativePath)) {
303
+ const relativePath = relative2(resolve2(projectDir), resolve2(changedPath));
304
+ if (relativePath === "" || relativePath.startsWith("..") || isAbsolute2(relativePath) || isAtomicTempPath(relativePath)) {
167
305
  return false;
168
306
  }
169
- const segments = relativePath.split(sep);
307
+ if (isRenderOutput(projectDir, changedPath, readRenderOutputsOrNone(projectDir))) return false;
308
+ const segments = relativePath.split(sep2);
170
309
  if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
171
310
  return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment));
172
311
  }
173
312
  var projectSignatureCache = /* @__PURE__ */ new Map();
174
313
  var COARSEST_FILE_TIME_TICK_MS = 2e3;
175
314
  function isPathWithin(parentDir, childPath) {
176
- const childRelativePath = relative(parentDir, childPath);
177
- return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
315
+ const childRelativePath = relative2(parentDir, childPath);
316
+ return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute2(childRelativePath);
178
317
  }
179
318
  function isTextContentEligible(file, size) {
180
319
  return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
181
320
  }
182
321
  var isSkippedEntry = (entry) => SIGNATURE_EXCLUDED_DIRS.has(entry) || isAtomicTempPath(entry);
183
- function collectProjectSignatureFiles(projectDir, dir, files) {
322
+ function signatureEntryStat(projectDir, entry, file, outputs) {
323
+ if (isSkippedEntry(entry) || !isPathWithin(projectDir, file)) return null;
324
+ if (isRenderOutput(projectDir, file, outputs)) return null;
325
+ try {
326
+ const stat2 = lstatSync(file);
327
+ return stat2.isSymbolicLink() ? null : stat2;
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+ function collectProjectSignatureFiles(projectDir, dir, files, outputs = readRenderOutputsOrNone(projectDir)) {
184
333
  let entries;
185
334
  try {
186
335
  entries = readdirSync(dir).sort();
@@ -188,18 +337,11 @@ function collectProjectSignatureFiles(projectDir, dir, files) {
188
337
  return;
189
338
  }
190
339
  for (const entry of entries) {
191
- if (isSkippedEntry(entry)) continue;
192
- const file = resolve(dir, entry);
193
- if (!isPathWithin(projectDir, file)) continue;
194
- let stat2;
195
- try {
196
- stat2 = lstatSync(file);
197
- } catch {
198
- continue;
199
- }
200
- if (stat2.isSymbolicLink()) continue;
340
+ const file = resolve2(dir, entry);
341
+ const stat2 = signatureEntryStat(projectDir, entry, file, outputs);
342
+ if (!stat2) continue;
201
343
  if (stat2.isDirectory()) {
202
- collectProjectSignatureFiles(projectDir, file, files);
344
+ collectProjectSignatureFiles(projectDir, file, files, outputs);
203
345
  } else if (stat2.isFile()) {
204
346
  files.push({
205
347
  file,
@@ -214,7 +356,7 @@ function collectProjectSignatureFiles(projectDir, dir, files) {
214
356
  function collectProjectSignatureManifestFiles(projectDir, files) {
215
357
  const seen = new Set(files.map((entry) => entry.file));
216
358
  for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
217
- const file = resolve(projectDir, manifestPath);
359
+ const file = resolve2(projectDir, manifestPath);
218
360
  if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
219
361
  let stat2;
220
362
  try {
@@ -236,7 +378,7 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
236
378
  function createProjectFingerprint(projectDir, files) {
237
379
  const hash = createHash2("sha256");
238
380
  for (const entry of files) {
239
- hash.update(relative(projectDir, entry.file));
381
+ hash.update(relative2(projectDir, entry.file));
240
382
  hash.update("\0");
241
383
  hash.update(String(entry.size));
242
384
  hash.update("\0");
@@ -256,9 +398,9 @@ function collectProjectFiles(normalizedProjectDir) {
256
398
  return collected;
257
399
  }
258
400
  function listProjectFiles(projectDir) {
259
- const normalizedProjectDir = resolve(projectDir);
401
+ const normalizedProjectDir = resolve2(projectDir);
260
402
  return collectProjectFiles(normalizedProjectDir).map((entry) => ({
261
- path: relative(normalizedProjectDir, entry.file).split(sep).join("/"),
403
+ path: relative2(normalizedProjectDir, entry.file).split(sep2).join("/"),
262
404
  size: entry.size,
263
405
  mtimeMs: entry.mtimeMs,
264
406
  ctimeMs: entry.ctimeMs
@@ -273,11 +415,11 @@ async function resolveProjectAndSignature(adapter, projectId) {
273
415
  return { project, signature: resolveProjectSignature(adapter, project.dir) };
274
416
  }
275
417
  function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
276
- const normalizedProjectDir = resolve(projectDir);
418
+ const normalizedProjectDir = resolve2(projectDir);
277
419
  const signedAt = Date.now();
278
420
  const collected = collectProjectFiles(normalizedProjectDir);
279
421
  const files = collected.filter(
280
- (entry) => !excluding.has(relative(normalizedProjectDir, entry.file).split(sep).join("/"))
422
+ (entry) => !excluding.has(relative2(normalizedProjectDir, entry.file).split(sep2).join("/"))
281
423
  );
282
424
  files.sort((a, b) => a.file.localeCompare(b.file));
283
425
  const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
@@ -286,14 +428,14 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
286
428
  if (cached?.fingerprint === fingerprint) return cached.signature;
287
429
  const hash = createHash2("sha256");
288
430
  for (const entry of files) {
289
- const relativePath = relative(normalizedProjectDir, entry.file);
431
+ const relativePath = relative2(normalizedProjectDir, entry.file);
290
432
  hash.update(relativePath);
291
433
  hash.update("\0");
292
434
  hash.update(String(entry.size));
293
435
  hash.update("\0");
294
436
  if (entry.textContentEligible) {
295
437
  try {
296
- hash.update(readFileSync(entry.file));
438
+ hash.update(readFileSync3(entry.file));
297
439
  } catch {
298
440
  hash.update(String(entry.mtimeMs));
299
441
  }
@@ -312,7 +454,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
312
454
  var BLOB_HASH = /^[0-9a-f]{64}$/;
313
455
  function blobPath(dir, hash) {
314
456
  if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
315
- return join(dir, hash.slice(0, 2), hash);
457
+ return join2(dir, hash.slice(0, 2), hash);
316
458
  }
317
459
  var readBlob = (dir, hash) => readFile(blobPath(dir, hash));
318
460
  async function hashFile(path) {
@@ -336,13 +478,13 @@ async function openBlobStore(dir) {
336
478
  const sizes = /* @__PURE__ */ new Map();
337
479
  await mkdir(dir, { recursive: true });
338
480
  for (const shard of await readdir(dir))
339
- for (const hash of await readdir(join(dir, shard)).catch(() => []))
340
- sizes.set(hash, (await stat(join(dir, shard, hash))).size);
481
+ for (const hash of await readdir(join2(dir, shard)).catch(() => []))
482
+ sizes.set(hash, (await stat(join2(dir, shard, hash))).size);
341
483
  const pathOf = (hash) => blobPath(dir, hash);
342
484
  let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
343
485
  return {
344
486
  async put(absPath) {
345
- const temp = join(dir, `incoming-${randomUUID2()}`);
487
+ const temp = join2(dir, `incoming-${randomUUID3()}`);
346
488
  await mkdir(dir, { recursive: true });
347
489
  await copyFile(absPath, temp, constants.COPYFILE_FICLONE);
348
490
  const hash = await hashFile(temp);
@@ -372,12 +514,12 @@ async function openBlobStore(dir) {
372
514
  }
373
515
  };
374
516
  }
375
- var ID_PATH = join2(".hyperframes", "history-id");
517
+ var ID_PATH = join3(".hyperframes", "history-id");
376
518
  var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
377
519
  var isHistoryId = (name) => ID_SHAPE.test(name);
378
520
  function readId(projectDir) {
379
521
  try {
380
- const id = readFileSync2(join2(projectDir, ID_PATH), "utf-8").trim();
522
+ const id = readFileSync4(join3(projectDir, ID_PATH), "utf-8").trim();
381
523
  return ID_SHAPE.test(id) ? id : null;
382
524
  } catch {
383
525
  return null;
@@ -394,7 +536,7 @@ function isRecordedFolder(historyDir, folder) {
394
536
  }
395
537
  function readRecord(historyDir) {
396
538
  try {
397
- return JSON.parse(readFileSync2(join2(historyDir, "project.json"), "utf-8"));
539
+ return JSON.parse(readFileSync4(join3(historyDir, "project.json"), "utf-8"));
398
540
  } catch {
399
541
  return null;
400
542
  }
@@ -409,20 +551,20 @@ function isCopyOf0878Folder(recordedDir, id, folder) {
409
551
  return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
410
552
  }
411
553
  function projectHistoryId(projectDir, historyRoot) {
412
- const dir = resolve2(projectDir);
554
+ const dir = resolve3(projectDir);
413
555
  const folder = statSync(dir);
414
556
  let id = readId(dir);
415
- if (!id && existsSync(join2(dir, ID_PATH)))
557
+ if (!id && existsSync(join3(dir, ID_PATH)))
416
558
  throw new HistoryIdError(
417
- `${join2(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
559
+ `${join3(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
418
560
  );
419
- if (!id || existsSync(join2(historyRoot, id)) && !isRecordedFolder(join2(historyRoot, id), folder)) {
420
- id = randomUUID3();
421
- mkdirWithinProject(dir, join2(dir, ".hyperframes"));
422
- writeFileSync(join2(dir, ID_PATH), `${id}
561
+ if (!id || existsSync(join3(historyRoot, id)) && !isRecordedFolder(join3(historyRoot, id), folder)) {
562
+ id = randomUUID4();
563
+ mkdirWithinProject(dir, join3(dir, ".hyperframes"));
564
+ writeFileSync2(join3(dir, ID_PATH), `${id}
423
565
  `);
424
566
  }
425
- recordProject(join2(historyRoot, id), dir, folder);
567
+ recordProject(join3(historyRoot, id), dir, folder);
426
568
  return id;
427
569
  }
428
570
  var HistoryIdError = class extends Error {
@@ -433,84 +575,8 @@ var HistoryIdError = class extends Error {
433
575
  };
434
576
  function recordProject(historyDir, dir, folder) {
435
577
  const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
436
- mkdirSync(historyDir, { recursive: true });
437
- replaceFileAtomically(join2(historyDir, "project.json"), JSON.stringify(record), 420);
438
- }
439
- var HistoryBusyError = class extends Error {
440
- constructor(pid) {
441
- super(`This project's history is open in another process (pid ${pid}).`);
442
- this.pid = pid;
443
- this.name = "HistoryBusyError";
444
- }
445
- pid;
446
- };
447
- function alive(pid) {
448
- if (!Number.isInteger(pid) || pid <= 0) return false;
449
- try {
450
- process.kill(pid, 0);
451
- return true;
452
- } catch (error) {
453
- return error.code === "EPERM";
454
- }
455
- }
456
- function ownerOf(file) {
457
- try {
458
- const text = readFileSync3(file, "utf-8");
459
- return /^\d+$/.test(text) ? Number(text) : Number.NaN;
460
- } catch (error) {
461
- if (error.code === "ENOENT") return null;
462
- throw error;
463
- }
464
- }
465
- function claim(file) {
466
- const draft = `${file}-${randomUUID4()}.tmp`;
467
- writeFileSync2(draft, String(process.pid));
468
- try {
469
- linkSync(draft, file);
470
- return true;
471
- } catch (error) {
472
- if (error.code !== "EEXIST") throw error;
473
- return false;
474
- } finally {
475
- rmSync(draft, { force: true });
476
- }
477
- }
478
- function releaseOwn(file) {
479
- if (ownerOf(file) === process.pid) rmSync(file, { force: true });
480
- }
481
- function evictDeadOwner(file) {
482
- const evictor = `${file}.evict`;
483
- if (!claim(evictor)) {
484
- const holder = ownerOf(evictor);
485
- if (holder !== null && !alive(holder)) rmSync(evictor, { force: true });
486
- return false;
487
- }
488
- try {
489
- const pid = ownerOf(file);
490
- if (pid !== null && !alive(pid)) rmSync(file, { force: true });
491
- return true;
492
- } finally {
493
- releaseOwn(evictor);
494
- }
495
- }
496
- async function takeHistoryOwnership(home, waitMs) {
497
- const file = join3(home, "owner.pid");
498
- const deadline = Date.now() + waitMs;
499
- mkdirSync2(home, { recursive: true });
500
- for (; ; ) {
501
- if (claim(file)) {
502
- let held = true;
503
- return () => {
504
- if (held) releaseOwn(file);
505
- held = false;
506
- };
507
- }
508
- const pid = ownerOf(file);
509
- if (pid === null) continue;
510
- if (!alive(pid) && evictDeadOwner(file)) continue;
511
- if (Date.now() >= deadline) throw new HistoryBusyError(pid);
512
- await new Promise((settle) => setTimeout(settle, 50));
513
- }
578
+ mkdirSync2(historyDir, { recursive: true });
579
+ replaceFileAtomically(join3(historyDir, "project.json"), JSON.stringify(record), 420);
514
580
  }
515
581
  var DAY_MS = 24 * 60 * 60 * 1e3;
516
582
  var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
@@ -641,7 +707,7 @@ async function bytesIn(dir) {
641
707
  }
642
708
  function prunedWithinADay(stamp, now) {
643
709
  try {
644
- const last = Number(readFileSync4(stamp, "utf-8"));
710
+ const last = Number(readFileSync5(stamp, "utf-8"));
645
711
  return last <= now && now - last < DAY_MS;
646
712
  } catch {
647
713
  return false;
@@ -656,6 +722,332 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
656
722
  pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync3(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
657
723
  });
658
724
  }
725
+ var kinds = ["checks", "snapshots"];
726
+ var records = (state, kind) => [
727
+ ...Object.values(state[kind].revisions).flat(),
728
+ ...state[kind].unversioned
729
+ ];
730
+ var protectedRevisions = (state) => /* @__PURE__ */ new Set([state.currentRevision, ...state.renders?.retainedRevisions ?? []]);
731
+ function managedPath(projectDir, path, kind) {
732
+ const canonicalProject = realFilePath(resolve4(projectDir));
733
+ const canonicalPath = realFilePath(path);
734
+ const root = join5(canonicalProject, ".hyperframes", "artifacts", kind);
735
+ const rel = relative3(root, canonicalPath);
736
+ return isAbsolute3(path) && rel !== "" && rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute3(rel) && pinProjectRecord(canonicalProject, relative3(canonicalProject, canonicalPath)) === canonicalPath;
737
+ }
738
+ function updateLatest(state) {
739
+ for (const kind of kinds) {
740
+ const available = records(state, kind).filter((record) => record.status !== "cleaned");
741
+ if (available.some(
742
+ (record) => record.id === state[kind].latest?.id && record.revision === state[kind].latest?.revision
743
+ ))
744
+ continue;
745
+ const latest = available.sort((a, b) => b.createdAt - a.createdAt)[0];
746
+ state[kind].latest = latest ? { revision: latest.revision, id: latest.id } : null;
747
+ }
748
+ }
749
+ function planArtifactCleanup(projectDir, state, render) {
750
+ const protectedIds = protectedRevisions(state);
751
+ const candidates = [];
752
+ const keepPaths = /* @__PURE__ */ new Set();
753
+ const removable = (record, path, kind) => Boolean(record.revision) && !protectedIds.has(record.revision) && record.createdAt <= render.completedAt && managedPath(projectDir, path, kind);
754
+ for (const kind of kinds) {
755
+ for (const record of records(state, kind)) {
756
+ const path = record.image ?? record.report;
757
+ if (!path || record.status === "cleaned") continue;
758
+ if (removable(record, path, kind)) candidates.push({ kind, record, path });
759
+ else keepPaths.add(realFilePath(path));
760
+ }
761
+ }
762
+ for (const { kind, record, path } of candidates) {
763
+ if (!keepPaths.has(realFilePath(path))) markCleaned(state, kind, record, path, render.id);
764
+ }
765
+ updateLatest(state);
766
+ }
767
+ function markCleaned(state, kind, record, path, renderId) {
768
+ (state.pendingArtifactCleanup ??= []).push({
769
+ path,
770
+ hash: record.contentHash,
771
+ revision: record.revision,
772
+ id: record.id,
773
+ kind
774
+ });
775
+ delete record.image;
776
+ delete record.report;
777
+ record.status = "cleaned";
778
+ record.cleanedAfterRender = renderId;
779
+ }
780
+ function restoreCleaned(state, item) {
781
+ const record = state[item.kind].revisions[item.revision]?.find(
782
+ (candidate) => candidate.id === item.id
783
+ );
784
+ if (record?.status !== "cleaned") return;
785
+ if (item.kind === "checks") record.report = item.path;
786
+ else record.image = item.path;
787
+ delete record.status;
788
+ delete record.cleanedAfterRender;
789
+ }
790
+ function stillPendingAfterUnlink(item) {
791
+ let descriptor;
792
+ try {
793
+ descriptor = openSync(item.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
794
+ const stat2 = fstatSync(descriptor);
795
+ if (!stat2.isFile() || stat2.isSymbolicLink()) return false;
796
+ const hash = createHash4("sha256").update(readFileSync6(descriptor)).digest("hex");
797
+ if (hash !== item.hash) return false;
798
+ const current = lstatSync2(item.path);
799
+ if (current.dev !== stat2.dev || current.ino !== stat2.ino) return false;
800
+ unlinkSync(item.path);
801
+ return false;
802
+ } catch (error) {
803
+ return error.code !== "ENOENT";
804
+ } finally {
805
+ if (descriptor !== void 0) closeSync(descriptor);
806
+ }
807
+ }
808
+ function finishArtifactCleanup(projectDir, state) {
809
+ const protectedIds = protectedRevisions(state);
810
+ const referenced = new Set(
811
+ kinds.flatMap(
812
+ (kind) => records(state, kind).flatMap((record) => record.image ?? record.report ?? []).map(realFilePath)
813
+ )
814
+ );
815
+ state.pendingArtifactCleanup = (state.pendingArtifactCleanup ?? []).filter((item) => {
816
+ if (!managedPath(projectDir, item.path, item.kind)) return false;
817
+ const canonicalPath = realFilePath(item.path);
818
+ if (protectedIds.has(item.revision)) {
819
+ restoreCleaned(state, item);
820
+ return false;
821
+ }
822
+ if (state.pendingChanges) return true;
823
+ if (referenced.has(canonicalPath)) return false;
824
+ return stillPendingAfterUnlink({ ...item, path: canonicalPath });
825
+ });
826
+ updateLatest(state);
827
+ }
828
+ function statePath(dir) {
829
+ const path = pinProjectRecord(dir, ".hyperframes/video-state.json");
830
+ if (!path) throw new Error("Video state must stay inside the project.");
831
+ return path;
832
+ }
833
+ var collection = () => ({ latest: null, revisions: {}, unversioned: [] });
834
+ var emptyState = () => ({
835
+ version: 1,
836
+ historyId: null,
837
+ currentRevision: null,
838
+ pendingChanges: true,
839
+ files: {},
840
+ revisions: {},
841
+ checks: collection(),
842
+ snapshots: collection()
843
+ });
844
+ function readState(dir) {
845
+ if (!existsSync2(statePath(dir))) return emptyState();
846
+ const state = JSON.parse(readFileSync7(statePath(dir), "utf8"));
847
+ if (state.version !== 1 || !state.checks || !state.snapshots || !state.revisions || !state.files)
848
+ throw new Error("Unsupported video-state schema.");
849
+ return state;
850
+ }
851
+ async function updateState(dir, update) {
852
+ const lock = pinProjectRecord(dir, ".hyperframes/video-state-lock");
853
+ if (!lock) throw new Error("Video state lock must stay inside the project.");
854
+ const release = await takeHistoryOwnership(lock, 5e3);
855
+ try {
856
+ let state;
857
+ try {
858
+ state = readState(dir);
859
+ } catch (error) {
860
+ if (!(error instanceof SyntaxError)) throw error;
861
+ renameSync3(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
862
+ state = emptyState();
863
+ }
864
+ update(state);
865
+ replaceFileAtomically(statePath(dir), JSON.stringify(state, null, 2) + "\n", 420);
866
+ } finally {
867
+ release();
868
+ }
869
+ }
870
+ async function writeHistoryState(projectDir, historyId, historyDirectory, entries, currentFiles, committedFiles) {
871
+ const blob = (hash) => hash === null ? null : resolve5(historyDirectory, "blobs", hash.slice(0, 2), hash);
872
+ await updateState(projectDir, (state) => {
873
+ if (state.historyId && state.historyId !== historyId) {
874
+ state.checks = collection();
875
+ state.snapshots = collection();
876
+ }
877
+ state.historyId = historyId;
878
+ state.currentRevision = entries.at(-1)?.id ?? null;
879
+ state.pendingChanges = currentFiles.size !== committedFiles.size || [...currentFiles].some(([path, hash]) => committedFiles.get(path) !== hash);
880
+ state.files = Object.fromEntries(
881
+ [...currentFiles].map(([path, hash]) => [path, { hash, content: blob(hash) }])
882
+ );
883
+ state.revisions = Object.fromEntries(
884
+ entries.map((entry) => [entry.id, revisionState(entry, blob)])
885
+ );
886
+ dropArtifactsOfGoneRevisions(state);
887
+ });
888
+ }
889
+ function revisionState(entry, blob) {
890
+ const targets = entry.targets?.filter(
891
+ (target) => entry.files.some((file) => file.path === target.file)
892
+ );
893
+ return {
894
+ actor: entry.who,
895
+ ...targets?.length && { targets },
896
+ label: entry.label,
897
+ startedAt: entry.startedAt,
898
+ endedAt: entry.endedAt,
899
+ ...entry.undoes && { undoes: entry.undoes },
900
+ ...entry.restoredTo && { restoredTo: entry.restoredTo },
901
+ files: Object.fromEntries(
902
+ entry.files.map((file) => [
903
+ file.path,
904
+ { before: blob(file.before), after: blob(file.after) }
905
+ ])
906
+ )
907
+ };
908
+ }
909
+ function dropArtifactsOfGoneRevisions(state) {
910
+ const kept = (revision) => Boolean(state.revisions[revision]) || Boolean(state.renders?.retainedRevisions?.includes(revision));
911
+ for (const artifacts of [state.checks, state.snapshots]) {
912
+ for (const revision of Object.keys(artifacts.revisions)) {
913
+ if (!kept(revision)) delete artifacts.revisions[revision];
914
+ }
915
+ if (artifacts.latest?.revision && !state.revisions[artifacts.latest.revision])
916
+ artifacts.latest = null;
917
+ }
918
+ }
919
+ async function fileHashes(dir) {
920
+ const files = {};
921
+ for (const { path } of listProjectFiles(dir).sort((a, b) => a.path.localeCompare(b.path))) {
922
+ const hash = createHash5("sha256");
923
+ for await (const chunk of createReadStream2(join6(dir, path))) hash.update(chunk);
924
+ files[path] = hash.digest("hex");
925
+ }
926
+ return files;
927
+ }
928
+ async function beginArtifactCapture(projectDir) {
929
+ const files = await fileHashes(projectDir);
930
+ const state = readState(projectDir);
931
+ const matches = state.historyId !== null && readId(projectDir) === state.historyId && !state.pendingChanges && Object.entries(state.files).every(([path, file]) => files[path] === file.hash) && Object.keys(files).filter((path) => !path.split("/").some((part) => part.startsWith("."))).every((path) => state.files[path]?.hash === files[path]);
932
+ return {
933
+ revision: matches ? state.currentRevision : null,
934
+ fingerprint: createHash5("sha256").update(JSON.stringify(files)).digest("hex")
935
+ };
936
+ }
937
+ async function recordArtifact(projectDir, capture, artifact) {
938
+ return recordArtifacts(projectDir, capture, [artifact]);
939
+ }
940
+ async function recordArtifacts(projectDir, capture, artifactsToRecord) {
941
+ const records2 = [];
942
+ const hashes = /* @__PURE__ */ new Map();
943
+ for (const artifact of artifactsToRecord) {
944
+ const path = artifact.kind === "check" ? artifact.report : artifact.image;
945
+ if (!path || !isAbsolute4(path)) throw new Error("Artifact needs an absolute output path.");
946
+ hashes.set(artifact.id, createHash5("sha256").update(readFileSync7(path)).digest("hex"));
947
+ }
948
+ const after = await beginArtifactCapture(projectDir);
949
+ const unchanged = after.fingerprint === capture.fingerprint;
950
+ await updateState(projectDir, (state) => {
951
+ const revision = unchanged ? knownRevision(state, capture.revision) : null;
952
+ const attribution = attributionOf(unchanged, revision);
953
+ for (const artifact of artifactsToRecord) {
954
+ const { kind, ...fields } = artifact;
955
+ const record = {
956
+ ...fields,
957
+ createdAt: Date.now(),
958
+ contentHash: hashes.get(artifact.id),
959
+ revision,
960
+ inputFingerprint: capture.fingerprint,
961
+ inputScope: "local-project-files",
962
+ attribution
963
+ };
964
+ const artifacts = kind === "check" ? state.checks : state.snapshots;
965
+ dropSupersededArtifacts(artifacts, artifact);
966
+ const list = revision ? artifacts.revisions[revision] ??= [] : artifacts.unversioned;
967
+ list.push(record);
968
+ records2.push(record);
969
+ artifacts.latest = { revision, id: record.id };
970
+ }
971
+ });
972
+ return records2;
973
+ }
974
+ function knownRevision(state, revision) {
975
+ return revision && state.revisions[revision] ? revision : null;
976
+ }
977
+ function attributionOf(unchanged, revision) {
978
+ if (!unchanged) return "inputs-changed";
979
+ return revision ? "recorded-revision" : "unversioned";
980
+ }
981
+ function supersedes(artifact, previous) {
982
+ if (previous.status !== "cleaned" && !existsSync2(previous.image ?? previous.report ?? ""))
983
+ return true;
984
+ if (previous.id === artifact.id) return true;
985
+ if (artifact.image && previous.image === artifact.image) return true;
986
+ return Boolean(artifact.report && previous.report === artifact.report);
987
+ }
988
+ function dropSupersededArtifacts(artifacts, artifact) {
989
+ for (const list of [...Object.values(artifacts.revisions), artifacts.unversioned]) {
990
+ for (let i = list.length - 1; i >= 0; i--) {
991
+ if (supersedes(artifact, list[i])) list.splice(i, 1);
992
+ }
993
+ }
994
+ }
995
+ function listsOf(artifacts) {
996
+ return [...Object.values(artifacts.revisions), artifacts.unversioned];
997
+ }
998
+ async function forgetRecordedArtifacts(projectDir, paths) {
999
+ const gone = new Set(paths.map((path) => resolve5(path)));
1000
+ if (gone.size === 0) return;
1001
+ await updateState(projectDir, (state) => {
1002
+ for (const artifacts of [state.checks, state.snapshots]) {
1003
+ for (const list of listsOf(artifacts)) {
1004
+ for (let i = list.length - 1; i >= 0; i--) {
1005
+ const record = list[i];
1006
+ const path = record.image ?? record.report;
1007
+ if (path && gone.has(resolve5(path))) list.splice(i, 1);
1008
+ }
1009
+ }
1010
+ if (artifacts.latest && !listsOf(artifacts).some(
1011
+ (list) => list.some((record) => record.id === artifacts.latest?.id)
1012
+ )) {
1013
+ artifacts.latest = null;
1014
+ }
1015
+ }
1016
+ });
1017
+ }
1018
+ function artifactDirectory(projectDir, kind) {
1019
+ return join6(resolve5(projectDir), ".hyperframes", "artifacts", kind, randomUUID6());
1020
+ }
1021
+ function retainRevision(retained, revision) {
1022
+ if (!revision) return [...retained];
1023
+ return [revision, ...retained.filter((id) => id !== revision)].slice(0, 2);
1024
+ }
1025
+ async function recordRenderSuccess(projectDir, output, status = "completed", capture) {
1026
+ const outputFile = statSync3(output);
1027
+ if (!outputFile.isFile() && !outputFile.isDirectory() || outputFile.isFile() && outputFile.size === 0)
1028
+ throw new Error("Render output is missing or empty.");
1029
+ const after = capture ? await beginArtifactCapture(projectDir) : null;
1030
+ const record = {
1031
+ id: randomUUID6(),
1032
+ completedAt: Date.now(),
1033
+ output: resolve5(output),
1034
+ status
1035
+ };
1036
+ const unchanged = Boolean(capture && after?.fingerprint === capture.fingerprint);
1037
+ await updateState(projectDir, (state) => {
1038
+ record.revision = unchanged ? knownRevision(state, capture?.revision) : null;
1039
+ state.renders = {
1040
+ latest: record,
1041
+ retainedRevisions: retainRevision(state.renders?.retainedRevisions ?? [], record.revision)
1042
+ };
1043
+ if (outputFile.isFile() && record.revision && !state.pendingChanges)
1044
+ planArtifactCleanup(projectDir, state, record);
1045
+ });
1046
+ await updateState(projectDir, (state) => {
1047
+ finishArtifactCleanup(projectDir, state);
1048
+ });
1049
+ return record;
1050
+ }
659
1051
  var START = "start";
660
1052
  function parseRecord(line) {
661
1053
  try {
@@ -667,7 +1059,7 @@ function parseRecord(line) {
667
1059
  function readLog(file, onUnreadable) {
668
1060
  let text;
669
1061
  try {
670
- text = readFileSync5(file, "utf-8");
1062
+ text = readFileSync8(file, "utf-8");
671
1063
  } catch {
672
1064
  return null;
673
1065
  }
@@ -691,7 +1083,7 @@ function applyRecord(log, record) {
691
1083
  function saveRecord(file, log, record) {
692
1084
  let fd;
693
1085
  try {
694
- fd = openSync(file, constants2.O_WRONLY | constants2.O_APPEND);
1086
+ fd = openSync2(file, constants3.O_WRONLY | constants3.O_APPEND);
695
1087
  } catch (error) {
696
1088
  if (error.code !== "ENOENT") throw error;
697
1089
  return writeLog(file, log);
@@ -700,7 +1092,7 @@ function saveRecord(file, log, record) {
700
1092
  writeSync(fd, `${JSON.stringify(record)}
701
1093
  `);
702
1094
  } finally {
703
- closeSync(fd);
1095
+ closeSync2(fd);
704
1096
  }
705
1097
  }
706
1098
  function baselineRecord(log) {
@@ -711,30 +1103,30 @@ function baselineRecord(log) {
711
1103
  };
712
1104
  }
713
1105
  function writeLog(file, log) {
714
- const records = [
1106
+ const records2 = [
715
1107
  baselineRecord(log),
716
1108
  ...log.entries.map((entry) => ({ type: "entry", entry })),
717
1109
  ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
718
1110
  ];
719
1111
  mkdirSync3(dirname3(file), { recursive: true });
720
- replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
1112
+ replaceFileAtomically(file, records2.map((r) => `${JSON.stringify(r)}
721
1113
  `).join(""), 420);
722
1114
  }
723
- function applyEntry(manifest, entry, side) {
1115
+ function applyEntry(manifest2, entry, side) {
724
1116
  for (const file of entry.files) {
725
1117
  const hash = file[side];
726
- if (hash === null) manifest.delete(file.path);
727
- else manifest.set(file.path, hash);
1118
+ if (hash === null) manifest2.delete(file.path);
1119
+ else manifest2.set(file.path, hash);
728
1120
  }
729
1121
  }
730
1122
  function manifestAt(log, point) {
731
1123
  return point === START ? new Map(log.baseline) : manifestAround(log, point, "after");
732
1124
  }
733
1125
  function manifestAround(log, id, side) {
734
- const manifest = new Map(log.baseline);
1126
+ const manifest2 = new Map(log.baseline);
735
1127
  for (const entry of log.entries) {
736
- applyEntry(manifest, entry, entry.id === id ? side : "after");
737
- if (entry.id === id) return manifest;
1128
+ applyEntry(manifest2, entry, entry.id === id ? side : "after");
1129
+ if (entry.id === id) return manifest2;
738
1130
  }
739
1131
  return null;
740
1132
  }
@@ -787,7 +1179,7 @@ function referencedHashes(log, current) {
787
1179
  }
788
1180
  return keep;
789
1181
  }
790
- var DEFAULT_HISTORY_ROOT = join5(homedir(), ".cache", "hyperframes", "history");
1182
+ var DEFAULT_HISTORY_ROOT = join7(homedir(), ".cache", "hyperframes", "history");
791
1183
  var MAX_WINDOW_IDLE_MS = 10 * 6e4;
792
1184
  var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
793
1185
  var OUTSIDE = { kind: "outside", name: "Outside" };
@@ -800,7 +1192,7 @@ function isHistoryPath(path) {
800
1192
  if (KEPT_HIDDEN_PATHS.has(path)) return true;
801
1193
  return !path.split("/").some((segment) => segment.startsWith("."));
802
1194
  }
803
- var logFileIn = (home) => join5(home, "log.jsonl");
1195
+ var logFileIn = (home) => join7(home, "log.jsonl");
804
1196
  function listOf(log) {
805
1197
  const undone = undoneIds(log.entries);
806
1198
  return log.entries.map((entry) => ({
@@ -840,20 +1232,20 @@ async function removeEmptyFolders(dir) {
840
1232
  throw error;
841
1233
  }
842
1234
  for (const entry of entries)
843
- if (entry.isDirectory()) await removeEmptyFolders(join5(dir, entry.name));
1235
+ if (entry.isDirectory()) await removeEmptyFolders(join7(dir, entry.name));
844
1236
  await rmdir(dir);
845
1237
  }
846
1238
  function fileLeftIn(dir, at, deleted) {
847
1239
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
848
1240
  const path = `${at}/${entry.name}`;
849
- const left = entry.isDirectory() ? fileLeftIn(join5(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
1241
+ const left = entry.isDirectory() ? fileLeftIn(join7(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
850
1242
  if (left) return left;
851
1243
  }
852
1244
  return void 0;
853
1245
  }
854
1246
  function ignoresCase(dir) {
855
- const self = statSync3(join5(dir, ID_PATH), { throwIfNoEntry: false });
856
- const other = statSync3(join5(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
1247
+ const self = statSync4(join7(dir, ID_PATH), { throwIfNoEntry: false });
1248
+ const other = statSync4(join7(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
857
1249
  return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
858
1250
  }
859
1251
  var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
@@ -876,15 +1268,24 @@ function addChange(group, path, before, after) {
876
1268
  if (from === after) group.changes.delete(path);
877
1269
  else group.changes.set(path, { path, before: from, after });
878
1270
  }
1271
+ var targetKey = (target) => JSON.stringify([target.file, target.id, target.hfId, target.selector, target.selectorIndex]);
1272
+ function keepTargetsOfItsFiles(entry) {
1273
+ const targets = entry.targets?.filter(
1274
+ (target) => entry.files.some((file) => file.path === target.file)
1275
+ );
1276
+ if (targets?.length) entry.targets = targets;
1277
+ else delete entry.targets;
1278
+ return entry;
1279
+ }
879
1280
  var Engine = class {
880
1281
  constructor(options, projectId, blobs) {
881
1282
  this.options = options;
882
1283
  this.projectId = projectId;
883
1284
  this.blobs = blobs;
884
- this.dir = resolve3(options.projectDir);
885
- this.home = join5(options.historyRoot, projectId);
1285
+ this.dir = resolve6(options.projectDir);
1286
+ this.home = join7(options.historyRoot, projectId);
886
1287
  this.foldsCase = ignoresCase(this.dir);
887
- const folder = statSync3(this.dir, { throwIfNoEntry: false });
1288
+ const folder = statSync4(this.dir, { throwIfNoEntry: false });
888
1289
  if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
889
1290
  throw this.replaced();
890
1291
  this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
@@ -928,13 +1329,28 @@ var Engine = class {
928
1329
  queue(task) {
929
1330
  if (this.closed)
930
1331
  return Promise.reject(new HistoryClosedError("This project's history is closed."));
931
- const run = this.tail.then(task);
1332
+ const run = this.tail.then(async () => {
1333
+ try {
1334
+ return await task();
1335
+ } finally {
1336
+ if (this.whereFolder() === "here") {
1337
+ await writeHistoryState(
1338
+ this.dir,
1339
+ this.projectId,
1340
+ this.home,
1341
+ this.log.entries,
1342
+ this.manifest(),
1343
+ manifestAt(this.log, this.log.entries.at(-1)?.id ?? START) ?? /* @__PURE__ */ new Map()
1344
+ ).catch((error) => this.options.onError?.(error));
1345
+ }
1346
+ }
1347
+ });
932
1348
  this.tail = run.catch(() => void 0);
933
1349
  return run;
934
1350
  }
935
1351
  /** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
936
1352
  whereFolder() {
937
- const now = statSync3(this.dir, { throwIfNoEntry: false });
1353
+ const now = statSync4(this.dir, { throwIfNoEntry: false });
938
1354
  if (!now) return "gone";
939
1355
  const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
940
1356
  return same && readId(this.dir) === this.projectId ? "here" : "replaced";
@@ -947,7 +1363,7 @@ var Engine = class {
947
1363
  persistLog(record) {
948
1364
  if (record) saveRecord(this.logFile, this.log, record);
949
1365
  else writeLog(this.logFile, this.log);
950
- if (!existsSync2(join5(this.home, "project.json")))
1366
+ if (!existsSync3(join7(this.home, "project.json")))
951
1367
  recordProject(this.home, this.dir, this.folder);
952
1368
  }
953
1369
  replaced() {
@@ -986,7 +1402,7 @@ var Engine = class {
986
1402
  if (!after || !affectsProjectSignature(realDir, absPath)) return;
987
1403
  const replaced = hashOfVersion(fileContentVersion(bytes));
988
1404
  if (!replaced || replaced === after) return;
989
- const path = relative2(realDir, absPath).split(sep2).join("/");
1405
+ const path = relative4(realDir, absPath).split(sep4).join("/");
990
1406
  if (!isHistoryPath(path)) return;
991
1407
  this.overwritten.set(
992
1408
  path,
@@ -998,7 +1414,7 @@ var Engine = class {
998
1414
  });
999
1415
  }
1000
1416
  async storeBytes(bytes) {
1001
- const staged = join5(this.home, `overwrote-${randomUUID6()}`);
1417
+ const staged = join7(this.home, `overwrote-${randomUUID7()}`);
1002
1418
  await mkdir2(this.home, { recursive: true });
1003
1419
  await writeFile(staged, bytes);
1004
1420
  try {
@@ -1052,7 +1468,7 @@ var Engine = class {
1052
1468
  this.saveAdopting();
1053
1469
  }
1054
1470
  saveAdopting() {
1055
- const file = join5(this.home, "adopting.json");
1471
+ const file = join7(this.home, "adopting.json");
1056
1472
  if (!this.adopting.size) return rmSync2(file, { force: true });
1057
1473
  mkdirSync4(this.home, { recursive: true });
1058
1474
  replaceFileAtomically(file, JSON.stringify([...this.adopting.keys()]), 420);
@@ -1088,7 +1504,7 @@ var Engine = class {
1088
1504
  }
1089
1505
  readStatCache() {
1090
1506
  try {
1091
- const files = JSON.parse(readFileSync6(join5(this.home, "stat.json"), "utf-8"));
1507
+ const files = JSON.parse(readFileSync9(join7(this.home, "stat.json"), "utf-8"));
1092
1508
  return new Map(Object.entries(files));
1093
1509
  } catch {
1094
1510
  return /* @__PURE__ */ new Map();
@@ -1097,7 +1513,7 @@ var Engine = class {
1097
1513
  readAdopting() {
1098
1514
  try {
1099
1515
  return new Set(
1100
- JSON.parse(readFileSync6(join5(this.home, "adopting.json"), "utf-8"))
1516
+ JSON.parse(readFileSync9(join7(this.home, "adopting.json"), "utf-8"))
1101
1517
  );
1102
1518
  } catch {
1103
1519
  return /* @__PURE__ */ new Set();
@@ -1106,7 +1522,7 @@ var Engine = class {
1106
1522
  saveStatCache() {
1107
1523
  const files = JSON.stringify(Object.fromEntries(this.tracked));
1108
1524
  mkdirSync4(this.home, { recursive: true });
1109
- replaceFileAtomically(join5(this.home, "stat.json"), files, 420);
1525
+ replaceFileAtomically(join7(this.home, "stat.json"), files, 420);
1110
1526
  }
1111
1527
  /**
1112
1528
  * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
@@ -1148,7 +1564,7 @@ var Engine = class {
1148
1564
  /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
1149
1565
  async storeIfPresent(path) {
1150
1566
  try {
1151
- return await this.blobs.put(join5(this.dir, path));
1567
+ return await this.blobs.put(join7(this.dir, path));
1152
1568
  } catch (error) {
1153
1569
  if (error.code === "ENOENT") return null;
1154
1570
  throw error;
@@ -1174,7 +1590,7 @@ var Engine = class {
1174
1590
  addChange(window ?? this.outsideGroup(), path, before, after);
1175
1591
  }
1176
1592
  /** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
1177
- async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {} }) {
1593
+ async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {}, targets = [] }) {
1178
1594
  await this.sweep();
1179
1595
  const taken = await this.takeClaimed(who, paths, overwrote);
1180
1596
  if (!taken.length) return this.claimNothing(coalesceKey);
@@ -1183,10 +1599,19 @@ var Engine = class {
1183
1599
  await this.commitPending();
1184
1600
  const group = held ?? this.newGroup(who, label);
1185
1601
  for (const change of taken) addChange(group, change.path, change.before, change.after);
1602
+ this.addTargets(group, targets);
1186
1603
  if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
1187
1604
  const entry = await this.commit(group);
1188
1605
  return entry && { id: entry.id };
1189
1606
  }
1607
+ addTargets(group, targets) {
1608
+ const unique = new Map((group.targets ?? []).map((target) => [targetKey(target), target]));
1609
+ for (const target of targets) {
1610
+ const normalized = { ...target, file: this.logPath(target.file) };
1611
+ if (group.changes.has(normalized.file)) unique.set(targetKey(normalized), normalized);
1612
+ }
1613
+ group.targets = [...unique.values()];
1614
+ }
1190
1615
  /** A claim that took nothing still ends a held claim under another key. */
1191
1616
  async claimNothing(coalesceKey) {
1192
1617
  if (coalesceKey !== this.claimed?.key) await this.commitClaim();
@@ -1213,7 +1638,7 @@ var Engine = class {
1213
1638
  return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
1214
1639
  }
1215
1640
  logPath(path) {
1216
- return relative2(this.dir, resolve3(this.dir, path)).split(sep2).join("/");
1641
+ return relative4(this.dir, resolve6(this.dir, path)).split(sep4).join("/");
1217
1642
  }
1218
1643
  /** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
1219
1644
  async takeClaimed(who, paths, overwrote) {
@@ -1304,7 +1729,7 @@ var Engine = class {
1304
1729
  return this.outside;
1305
1730
  }
1306
1731
  newGroup(who, label) {
1307
- return { id: randomUUID6(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
1732
+ return { id: randomUUID7(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
1308
1733
  }
1309
1734
  async commitOutside() {
1310
1735
  if (this.outside?.changes.size) await this.commitClaim();
@@ -1319,7 +1744,7 @@ var Engine = class {
1319
1744
  if (!group.changes.size && !undoesEmptied) return null;
1320
1745
  const pending = this.pendingEntry(group);
1321
1746
  const endedAt = Math.max(pending.endedAt, this.log.entries.at(-1)?.endedAt ?? 0);
1322
- const entry = { ...pending, endedAt, ...extra };
1747
+ const entry = keepTargetsOfItsFiles({ ...pending, endedAt, ...extra });
1323
1748
  this.log.entries.push(entry);
1324
1749
  try {
1325
1750
  this.persistLog({ type: "entry", entry });
@@ -1374,9 +1799,9 @@ var Engine = class {
1374
1799
  }
1375
1800
  /** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
1376
1801
  noteChange(path) {
1377
- const absolute = resolve3(this.dir, path);
1802
+ const absolute = resolve6(this.dir, path);
1378
1803
  if (this.notedTimer || !affectsProjectSignature(this.dir, absolute)) return;
1379
- if (!isHistoryPath(relative2(this.dir, absolute).split(sep2).join("/"))) return;
1804
+ if (!isHistoryPath(relative4(this.dir, absolute).split(sep4).join("/"))) return;
1380
1805
  this.notedTimer = setTimeout(() => {
1381
1806
  this.notedTimer = null;
1382
1807
  this.background(() => this.sweep());
@@ -1388,9 +1813,19 @@ var Engine = class {
1388
1813
  if (!(error instanceof HistoryClosedError)) this.options.onError?.(error);
1389
1814
  });
1390
1815
  }
1391
- beginWindow(who, label, idleMs) {
1816
+ beginWindow(who, label, idleMs, exclusive = false) {
1392
1817
  return this.queue(async () => {
1393
1818
  await this.settle();
1819
+ const at = this.now();
1820
+ if (exclusive) {
1821
+ const blocking = this.windows.find(
1822
+ (open) => !sameWho(open.who, who) && takesWrite(open, at)
1823
+ );
1824
+ if (blocking)
1825
+ throw new Error(`${blocking.who.name}'s turn is open; it has to end before yours begins`);
1826
+ for (const open of [...this.windows])
1827
+ if (!takesWrite(open, at) || sameWho(open.who, who)) await this.endWindow(open);
1828
+ }
1394
1829
  const window = { ...this.newGroup(who, label), idleMs };
1395
1830
  this.windows.push(window);
1396
1831
  this.touch(window, this.now());
@@ -1442,7 +1877,7 @@ var Engine = class {
1442
1877
  const parts = path.split("/");
1443
1878
  for (let depth = 1; depth <= parts.length; depth += 1) {
1444
1879
  const at = parts.slice(0, depth).join("/");
1445
- const stat2 = lstatSync2(join5(this.dir, at), { throwIfNoEntry: false });
1880
+ const stat2 = lstatSync3(join7(this.dir, at), { throwIfNoEntry: false });
1446
1881
  if (!stat2) return void 0;
1447
1882
  if (at === path || !stat2.isDirectory()) return { at, stat: stat2 };
1448
1883
  }
@@ -1454,8 +1889,8 @@ var Engine = class {
1454
1889
  if (!standing) return void 0;
1455
1890
  if (standing.stat.isSymbolicLink()) return standing.at;
1456
1891
  if (standing.at === path && !standing.stat.isDirectory()) return void 0;
1457
- const real = realpathSync.native(join5(this.dir, standing.at));
1458
- const at = relative2(realpathSync.native(this.dir), real).split(sep2).join("/");
1892
+ const real = realpathSync.native(join7(this.dir, standing.at));
1893
+ const at = relative4(realpathSync.native(this.dir), real).split(sep4).join("/");
1459
1894
  if (standing.at !== path) return deleted.has(at) ? void 0 : at;
1460
1895
  return fileLeftIn(real, at, deleted);
1461
1896
  }
@@ -1479,7 +1914,7 @@ var Engine = class {
1479
1914
  /** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
1480
1915
  async writeProjectFile(path, hash) {
1481
1916
  this.assertWritable();
1482
- const absPath = join5(this.dir, path);
1917
+ const absPath = join7(this.dir, path);
1483
1918
  const { writeToken } = this;
1484
1919
  if (writeToken) {
1485
1920
  const version = hash === null ? DELETED_VERSION : hashVersion(hash);
@@ -1520,7 +1955,10 @@ var Engine = class {
1520
1955
  );
1521
1956
  return {
1522
1957
  ok: true,
1523
- entry: await this.writeAs(who, this.undoLabel(entry), target, { undoes: id })
1958
+ entry: await this.writeAs(who, this.undoLabel(entry), target, {
1959
+ undoes: id,
1960
+ ...entry.targets && { targets: entry.targets }
1961
+ })
1524
1962
  };
1525
1963
  }
1526
1964
  movedOn(entry) {
@@ -1544,7 +1982,15 @@ var Engine = class {
1544
1982
  pendingEntry(group) {
1545
1983
  const files = [...group.changes.values()].sort((a, b) => a.path.localeCompare(b.path));
1546
1984
  const { id, who, label, startedAt, lastWriteAt } = group;
1547
- return { id, who, label, startedAt, endedAt: lastWriteAt ?? this.now(), files };
1985
+ return {
1986
+ id,
1987
+ who,
1988
+ label,
1989
+ startedAt,
1990
+ endedAt: lastWriteAt ?? this.now(),
1991
+ files,
1992
+ ...group.targets?.length && { targets: group.targets }
1993
+ };
1548
1994
  }
1549
1995
  conflict(entry, changed) {
1550
1996
  const paths = new Set(changed.map((file) => file.path));
@@ -1579,7 +2025,12 @@ var Engine = class {
1579
2025
  api() {
1580
2026
  return {
1581
2027
  projectId: this.projectId,
1582
- beginWindow: (who, label, options = {}) => this.beginWindow(who, label, options.idleMs ?? this.options.maxGroupMs ?? 3e4),
2028
+ beginWindow: (who, label, options = {}) => this.beginWindow(
2029
+ who,
2030
+ label,
2031
+ options.idleMs ?? this.options.maxGroupMs ?? 3e4,
2032
+ options.exclusive === true
2033
+ ),
1583
2034
  claim: async (who, label, paths, options = {}) => {
1584
2035
  await Promise.all(paths.map((path) => this.adopting.get(this.logPath(path))));
1585
2036
  return this.queue(() => this.claimNow(who, label, paths, options));
@@ -1606,18 +2057,18 @@ var Engine = class {
1606
2057
  },
1607
2058
  checkout: (entryId, side, emptyDir) => this.queue(async () => {
1608
2059
  this.entry(entryId);
1609
- if (!existsSync2(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
1610
- const dest = resolve3(emptyDir);
2060
+ if (!existsSync3(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
2061
+ const dest = resolve6(emptyDir);
1611
2062
  if (isWithin(this.dir, dest))
1612
2063
  throw new Error(`Checkout writes outside the project only: ${dest}`);
1613
2064
  if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
1614
2065
  throw new Error(`Checkout writes into an empty folder only: ${dest}`);
1615
2066
  const files = manifestAround(this.log, entryId, side);
1616
2067
  try {
1617
- for (const [path, hash] of files) await this.blobs.writeTo(hash, join5(dest, path));
2068
+ for (const [path, hash] of files) await this.blobs.writeTo(hash, join7(dest, path));
1618
2069
  } catch (error) {
1619
2070
  for (const name of await readdir3(dest).catch(() => []))
1620
- await rm3(join5(dest, name), { recursive: true, force: true });
2071
+ await rm3(join7(dest, name), { recursive: true, force: true });
1621
2072
  throw error;
1622
2073
  }
1623
2074
  }),
@@ -1640,6 +2091,7 @@ var Engine = class {
1640
2091
  this.listeners.add(listener);
1641
2092
  return () => this.listeners.delete(listener);
1642
2093
  },
2094
+ checkpoint: () => this.queue(() => this.settle()),
1643
2095
  flush: () => this.queue(() => this.settleAll()),
1644
2096
  replacedAtPath: () => this.whereFolder() === "replaced",
1645
2097
  close: () => this.closing ??= (async () => {
@@ -1654,10 +2106,10 @@ var Engine = class {
1654
2106
  }
1655
2107
  };
1656
2108
  function isWithin(dir, path) {
1657
- let probe = resolve3(path);
1658
- while (!existsSync2(probe) && dirname4(probe) !== probe) probe = dirname4(probe);
1659
- const rel = relative2(realpathSync.native(dir), realpathSync.native(probe));
1660
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
2109
+ let probe = resolve6(path);
2110
+ while (!existsSync3(probe) && dirname4(probe) !== probe) probe = dirname4(probe);
2111
+ const rel = relative4(realpathSync.native(dir), realpathSync.native(probe));
2112
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute5(rel);
1661
2113
  }
1662
2114
  function missingIsEmpty(error) {
1663
2115
  if (error.code === "ENOENT") return [];
@@ -1670,16 +2122,17 @@ var HistoryClosedError = class extends Error {
1670
2122
  }
1671
2123
  };
1672
2124
  async function openProjectHistory(options) {
1673
- if (!existsSync2(options.projectDir))
2125
+ if (!existsSync3(options.projectDir))
1674
2126
  throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
1675
2127
  const projectId = projectHistoryId(options.projectDir, options.historyRoot);
1676
2128
  const release = await takeHistoryOwnership(
1677
- join5(options.historyRoot, projectId),
2129
+ join7(options.historyRoot, projectId),
1678
2130
  options.ownerWaitMs ?? 5e3
1679
2131
  );
1680
2132
  try {
1681
- const blobs = await openBlobStore(join5(options.historyRoot, projectId, "blobs"));
1682
- const engine = new Engine(options, projectId, blobs);
2133
+ const blobs = await openBlobStore(join7(options.historyRoot, projectId, "blobs"));
2134
+ const closedWindow = options.closedWindowAfterOwnership?.() ?? options.closedWindow;
2135
+ const engine = new Engine({ ...options, closedWindow }, projectId, blobs);
1683
2136
  await engine.queue(async () => {
1684
2137
  await engine.start();
1685
2138
  engine.hearWrites();
@@ -1699,9 +2152,9 @@ async function openProjectHistory(options) {
1699
2152
  }
1700
2153
  }
1701
2154
  function readProjectHistory(options) {
1702
- const dir = resolve3(options.projectDir);
2155
+ const dir = resolve6(options.projectDir);
1703
2156
  const id = readId(dir);
1704
- const home = id && isRecordedFolder(join5(options.historyRoot, id), statSync3(dir)) ? join5(options.historyRoot, id) : null;
2157
+ const home = id && isRecordedFolder(join7(options.historyRoot, id), statSync4(dir)) ? join7(options.historyRoot, id) : null;
1705
2158
  const read = home && readLog(logFileIn(home), () => {
1706
2159
  });
1707
2160
  const log = withoutHiddenPaths(read || { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() });
@@ -1710,7 +2163,7 @@ function readProjectHistory(options) {
1710
2163
  peek: (point) => peekOf(log, point),
1711
2164
  readBlob: async (hash) => {
1712
2165
  if (!home) throw new Error("That is not a history blob.");
1713
- return readBlob(join5(home, "blobs"), hash);
2166
+ return readBlob(join7(home, "blobs"), hash);
1714
2167
  }
1715
2168
  };
1716
2169
  }
@@ -1746,15 +2199,22 @@ export {
1746
2199
  clearFileWriteReceipt,
1747
2200
  identifyFileWrite,
1748
2201
  consumeFileWriteReceipt,
2202
+ HistoryBusyError,
2203
+ registerRenderOutput,
1749
2204
  STUDIO_SIGNATURE_MANIFEST_PATHS,
1750
2205
  affectsProjectSignature,
1751
2206
  resolveProjectSignature,
1752
2207
  resolveProjectAndSignature,
1753
2208
  createProjectSignature,
1754
2209
  HistoryIdError,
1755
- HistoryBusyError,
1756
2210
  listProjectHistories,
1757
2211
  pruneGoneProjectHistories,
2212
+ beginArtifactCapture,
2213
+ recordArtifact,
2214
+ recordArtifacts,
2215
+ forgetRecordedArtifacts,
2216
+ artifactDirectory,
2217
+ recordRenderSuccess,
1758
2218
  START,
1759
2219
  DEFAULT_HISTORY_ROOT,
1760
2220
  MAX_WINDOW_IDLE_MS,