hyperframes 0.8.139 → 0.8.140

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 (195) hide show
  1. package/dist/{add-VYJQ4S3P.js → add-7SPPQB64.js} +20 -22
  2. package/dist/{animatedGifPrep-IBMW6FZB.js → animatedGifPrep-HN3CJSFX.js} +15 -8
  3. package/dist/{asset-FLSUS73X.js → asset-AZMFUTRL.js} +3 -3
  4. package/dist/{assetCataloger-TGD7TRCR.js → assetCataloger-TQUWYLHE.js} +6 -3
  5. package/dist/{auth-EZXIN4M4.js → auth-XGY74R4A.js} +7 -7
  6. package/dist/{autoUpdate-ZPMY5T3X.js → autoUpdate-44NTJBQU.js} +2 -2
  7. package/dist/{backgroundChecks-JIDTAUFE.js → backgroundChecks-ET4DJEPG.js} +3 -3
  8. package/dist/backgroundChecksWorker.js +3 -3
  9. package/dist/{batchRender-FNZ6TJYY.js → batchRender-HTWWNF3P.js} +11 -8
  10. package/dist/beat-analyzer.global.js +5 -2
  11. package/dist/{beats-EAA4U3YN.js → beats-4GYTZUZG.js} +24 -17
  12. package/dist/{benchmark-KKZQVK2Y.js → benchmark-CJQHE3QN.js} +20 -23
  13. package/dist/{browser-ZIVVJLJT.js → browser-NXOVPGDG.js} +14 -28
  14. package/dist/{bundleWithLocalizedFonts-2XWC3PEM.js → bundleWithLocalizedFonts-6PJP54TC.js} +5 -5
  15. package/dist/{capture-MKJHABUL.js → capture-5EHGRJZM.js} +2 -2
  16. package/dist/{capture-EFYPXI42.js → capture-EHOMO4MP.js} +22 -15
  17. package/dist/{catalog-EWX4BBI2.js → catalog-PTX5Z52R.js} +28 -30
  18. package/dist/{catch-up-FJFH7T4W.js → catch-up-I45SPSCU.js} +11 -25
  19. package/dist/{check-YJNVDCBP.js → check-LJ7LG7OI.js} +72 -54
  20. package/dist/{checkBrowser-I6SPPEGD.js → checkBrowser-6EK5E4PG.js} +81 -36
  21. package/dist/chunk-24OWOBQH.js +3656 -0
  22. package/dist/{chunk-SIKMUO45.js → chunk-2GMKWYEH.js} +12 -12
  23. package/dist/{chunk-GIDHR5OL.js → chunk-5IKJONRY.js} +1 -1
  24. package/dist/{chunk-GWV2Y36Q.js → chunk-6U3MN4HW.js} +5 -106
  25. package/dist/chunk-A4KQZXZI.js +116 -0
  26. package/dist/{chunk-PFI3VD77.js → chunk-ABJNXQUA.js} +10 -10
  27. package/dist/{chunk-HOIHGPWO.js → chunk-ASW6NWON.js} +1 -1
  28. package/dist/chunk-B3I3XJ47.js +3711 -0
  29. package/dist/{chunk-3RT6D4P6.js → chunk-BJ5JWOES.js} +1 -1
  30. package/dist/{chunk-4VZBKOQJ.js → chunk-BQKEGPPL.js} +1 -1
  31. package/dist/{chunk-SHB5VDMH.js → chunk-BRITLL56.js} +1 -1
  32. package/dist/{chunk-7SI7GCX6.js → chunk-C2JUHIG5.js} +6 -6
  33. package/dist/{chunk-TCWL4BKK.js → chunk-DDQ4LOML.js} +1 -1
  34. package/dist/{chunk-CDJ7FPPJ.js → chunk-DQNOPYAG.js} +1 -1
  35. package/dist/{chunk-JH556Q42.js → chunk-EDYBFSPE.js} +2 -2
  36. package/dist/{chunk-W7SAWBQC.js → chunk-F3SPK6SO.js} +1 -1
  37. package/dist/chunk-FXMKHHPO.js +123 -0
  38. package/dist/chunk-G75ENCEZ.js +283 -0
  39. package/dist/{chunk-PLNWWGE7.js → chunk-GQJZORQP.js} +1 -1
  40. package/dist/{chunk-Y6W4DVPS.js → chunk-HKCNZITN.js} +1 -1
  41. package/dist/{chunk-U3GEXWI5.js → chunk-IFY57LUW.js} +1 -1
  42. package/dist/{chunk-UQXOHEQ7.js → chunk-IRBESGJ7.js} +4 -4
  43. package/dist/chunk-JFKL3WAE.js +161 -0
  44. package/dist/{chunk-25DHUDOK.js → chunk-JSGANFFH.js} +12 -3
  45. package/dist/{chunk-UGI77E35.js → chunk-KECV5G4V.js} +165 -59
  46. package/dist/{chunk-EZ2P7LAP.js → chunk-KFQNMY36.js} +8 -8
  47. package/dist/{chunk-FLRCVYNE.js → chunk-KJLY2SDJ.js} +2 -2
  48. package/dist/{chunk-XOZ3J3LB.js → chunk-KMKWRIMC.js} +1 -1
  49. package/dist/{chunk-O6K5DWXM.js → chunk-KOEWEFHK.js} +1 -1
  50. package/dist/{chunk-IBRZUVTH.js → chunk-L2H2L22P.js} +2 -2
  51. package/dist/{chunk-BC3OY2ED.js → chunk-LQG6GPVK.js} +1 -1
  52. package/dist/{chunk-Z67RZ4XO.js → chunk-MHK5DE2P.js} +263 -1960
  53. package/dist/{chunk-A4X2YWNQ.js → chunk-MTVDXB6S.js} +18 -289
  54. package/dist/{chunk-CDMB3PRC.js → chunk-MU6C2VP6.js} +7 -7
  55. package/dist/{chunk-UMWROSIP.js → chunk-MUINFURL.js} +2 -2
  56. package/dist/{chunk-JTAYFSLO.js → chunk-PB3P4MKP.js} +1 -1
  57. package/dist/{chunk-JIIIBTN2.js → chunk-PE2I3AFA.js} +1 -1
  58. package/dist/{chunk-OENM2JVZ.js → chunk-PFJFHNU3.js} +3 -3
  59. package/dist/{chunk-EH4VUY27.js → chunk-PPKFM5WE.js} +2 -2
  60. package/dist/{chunk-DW32KBHN.js → chunk-PUBKHEEA.js} +13 -13
  61. package/dist/{chunk-DUQJF7FK.js → chunk-QBK7WKHT.js} +8 -8
  62. package/dist/{chunk-U3HX7WE6.js → chunk-QG3PDRSO.js} +1 -1
  63. package/dist/{chunk-EJ7OWYF2.js → chunk-QP3BRKGF.js} +1 -163
  64. package/dist/chunk-QZ3AZTXI.js +1740 -0
  65. package/dist/{chunk-YOL4QRSL.js → chunk-SLEPCESN.js} +2 -2
  66. package/dist/chunk-T76PLPD6.js +57 -0
  67. package/dist/{chunk-QXOFUKBF.js → chunk-TPECLKPA.js} +168 -165
  68. package/dist/{chunk-R7LWDIKB.js → chunk-TYT2TLJR.js} +1 -1
  69. package/dist/{chunk-Q73ISOVQ.js → chunk-U7E736AJ.js} +1 -1
  70. package/dist/chunk-USDEMXXL.js +110 -0
  71. package/dist/{chunk-VTVS4IIP.js → chunk-VVJFZU6B.js} +2 -2
  72. package/dist/{chunk-JX7Y57YY.js → chunk-WKTHUC2R.js} +1 -1
  73. package/dist/{chunk-NTIOLSFE.js → chunk-WQBA3MH4.js} +3 -3
  74. package/dist/{chunk-STA43FMY.js → chunk-WY5OODN5.js} +71 -3855
  75. package/dist/{chunk-X3TMMIDP.js → chunk-X3X3VETN.js} +8 -8
  76. package/dist/{chunk-ZTZ7NCJT.js → chunk-XTFJOR3K.js} +1 -1
  77. package/dist/chunk-YA62XGXS.js +45 -0
  78. package/dist/{clean-P4KTCOUA.js → clean-Y3AQITOX.js} +29 -20
  79. package/dist/cli.js +55 -55
  80. package/dist/{cloud-PDY7S4HU.js → cloud-TDPOMBZF.js} +7 -7
  81. package/dist/{cloudrun-54MA2W5H.js → cloudrun-FPAVAVHP.js} +11 -8
  82. package/dist/commands/layout-audit.browser.js +37 -8
  83. package/dist/commands/motion-signature.browser.js +51 -2
  84. package/dist/{compare-LDWVSPC5.js → compare-LHL225ND.js} +40 -31
  85. package/dist/{compiler-CWSZ4W7J.js → compiler-RTU3KTLF.js} +7 -3
  86. package/dist/{component-FIEGC22U.js → component-L2YX7J56.js} +4 -4
  87. package/dist/{compositions-JR7PX356.js → compositions-V2UZKG7S.js} +23 -16
  88. package/dist/{delete-FXA2IHOS.js → delete-OXZEXUQY.js} +8 -8
  89. package/dist/{dist-4JZWRWS6.js → dist-N33CM63P.js} +21 -15
  90. package/dist/{dist-OJTEVNJR.js → dist-NZSQCYOF.js} +3 -1
  91. package/dist/{doctor-OTDHPM6K.js → doctor-MWTPSIVD.js} +20 -13
  92. package/dist/{events-UAFTI24U.js → events-4TZGQZWF.js} +6 -22
  93. package/dist/{events-6T7M2VCO.js → events-7VJJGC3D.js} +10 -23
  94. package/dist/{feedback-ZGZODHB6.js → feedback-HSGAPVN7.js} +67 -45
  95. package/dist/{figma-D26EJ7ZB.js → figma-AMC6FAK3.js} +6 -6
  96. package/dist/fontLocalizeCli.js +17 -10
  97. package/dist/{get-IAREU7IK.js → get-Y7DFEFYM.js} +8 -8
  98. package/dist/{gpuPolicy-NS7WZRDJ.js → gpuPolicy-A45VSUKR.js} +15 -8
  99. package/dist/{grade-compare-ZXHFHCD2.js → grade-compare-ERSA3OUI.js} +40 -31
  100. package/dist/{help-RVNYHW3N.js → help-TW2AADLZ.js} +4 -4
  101. package/dist/{history-THP4VNMF.js → history-DKGDSVWF.js} +12 -30
  102. package/dist/hyperframe-runtime.js +59 -59
  103. package/dist/hyperframe.manifest.json +1 -1
  104. package/dist/hyperframe.runtime.iife.js +59 -59
  105. package/dist/hyperframes-player.global.js +3 -3
  106. package/dist/{info-EOFRHN5A.js → info-I3NRRNX4.js} +17 -20
  107. package/dist/{init-YLIJE4CG.js → init-5Y3ZWZNF.js} +48 -41
  108. package/dist/{inspect-3D56ONJD.js → inspect-BJQKBB3X.js} +39 -30
  109. package/dist/{installer-GRR2GDZK.js → installer-27K4JS2I.js} +8 -5
  110. package/dist/{keyframes-NXNCTQPK.js → keyframes-6V2NZO22.js} +14 -25
  111. package/dist/{lambda-VGRZD7TR.js → lambda-AIMF3K4A.js} +16 -13
  112. package/dist/{layout-Z5DOS6DU.js → layout-YAPEBTU4.js} +39 -30
  113. package/dist/{lint-YD6NJGR2.js → lint-3LWWUIV7.js} +17 -23
  114. package/dist/{lintProject-ANMPLBIT.js → lintProject-LM3XZRPZ.js} +3 -1
  115. package/dist/{list-XGDKILYN.js → list-NTSGEZJ6.js} +8 -8
  116. package/dist/{login-GD5SA4RD.js → login-ZTDFGCCW.js} +9 -9
  117. package/dist/{logout-Q654A3SI.js → logout-GJXZQKOA.js} +5 -5
  118. package/dist/{media-treatment-TY4X4GQQ.js → media-treatment-ARHX446F.js} +21 -20
  119. package/dist/{models-MXRDH4WJ.js → models-VHNQVTHG.js} +19 -12
  120. package/dist/{motionShot-6LKR3C2E.js → motionShot-35HCSNCI.js} +22 -15
  121. package/dist/{normalize-O76AC4HE.js → normalize-IGLHCWTF.js} +0 -4
  122. package/dist/{normalize-audio-5ADDZKPJ.js → normalize-audio-AUC73JH3.js} +13 -26
  123. package/dist/{open-JXAMO325.js → open-C5LVNU3U.js} +11 -25
  124. package/dist/{pipeline-Q5MOPIIX.js → pipeline-4GDTK54T.js} +15 -8
  125. package/dist/{play-3SPH5GWZ.js → play-U7UANF7Z.js} +38 -32
  126. package/dist/{preflight-2PWAPKWC.js → preflight-Y5DN5D37.js} +3 -18
  127. package/dist/{present-ZTJQDKXZ.js → present-X6V6TX3E.js} +32 -26
  128. package/dist/{preview-4A3XZR6J.js → preview-K43RTRU2.js} +33 -26
  129. package/dist/{producer-5ZKKKQPB.js → producer-PVMFMW23.js} +1 -1
  130. package/dist/{projectConfig-WA4CS4SN.js → projectConfig-PI6MEN2T.js} +10 -7
  131. package/dist/{publication-CLG2FRRI.js → publication-67C7KVTL.js} +6 -3
  132. package/dist/{publish-YTT2WMXO.js → publish-RBV7EAMH.js} +27 -20
  133. package/dist/{refresh-K24ETHMA.js → refresh-MFITBIKV.js} +5 -5
  134. package/dist/{remove-background-W35INR3B.js → remove-background-Z2ECZX2Q.js} +22 -15
  135. package/dist/{render-7A4P5WMU.js → render-2GQMQBU3.js} +10 -7
  136. package/dist/{render-KICXUPNR.js → render-EFU4BZRZ.js} +23 -26
  137. package/dist/{render-WUBFQMAH.js → render-YBS3XGRJ.js} +52 -32
  138. package/dist/{render-batch-N7EFGE6S.js → render-batch-QC64OTB5.js} +11 -8
  139. package/dist/{renderCancellation-LYQPPFAO.js → renderCancellation-NTVU7IMB.js} +15 -8
  140. package/dist/renderSetupWorker.js +4 -2
  141. package/dist/{resolver-YTT2NENF.js → resolver-GBWYG3EI.js} +8 -5
  142. package/dist/{sherpa-353MW4UV.js → sherpa-ZW6PWJS4.js} +16 -9
  143. package/dist/skills/hyperframes/references/capability-menu.md +1 -1
  144. package/dist/skills/hyperframes-cli/SKILL.md +5 -0
  145. package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +1 -1
  146. package/dist/skills/hyperframes-cli/references/preview-render.md +1 -1
  147. package/dist/skills/media-use/SKILL.md +6 -4
  148. package/dist/skills/media-use/audio/scripts/lib/host-audio.mjs +28 -0
  149. package/dist/skills/media-use/audio/scripts/lib/host-audio.test.mjs +106 -0
  150. package/dist/skills/media-use/references/setup-providers.md +3 -2
  151. package/dist/skills/media-use/scripts/lib/heygen-cli.mjs +21 -1
  152. package/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +14 -0
  153. package/dist/skills/media-use/scripts/resolve.mjs +11 -6
  154. package/dist/{skills-2XJIOEBT.js → skills-VKZAX5AS.js} +15 -30
  155. package/dist/{skillsUpdateCheck-LMCCWGNO.js → skillsUpdateCheck-PAY2OPDE.js} +3 -3
  156. package/dist/{snapshot-WBRSTLJ2.js → snapshot-SD5R3NWX.js} +41 -32
  157. package/dist/{src-YWFJILQF.js → src-7SSERHNP.js} +19 -11
  158. package/dist/{src-ZS3BLTZ5.js → src-RXTXL3VL.js} +16 -9
  159. package/dist/{staticProjectServer-4VKVHPNG.js → staticProjectServer-XELJXX4J.js} +24 -16
  160. package/dist/{status-73MBN37B.js → status-JA6SKQVH.js} +7 -24
  161. package/dist/studio/assets/{hyperframes-player-CckOoCJ8.js → hyperframes-player-BLuyxnK7.js} +1 -1
  162. package/dist/studio/assets/{index-DsEkE5_G.js → index-CWfuDCoA.js} +1 -1
  163. package/dist/studio/assets/{index-BmIPrb8I.js → index-Dr3Bo9uH.js} +1 -1
  164. package/dist/studio/assets/{index-Br1Whc0f.js → index-VAHDM_zL.js} +204 -204
  165. package/dist/studio/{chunk-E422JDF5.js → chunk-H2DUOXTF.js} +2 -1
  166. package/dist/studio/index.d.ts +19 -3
  167. package/dist/studio/index.html +1 -1
  168. package/dist/studio/index.js +6498 -5813
  169. package/dist/studio/index.js.map +1 -1
  170. package/dist/studio/player.js +1 -1
  171. package/dist/{studio-api-5V4TVSQV.js → studio-api-4FNUZPJ4.js} +40 -28
  172. package/dist/{studioServer-M5XY3HYC.js → studioServer-GHLL3F2M.js} +99 -68
  173. package/dist/{system-RCRNX5VF.js → system-OQLOGGPJ.js} +2 -19
  174. package/dist/{telemetry-LOBRQGUD.js → telemetry-7AO77PMH.js} +8 -23
  175. package/dist/{timeline-3JCP6PXU.js → timeline-6LTHSRXM.js} +32 -22
  176. package/dist/{tokens-MLMIJBVF.js → tokens-K6B46KNL.js} +3 -3
  177. package/dist/{transcribe-KNFXDEAX.js → transcribe-CLLISRLZ.js} +16 -9
  178. package/dist/{transcribe-POH25ZPX.js → transcribe-K2OU6FZF.js} +20 -44
  179. package/dist/{updateCheck-5ITBSBST.js → updateCheck-X6UQBS6I.js} +2 -2
  180. package/dist/{upgrade-K6OK4FKT.js → upgrade-BZFKEADI.js} +5 -5
  181. package/dist/{urlDownloader-TJJQKADT.js → urlDownloader-VAVUXYEQ.js} +14 -7
  182. package/dist/{validate-PPPOBJHB.js → validate-VO5KEXWN.js} +26 -19
  183. package/dist/{video-OB22HWXZ.js → video-JV6Y4UJR.js} +18 -11
  184. package/package.json +1 -1
  185. package/dist/chunk-HYYSJI5D.js +0 -3742
  186. package/dist/{chunk-I465DBM6.js → chunk-246MOR5U.js} +3 -3
  187. package/dist/{deploy-MX76RZ36.js → deploy-5G4Z6LPA.js} +1 -1
  188. package/dist/{destroy-H2TZBYHH.js → destroy-GFQLTIY3.js} +1 -1
  189. package/dist/{docs-QAPJEGLS.js → docs-MPOM3PHH.js} +3 -3
  190. package/dist/{policies-FSDZVS2E.js → policies-4FEXMXPK.js} +3 -3
  191. package/dist/{progress-FBIEEB7Y.js → progress-5AM2MZ7V.js} +3 -3
  192. package/dist/{sites-TNVKZS73.js → sites-HZMADOA3.js} +1 -1
  193. package/dist/studio/{chunk-E422JDF5.js.map → chunk-H2DUOXTF.js.map} +0 -0
  194. package/dist/{telemetry-2YC4SGQU.js → telemetry-W5AKPI4R.js} +3 -3
  195. package/dist/{tts-FU35O6YB.js → tts-TOHSPWEG.js} +4 -4
@@ -0,0 +1,1740 @@
1
+ import { createRequire as __hf_createRequire } from "node:module";
2
+ import { fileURLToPath as __hf_fileURLToPath } from "node:url";
3
+ import { dirname as __hf_dirname } from "node:path";
4
+ var require = __hf_createRequire(import.meta.url);
5
+ var __filename = __hf_fileURLToPath(import.meta.url);
6
+ var __dirname = __hf_dirname(__filename);
7
+ import {
8
+ realFilePath
9
+ } from "./chunk-G75ENCEZ.js";
10
+ import {
11
+ atomicTempPath,
12
+ isAtomicTempPath,
13
+ replaceFileAtomically
14
+ } from "./chunk-EOVYQ3IG.js";
15
+ import {
16
+ isSafePath,
17
+ mkdirWithinProject
18
+ } from "./chunk-OFQWTKPW.js";
19
+
20
+ // ../studio-server/dist/chunk-K3H6HTVC.js
21
+ import { randomUUID as randomUUID6 } from "crypto";
22
+ import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
23
+ import {
24
+ existsSync as existsSync2,
25
+ lstatSync as lstatSync2,
26
+ statSync as statSync3,
27
+ mkdirSync as mkdirSync4,
28
+ readdirSync as readdirSync3,
29
+ readFileSync as readFileSync6,
30
+ realpathSync,
31
+ rmSync as rmSync2
32
+ } from "fs";
33
+ 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 { createHash, randomUUID } from "crypto";
36
+ 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";
40
+ import { constants, createReadStream, renameSync } from "fs";
41
+ import { copyFile, mkdir, readdir, readFile, rename, rm, stat } from "fs/promises";
42
+ import { dirname, join } from "path";
43
+ 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";
45
+ import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
46
+ import { tmpdir } from "os";
47
+ 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
+ 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 { dirname as dirname3 } from "path";
56
+ var RECEIPT_TTL_MS = 1e4;
57
+ var receipts = /* @__PURE__ */ new Map();
58
+ var overwriteListeners = /* @__PURE__ */ new Set();
59
+ function fileContentVersion(content) {
60
+ return hashVersion(createHash("sha256").update(content).digest("hex"));
61
+ }
62
+ var DELETED_VERSION = '"deleted"';
63
+ function hashVersion(hex) {
64
+ return `"sha256:${hex}"`;
65
+ }
66
+ function hashOfVersion(version) {
67
+ return /^"sha256:([0-9a-f]{64})"$/.exec(version)?.[1];
68
+ }
69
+ function settledFileTag(stat2, now = Date.now()) {
70
+ if (now - stat2.ctimeMs < 3e3) return null;
71
+ return [stat2.ino, stat2.ctimeMs, stat2.size].map((n) => n.toString(36)).join("-");
72
+ }
73
+ function createWriteToken(requestToken) {
74
+ const token = requestToken?.trim();
75
+ return token && token.length <= 200 ? token : randomUUID();
76
+ }
77
+ function onFileOverwritten(listener) {
78
+ overwriteListeners.add(listener);
79
+ return () => overwriteListeners.delete(listener);
80
+ }
81
+ function recordFileWriteReceipt(filePath, {
82
+ overwrote,
83
+ path,
84
+ version,
85
+ writeToken
86
+ }) {
87
+ const receipt = { path, version, writeToken };
88
+ const absPath = realFilePath(filePath);
89
+ if (overwrote !== void 0)
90
+ for (const listener of overwriteListeners) listener(absPath, receipt.version, overwrote);
91
+ const now = Date.now();
92
+ for (const [path2, list] of receipts) {
93
+ const live = list.filter((entry) => now - entry.recordedAt < RECEIPT_TTL_MS);
94
+ if (live.length > 0) receipts.set(path2, live);
95
+ else receipts.delete(path2);
96
+ }
97
+ const from = overwrote === void 0 ? void 0 : fileContentVersion(overwrote);
98
+ receipts.set(absPath, [
99
+ ...receipts.get(absPath) ?? [],
100
+ { ...receipt, ...from && { from }, recordedAt: now }
101
+ ]);
102
+ }
103
+ function clearFileWriteReceipt(filePath, version, writeToken) {
104
+ const absPath = realFilePath(filePath);
105
+ const current = (receipts.get(absPath) ?? []).filter(
106
+ (entry) => entry.version !== version || entry.writeToken !== writeToken
107
+ );
108
+ if (current.length > 0) receipts.set(absPath, current);
109
+ else receipts.delete(absPath);
110
+ }
111
+ function identifyFileWrite(filePath, expectedVersion) {
112
+ const receipt = newestReceipt(realFilePath(filePath), expectedVersion);
113
+ if (!receipt) return null;
114
+ const { path, version, writeToken, from } = receipt;
115
+ return { path, version, writeToken, ...from && { from } };
116
+ }
117
+ function newestReceipt(absPath, expectedVersion) {
118
+ const now = Date.now();
119
+ const current = (receipts.get(absPath) ?? []).filter(
120
+ (entry) => now - entry.recordedAt < RECEIPT_TTL_MS
121
+ );
122
+ if (current.length > 0) receipts.set(absPath, current);
123
+ else receipts.delete(absPath);
124
+ let receipt;
125
+ for (let i = current.length - 1; i >= 0 && !receipt; i -= 1) {
126
+ if (current[i]?.version === expectedVersion) receipt = current[i];
127
+ }
128
+ return receipt;
129
+ }
130
+ var consumeFileWriteReceipt = identifyFileWrite;
131
+ var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
132
+ ".cjs",
133
+ ".css",
134
+ ".html",
135
+ ".js",
136
+ ".json",
137
+ ".jsx",
138
+ ".mjs",
139
+ ".svg",
140
+ ".ts",
141
+ ".tsx"
142
+ ]);
143
+ var SIGNATURE_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
144
+ ".cache",
145
+ ".git",
146
+ ".hyperframes",
147
+ ".next",
148
+ ".thumbnails",
149
+ ".transcode-cache",
150
+ ".vite",
151
+ ".waveform-cache",
152
+ "build",
153
+ "coverage",
154
+ "dist",
155
+ "node_modules",
156
+ "outputs",
157
+ "renders"
158
+ ]);
159
+ var MAX_SIGNATURE_TEXT_BYTES = 2e6;
160
+ var STUDIO_SIGNATURE_MANIFEST_PATHS = [
161
+ ".hyperframes/studio-manual-edits.json",
162
+ ".hyperframes/studio-motion.json"
163
+ ];
164
+ function affectsProjectSignature(projectDir, changedPath) {
165
+ const relativePath = relative(resolve(projectDir), resolve(changedPath));
166
+ if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath) || isAtomicTempPath(relativePath)) {
167
+ return false;
168
+ }
169
+ const segments = relativePath.split(sep);
170
+ if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
171
+ return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment));
172
+ }
173
+ var projectSignatureCache = /* @__PURE__ */ new Map();
174
+ var COARSEST_FILE_TIME_TICK_MS = 2e3;
175
+ function isPathWithin(parentDir, childPath) {
176
+ const childRelativePath = relative(parentDir, childPath);
177
+ return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
178
+ }
179
+ function isTextContentEligible(file, size) {
180
+ return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
181
+ }
182
+ var isSkippedEntry = (entry) => SIGNATURE_EXCLUDED_DIRS.has(entry) || isAtomicTempPath(entry);
183
+ function collectProjectSignatureFiles(projectDir, dir, files) {
184
+ let entries;
185
+ try {
186
+ entries = readdirSync(dir).sort();
187
+ } catch {
188
+ return;
189
+ }
190
+ 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;
201
+ if (stat2.isDirectory()) {
202
+ collectProjectSignatureFiles(projectDir, file, files);
203
+ } else if (stat2.isFile()) {
204
+ files.push({
205
+ file,
206
+ mtimeMs: stat2.mtimeMs,
207
+ ctimeMs: stat2.ctimeMs,
208
+ size: stat2.size,
209
+ textContentEligible: isTextContentEligible(file, stat2.size)
210
+ });
211
+ }
212
+ }
213
+ }
214
+ function collectProjectSignatureManifestFiles(projectDir, files) {
215
+ const seen = new Set(files.map((entry) => entry.file));
216
+ for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
217
+ const file = resolve(projectDir, manifestPath);
218
+ if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
219
+ let stat2;
220
+ try {
221
+ stat2 = lstatSync(file);
222
+ } catch {
223
+ continue;
224
+ }
225
+ if (stat2.isSymbolicLink() || !stat2.isFile()) continue;
226
+ files.push({
227
+ file,
228
+ mtimeMs: stat2.mtimeMs,
229
+ ctimeMs: stat2.ctimeMs,
230
+ size: stat2.size,
231
+ textContentEligible: isTextContentEligible(file, stat2.size)
232
+ });
233
+ seen.add(file);
234
+ }
235
+ }
236
+ function createProjectFingerprint(projectDir, files) {
237
+ const hash = createHash2("sha256");
238
+ for (const entry of files) {
239
+ hash.update(relative(projectDir, entry.file));
240
+ hash.update("\0");
241
+ hash.update(String(entry.size));
242
+ hash.update("\0");
243
+ hash.update(String(entry.mtimeMs));
244
+ hash.update("\0");
245
+ hash.update(String(entry.ctimeMs));
246
+ hash.update("\0");
247
+ hash.update(entry.textContentEligible ? "text" : "binary");
248
+ hash.update("\0");
249
+ }
250
+ return hash.digest("hex").slice(0, 24);
251
+ }
252
+ function collectProjectFiles(normalizedProjectDir) {
253
+ const collected = [];
254
+ collectProjectSignatureFiles(normalizedProjectDir, normalizedProjectDir, collected);
255
+ collectProjectSignatureManifestFiles(normalizedProjectDir, collected);
256
+ return collected;
257
+ }
258
+ function listProjectFiles(projectDir) {
259
+ const normalizedProjectDir = resolve(projectDir);
260
+ return collectProjectFiles(normalizedProjectDir).map((entry) => ({
261
+ path: relative(normalizedProjectDir, entry.file).split(sep).join("/"),
262
+ size: entry.size,
263
+ mtimeMs: entry.mtimeMs,
264
+ ctimeMs: entry.ctimeMs
265
+ }));
266
+ }
267
+ function resolveProjectSignature(adapter, projectDir) {
268
+ return adapter.getProjectSignature?.(projectDir) ?? createProjectSignature(projectDir);
269
+ }
270
+ async function resolveProjectAndSignature(adapter, projectId) {
271
+ const project = await adapter.resolveProject(projectId);
272
+ if (!project) return null;
273
+ return { project, signature: resolveProjectSignature(adapter, project.dir) };
274
+ }
275
+ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
276
+ const normalizedProjectDir = resolve(projectDir);
277
+ const signedAt = Date.now();
278
+ const collected = collectProjectFiles(normalizedProjectDir);
279
+ const files = collected.filter(
280
+ (entry) => !excluding.has(relative(normalizedProjectDir, entry.file).split(sep).join("/"))
281
+ );
282
+ files.sort((a, b) => a.file.localeCompare(b.file));
283
+ const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
284
+ const cacheKey = excluding.size ? `${normalizedProjectDir}\0${createHash2("sha256").update([...excluding].sort().join("\0")).digest("hex")}` : normalizedProjectDir;
285
+ const cached = projectSignatureCache.get(cacheKey);
286
+ if (cached?.fingerprint === fingerprint) return cached.signature;
287
+ const hash = createHash2("sha256");
288
+ for (const entry of files) {
289
+ const relativePath = relative(normalizedProjectDir, entry.file);
290
+ hash.update(relativePath);
291
+ hash.update("\0");
292
+ hash.update(String(entry.size));
293
+ hash.update("\0");
294
+ if (entry.textContentEligible) {
295
+ try {
296
+ hash.update(readFileSync(entry.file));
297
+ } catch {
298
+ hash.update(String(entry.mtimeMs));
299
+ }
300
+ } else {
301
+ hash.update(String(entry.mtimeMs));
302
+ }
303
+ hash.update("\0");
304
+ }
305
+ const signature = hash.digest("hex").slice(0, 24);
306
+ const settledATickBeforeSigning = files.every(
307
+ (entry) => Math.max(entry.mtimeMs, entry.ctimeMs) < signedAt - COARSEST_FILE_TIME_TICK_MS
308
+ );
309
+ if (settledATickBeforeSigning) projectSignatureCache.set(cacheKey, { fingerprint, signature });
310
+ return signature;
311
+ }
312
+ var BLOB_HASH = /^[0-9a-f]{64}$/;
313
+ async function hashFile(path) {
314
+ const hash = createHash3("sha256");
315
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
316
+ return hash.digest("hex");
317
+ }
318
+ async function cloneOrCopy(from, to, beforeReplace) {
319
+ await mkdir(dirname(to), { recursive: true });
320
+ const temp = atomicTempPath(to);
321
+ try {
322
+ await copyFile(from, temp, constants.COPYFILE_FICLONE);
323
+ beforeReplace?.();
324
+ renameSync(temp, to);
325
+ } catch (error) {
326
+ await rm(temp, { force: true });
327
+ throw error;
328
+ }
329
+ }
330
+ async function openBlobStore(dir) {
331
+ const sizes = /* @__PURE__ */ new Map();
332
+ await mkdir(dir, { recursive: true });
333
+ for (const shard of await readdir(dir))
334
+ for (const hash of await readdir(join(dir, shard)).catch(() => []))
335
+ sizes.set(hash, (await stat(join(dir, shard, hash))).size);
336
+ const pathOf = (hash) => {
337
+ if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
338
+ return join(dir, hash.slice(0, 2), hash);
339
+ };
340
+ let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
341
+ return {
342
+ async put(absPath) {
343
+ const temp = join(dir, `incoming-${randomUUID2()}`);
344
+ await mkdir(dir, { recursive: true });
345
+ await copyFile(absPath, temp, constants.COPYFILE_FICLONE);
346
+ const hash = await hashFile(temp);
347
+ if (sizes.has(hash)) {
348
+ await rm(temp, { force: true });
349
+ return hash;
350
+ }
351
+ await mkdir(dirname(pathOf(hash)), { recursive: true });
352
+ const size = (await stat(temp)).size;
353
+ await rename(temp, pathOf(hash));
354
+ sizes.set(hash, size);
355
+ total += size;
356
+ return hash;
357
+ },
358
+ has: (hash) => sizes.has(hash),
359
+ read: async (hash) => readFile(pathOf(hash)),
360
+ writeTo: async (hash, absPath, beforeReplace) => cloneOrCopy(pathOf(hash), absPath, beforeReplace),
361
+ bytes: () => total,
362
+ size: (hash) => sizes.get(hash) ?? 0,
363
+ async prune(keep) {
364
+ for (const [hash, size] of sizes) {
365
+ if (keep.has(hash)) continue;
366
+ await rm(pathOf(hash), { force: true });
367
+ sizes.delete(hash);
368
+ total -= size;
369
+ }
370
+ }
371
+ };
372
+ }
373
+ var ID_PATH = join2(".hyperframes", "history-id");
374
+ var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
375
+ var isHistoryId = (name) => ID_SHAPE.test(name);
376
+ function readId(projectDir) {
377
+ try {
378
+ const id = readFileSync2(join2(projectDir, ID_PATH), "utf-8").trim();
379
+ return ID_SHAPE.test(id) ? id : null;
380
+ } catch {
381
+ return null;
382
+ }
383
+ }
384
+ var sameFolder = (a, b) => a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
385
+ function isRecordedFolder(historyDir, folder) {
386
+ const was = readRecord(historyDir);
387
+ if (typeof was?.ino === "number")
388
+ return sameFolder({ ino: was.ino, birthtimeMs: was.born ?? NaN }, folder);
389
+ const unreadableSo0878KeptTheId = typeof was?.dir !== "string";
390
+ if (unreadableSo0878KeptTheId) return true;
391
+ return !isCopyOf0878Folder(was.dir, basename(historyDir), folder);
392
+ }
393
+ function readRecord(historyDir) {
394
+ try {
395
+ return JSON.parse(readFileSync2(join2(historyDir, "project.json"), "utf-8"));
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+ function isCopyOf0878Folder(recordedDir, id, folder) {
401
+ let there;
402
+ try {
403
+ there = statSync(recordedDir, { throwIfNoEntry: false });
404
+ } catch {
405
+ return false;
406
+ }
407
+ return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
408
+ }
409
+ function projectHistoryId(projectDir, historyRoot) {
410
+ const dir = resolve2(projectDir);
411
+ const folder = statSync(dir);
412
+ let id = readId(dir);
413
+ if (!id && existsSync(join2(dir, ID_PATH)))
414
+ throw new HistoryIdError(
415
+ `${join2(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
416
+ );
417
+ if (!id || existsSync(join2(historyRoot, id)) && !isRecordedFolder(join2(historyRoot, id), folder)) {
418
+ id = randomUUID3();
419
+ mkdirWithinProject(dir, join2(dir, ".hyperframes"));
420
+ writeFileSync(join2(dir, ID_PATH), `${id}
421
+ `);
422
+ }
423
+ recordProject(join2(historyRoot, id), dir, folder);
424
+ return id;
425
+ }
426
+ var HistoryIdError = class extends Error {
427
+ constructor(message) {
428
+ super(message);
429
+ this.name = "HistoryIdError";
430
+ }
431
+ };
432
+ function recordProject(historyDir, dir, folder) {
433
+ const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
434
+ mkdirSync(historyDir, { recursive: true });
435
+ replaceFileAtomically(join2(historyDir, "project.json"), JSON.stringify(record), 420);
436
+ }
437
+ var HistoryBusyError = class extends Error {
438
+ constructor(pid) {
439
+ super(`This project's history is open in another process (pid ${pid}).`);
440
+ this.pid = pid;
441
+ this.name = "HistoryBusyError";
442
+ }
443
+ pid;
444
+ };
445
+ function alive(pid) {
446
+ if (!Number.isInteger(pid) || pid <= 0) return false;
447
+ try {
448
+ process.kill(pid, 0);
449
+ return true;
450
+ } catch (error) {
451
+ return error.code === "EPERM";
452
+ }
453
+ }
454
+ function ownerOf(file) {
455
+ try {
456
+ const text = readFileSync3(file, "utf-8");
457
+ return /^\d+$/.test(text) ? Number(text) : Number.NaN;
458
+ } catch (error) {
459
+ if (error.code === "ENOENT") return null;
460
+ throw error;
461
+ }
462
+ }
463
+ function claim(file) {
464
+ const draft = `${file}-${randomUUID4()}.tmp`;
465
+ writeFileSync2(draft, String(process.pid));
466
+ try {
467
+ linkSync(draft, file);
468
+ return true;
469
+ } catch (error) {
470
+ if (error.code !== "EEXIST") throw error;
471
+ return false;
472
+ } finally {
473
+ rmSync(draft, { force: true });
474
+ }
475
+ }
476
+ function releaseOwn(file) {
477
+ if (ownerOf(file) === process.pid) rmSync(file, { force: true });
478
+ }
479
+ function evictDeadOwner(file) {
480
+ const evictor = `${file}.evict`;
481
+ if (!claim(evictor)) {
482
+ const holder = ownerOf(evictor);
483
+ if (holder !== null && !alive(holder)) rmSync(evictor, { force: true });
484
+ return false;
485
+ }
486
+ try {
487
+ const pid = ownerOf(file);
488
+ if (pid !== null && !alive(pid)) rmSync(file, { force: true });
489
+ return true;
490
+ } finally {
491
+ releaseOwn(evictor);
492
+ }
493
+ }
494
+ async function takeHistoryOwnership(home, waitMs) {
495
+ const file = join3(home, "owner.pid");
496
+ const deadline = Date.now() + waitMs;
497
+ mkdirSync2(home, { recursive: true });
498
+ for (; ; ) {
499
+ if (claim(file)) {
500
+ let held = true;
501
+ return () => {
502
+ if (held) releaseOwn(file);
503
+ held = false;
504
+ };
505
+ }
506
+ const pid = ownerOf(file);
507
+ if (pid === null) continue;
508
+ if (!alive(pid) && evictDeadOwner(file)) continue;
509
+ if (Date.now() >= deadline) throw new HistoryBusyError(pid);
510
+ await new Promise((settle) => setTimeout(settle, 50));
511
+ }
512
+ }
513
+ var DAY_MS = 24 * 60 * 60 * 1e3;
514
+ var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
515
+ var PRUNED_PREFIX = ".pruned-";
516
+ var LAST_PRUNE_FILE = ".last-prune";
517
+ function listProjectHistories(historyRoot) {
518
+ return namesIn(historyRoot).filter(isHistoryId).flatMap((id) => {
519
+ const home = join4(historyRoot, id);
520
+ const projectDir = readRecord(home)?.dir;
521
+ if (typeof projectDir !== "string" || readId(projectDir) !== id) return [];
522
+ return [{ id, projectDir, lastUsedMs: lastUsed(home) }];
523
+ });
524
+ }
525
+ function namesIn(historyRoot) {
526
+ try {
527
+ return readdirSync2(historyRoot);
528
+ } catch (error) {
529
+ if (error.code === "ENOENT") return [];
530
+ throw error;
531
+ }
532
+ }
533
+ async function pruneGoneProjectHistories(historyRoot, options = {}) {
534
+ return (await pruneWithin(historyRoot, options)).pruned;
535
+ }
536
+ async function pruneWithin(historyRoot, {
537
+ now = Date.now(),
538
+ dryRun = false,
539
+ tempDir = tmpdir(),
540
+ onError = (error) => console.warn("[history] could not prune a history:", error),
541
+ budgetMs = Infinity,
542
+ measure = true
543
+ }) {
544
+ const started = performance.now();
545
+ const names = namesIn(historyRoot);
546
+ const pruned = [];
547
+ for (const name of names) {
548
+ if (performance.now() - started >= budgetMs) return { pruned, finished: false };
549
+ try {
550
+ const gone = await pruneEntry(historyRoot, name, { now, dryRun, tempDir, measure });
551
+ if (gone) pruned.push(gone);
552
+ } catch (error) {
553
+ onError(error);
554
+ }
555
+ }
556
+ return { pruned, finished: true };
557
+ }
558
+ async function pruneEntry(historyRoot, name, run) {
559
+ if (name.startsWith(PRUNED_PREFIX)) {
560
+ if (!run.dryRun) await rm2(join4(historyRoot, name), { recursive: true, force: true });
561
+ return null;
562
+ }
563
+ return isHistoryId(name) ? pruneOne(historyRoot, name, run) : null;
564
+ }
565
+ async function pruneOne(historyRoot, id, run) {
566
+ const home = join4(historyRoot, id);
567
+ if (abandonedProject(home, id, run.now, run.tempDir) === null) return null;
568
+ const release = await takeUnlessBusy(home);
569
+ if (!release) return null;
570
+ let taken;
571
+ try {
572
+ taken = recheckAndMoveAway(historyRoot, id, run);
573
+ } catch (error) {
574
+ release();
575
+ throw error;
576
+ }
577
+ if (!taken?.trash) release();
578
+ if (!taken) return null;
579
+ const bytes = await clearAway(taken.trash, home, run.measure);
580
+ return { id, projectDir: taken.projectDir, bytes };
581
+ }
582
+ async function clearAway(trash, home, measure) {
583
+ const bytes = measure ? await bytesIn(trash ?? home) : 0;
584
+ if (trash) await rm2(trash, { recursive: true, force: true });
585
+ return bytes;
586
+ }
587
+ async function takeUnlessBusy(home) {
588
+ try {
589
+ return await takeHistoryOwnership(home, 0);
590
+ } catch (error) {
591
+ if (error instanceof HistoryBusyError) return null;
592
+ throw error;
593
+ }
594
+ }
595
+ function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
596
+ const home = join4(historyRoot, id);
597
+ const projectDir = abandonedProject(home, id, now, tempDir);
598
+ if (projectDir === null) return null;
599
+ if (dryRun) return { projectDir, trash: null };
600
+ const trash = join4(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID5()}`);
601
+ renameSync2(home, trash);
602
+ return { projectDir, trash };
603
+ }
604
+ function abandonedProject(home, id, now, tempDir) {
605
+ const record = readRecord(home);
606
+ const dir = record?.dir;
607
+ if (typeof dir !== "string") return null;
608
+ if (now - lastUsed(home) < KEEP_GONE_PROJECT_HISTORY_MS && !isSafePath(tempDir, dir)) return null;
609
+ return projectGone(dir, id, record?.dev, tempDir) ? dir : null;
610
+ }
611
+ function projectGone(dir, id, dev, tempDir) {
612
+ if (!statSync2(join4(dir, ID_PATH), { throwIfNoEntry: false }))
613
+ return diskStillHere(dir, dev, tempDir);
614
+ const there = readId(dir);
615
+ return there !== null && there !== id;
616
+ }
617
+ function diskStillHere(dir, dev, tempDir) {
618
+ let at = dir;
619
+ let stat2 = statSync2(at, { throwIfNoEntry: false });
620
+ while (!stat2) {
621
+ if (dirname2(at) === at) return false;
622
+ at = dirname2(at);
623
+ stat2 = statSync2(at, { throwIfNoEntry: false });
624
+ }
625
+ if (typeof dev === "number") return stat2.dev === dev;
626
+ return at === dir || isSafePath(tempDir, at);
627
+ }
628
+ function lastUsed(home) {
629
+ const mtime = (file) => statSync2(join4(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
630
+ return Math.max(mtime("project.json"), mtime("log.jsonl"));
631
+ }
632
+ async function bytesIn(dir) {
633
+ let total = 0;
634
+ for (const entry of await readdir2(dir, { withFileTypes: true })) {
635
+ const path = join4(dir, entry.name);
636
+ total += entry.isDirectory() ? await bytesIn(path) : (await lstat(path)).size;
637
+ }
638
+ return total;
639
+ }
640
+ function prunedWithinADay(stamp, now) {
641
+ try {
642
+ const last = Number(readFileSync4(stamp, "utf-8"));
643
+ return last <= now && now - last < DAY_MS;
644
+ } catch {
645
+ return false;
646
+ }
647
+ }
648
+ var pruning = /* @__PURE__ */ new Set();
649
+ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs } = {}) {
650
+ const stamp = join4(historyRoot, LAST_PRUNE_FILE);
651
+ if (pruning.has(historyRoot) || prunedWithinADay(stamp, now)) return;
652
+ pruning.add(historyRoot);
653
+ setImmediate(() => {
654
+ pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync3(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
655
+ });
656
+ }
657
+ var START = "start";
658
+ function parseRecord(line) {
659
+ try {
660
+ return JSON.parse(line);
661
+ } catch {
662
+ return null;
663
+ }
664
+ }
665
+ function readLog(file, onUnreadable) {
666
+ let text;
667
+ try {
668
+ text = readFileSync5(file, "utf-8");
669
+ } catch {
670
+ return null;
671
+ }
672
+ const log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
673
+ const lines = text.split("\n");
674
+ lines.forEach((line, index) => {
675
+ const record = line ? parseRecord(line) : null;
676
+ if (record) applyRecord(log, record);
677
+ else if (line && index < lines.length - 1) onUnreadable(index + 1);
678
+ });
679
+ return log;
680
+ }
681
+ function applyRecord(log, record) {
682
+ if (record.type === "baseline") {
683
+ log.baseline = new Map(Object.entries(record.files));
684
+ log.keepsLedger = record.keepsLedger === true;
685
+ } else if (record.type === "entry") log.entries.push(record.entry);
686
+ else if (record.pinned) log.pins.add(record.id);
687
+ else log.pins.delete(record.id);
688
+ }
689
+ function saveRecord(file, log, record) {
690
+ let fd;
691
+ try {
692
+ fd = openSync(file, constants2.O_WRONLY | constants2.O_APPEND);
693
+ } catch (error) {
694
+ if (error.code !== "ENOENT") throw error;
695
+ return writeLog(file, log);
696
+ }
697
+ try {
698
+ writeSync(fd, `${JSON.stringify(record)}
699
+ `);
700
+ } finally {
701
+ closeSync(fd);
702
+ }
703
+ }
704
+ function baselineRecord(log) {
705
+ return {
706
+ type: "baseline",
707
+ files: Object.fromEntries(log.baseline),
708
+ ...log.keepsLedger && { keepsLedger: true }
709
+ };
710
+ }
711
+ function writeLog(file, log) {
712
+ const records = [
713
+ baselineRecord(log),
714
+ ...log.entries.map((entry) => ({ type: "entry", entry })),
715
+ ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
716
+ ];
717
+ mkdirSync3(dirname3(file), { recursive: true });
718
+ replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
719
+ `).join(""), 420);
720
+ }
721
+ function applyEntry(manifest, entry, side) {
722
+ for (const file of entry.files) {
723
+ const hash = file[side];
724
+ if (hash === null) manifest.delete(file.path);
725
+ else manifest.set(file.path, hash);
726
+ }
727
+ }
728
+ function manifestAt(log, point) {
729
+ return point === START ? new Map(log.baseline) : manifestAround(log, point, "after");
730
+ }
731
+ function manifestAround(log, id, side) {
732
+ const manifest = new Map(log.baseline);
733
+ for (const entry of log.entries) {
734
+ applyEntry(manifest, entry, entry.id === id ? side : "after");
735
+ if (entry.id === id) return manifest;
736
+ }
737
+ return null;
738
+ }
739
+ function undoneIds(entries) {
740
+ const undone = /* @__PURE__ */ new Set();
741
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
742
+ const entry = entries[i];
743
+ if (entry.undoes && !undone.has(entry.id)) undone.add(entry.undoes);
744
+ }
745
+ return undone;
746
+ }
747
+ function stepTarget(entries, direction, mine) {
748
+ const undone = undoneIds(entries);
749
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
750
+ const owns = ownerOf2(entries, byId, mine);
751
+ const newestFirst = [...entries].reverse();
752
+ if (direction === "back")
753
+ return newestFirst.find((entry) => !entry.undoes && !undone.has(entry.id) && owns(entry));
754
+ for (const entry of newestFirst) {
755
+ if (!entry.undoes) {
756
+ if (owns(entry)) return void 0;
757
+ continue;
758
+ }
759
+ const target = byId.get(entry.undoes);
760
+ if (mine(entry) && !undone.has(entry.id) && target && !target.undoes) return entry;
761
+ }
762
+ return void 0;
763
+ }
764
+ function ownerOf2(entries, byId, mine) {
765
+ const redoneBy = /* @__PURE__ */ new Map();
766
+ for (const entry of entries) {
767
+ const target = entry.undoes ? byId.get(entry.undoes) : void 0;
768
+ if (target?.undoes) redoneBy.set(target.undoes, entry);
769
+ }
770
+ return (entry) => mine(entry) || mine(redoneBy.get(entry.id) ?? entry);
771
+ }
772
+ function foldOldest(log) {
773
+ const oldest = log.entries[0];
774
+ if (!oldest || log.pins.has(oldest.id)) return false;
775
+ applyEntry(log.baseline, oldest, "after");
776
+ log.entries.shift();
777
+ return true;
778
+ }
779
+ function referencedHashes(log, current) {
780
+ const keep = /* @__PURE__ */ new Set([...log.baseline.values(), ...current.values()]);
781
+ for (const entry of log.entries)
782
+ for (const file of entry.files) {
783
+ if (file.before) keep.add(file.before);
784
+ if (file.after) keep.add(file.after);
785
+ }
786
+ return keep;
787
+ }
788
+ var DEFAULT_HISTORY_ROOT = join5(homedir(), ".cache", "hyperframes", "history");
789
+ var MAX_WINDOW_IDLE_MS = 10 * 6e4;
790
+ var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
791
+ var OUTSIDE = { kind: "outside", name: "Outside" };
792
+ var OUTSIDE_LABEL = "Changed outside the app";
793
+ var RACY_MS = 2e3;
794
+ var statKey = (file, sweptAt) => sweptAt - Math.max(file.mtimeMs, file.ctimeMs) < RACY_MS ? "" : `${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
795
+ var MEDIA_LEDGER = ".media/manifest.jsonl";
796
+ var KEPT_HIDDEN_PATHS = /* @__PURE__ */ new Set([...STUDIO_SIGNATURE_MANIFEST_PATHS, MEDIA_LEDGER]);
797
+ function isHistoryPath(path) {
798
+ if (KEPT_HIDDEN_PATHS.has(path)) return true;
799
+ return !path.split("/").some((segment) => segment.startsWith("."));
800
+ }
801
+ function withoutHiddenPaths(log) {
802
+ for (const path of log.baseline.keys()) if (!isHistoryPath(path)) log.baseline.delete(path);
803
+ for (const entry of log.entries)
804
+ entry.files = entry.files.filter((file) => isHistoryPath(file.path));
805
+ return log;
806
+ }
807
+ var historyFiles = (dir) => listProjectFiles(dir).filter((file) => isHistoryPath(file.path));
808
+ var COPY_LATER_ABOVE_BYTES = 1024 ** 2;
809
+ var sameWho = (a, b) => a.kind === b.kind && a.name === b.name;
810
+ function splitAt(change, at, kept) {
811
+ if (at === change.after) return [change, null];
812
+ if (!at || at === change.before || !kept) return [null, change];
813
+ return [
814
+ { ...change, after: at },
815
+ { ...change, before: at }
816
+ ];
817
+ }
818
+ var isMissingFolder = (error) => ["ENOENT", "ENOTDIR"].includes(error.code ?? "");
819
+ async function removeEmptyFolders(dir) {
820
+ let entries;
821
+ try {
822
+ entries = await readdir3(dir, { withFileTypes: true });
823
+ } catch (error) {
824
+ if (isMissingFolder(error)) return;
825
+ throw error;
826
+ }
827
+ for (const entry of entries)
828
+ if (entry.isDirectory()) await removeEmptyFolders(join5(dir, entry.name));
829
+ await rmdir(dir);
830
+ }
831
+ function fileLeftIn(dir, at, deleted) {
832
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
833
+ const path = `${at}/${entry.name}`;
834
+ const left = entry.isDirectory() ? fileLeftIn(join5(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
835
+ if (left) return left;
836
+ }
837
+ return void 0;
838
+ }
839
+ function ignoresCase(dir) {
840
+ const self = statSync3(join5(dir, ID_PATH), { throwIfNoEntry: false });
841
+ const other = statSync3(join5(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
842
+ return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
843
+ }
844
+ var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
845
+ var MAX_TIMER_MS = 2 ** 31 - 1;
846
+ var takesWrite = (window, at) => window.idleMs === void 0 || at - (window.lastWriteAt ?? at) <= window.idleMs;
847
+ function chainBackFrom(byAfter, hash) {
848
+ const chain = /* @__PURE__ */ new Map();
849
+ let at = hash;
850
+ let prior = byAfter?.get(at);
851
+ while (prior !== void 0 && !chain.has(at)) {
852
+ chain.set(at, prior);
853
+ at = prior;
854
+ prior = byAfter?.get(at);
855
+ }
856
+ return chain;
857
+ }
858
+ function addChange(group, path, before, after) {
859
+ const earlier = group.changes.get(path);
860
+ const from = earlier ? earlier.before : before;
861
+ if (from === after) group.changes.delete(path);
862
+ else group.changes.set(path, { path, before: from, after });
863
+ }
864
+ var Engine = class {
865
+ constructor(options, projectId, blobs) {
866
+ this.options = options;
867
+ this.projectId = projectId;
868
+ this.blobs = blobs;
869
+ this.dir = resolve3(options.projectDir);
870
+ this.home = join5(options.historyRoot, projectId);
871
+ this.foldsCase = ignoresCase(this.dir);
872
+ const folder = statSync3(this.dir, { throwIfNoEntry: false });
873
+ if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
874
+ throw this.replaced();
875
+ this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
876
+ }
877
+ options;
878
+ projectId;
879
+ blobs;
880
+ dir;
881
+ foldsCase;
882
+ folder;
883
+ home;
884
+ log = { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() };
885
+ tracked = /* @__PURE__ */ new Map();
886
+ windows = [];
887
+ outside = null;
888
+ /** A coalescing claim, open until another key, its idle timer, an operation, a window, or another write. */
889
+ claimed = null;
890
+ adopting = /* @__PURE__ */ new Map();
891
+ /** Per path and hash an API write left, the hash of the bytes it replaced, until walked or written past. */
892
+ overwritten = /* @__PURE__ */ new Map();
893
+ stopHearing;
894
+ writeToken;
895
+ quietTimer;
896
+ maxTimer;
897
+ notedTimer = null;
898
+ listeners = /* @__PURE__ */ new Set();
899
+ tail = Promise.resolve();
900
+ closed = false;
901
+ closing;
902
+ /** A path as the project's disk compares names: folded where `A` and `a` are one name. */
903
+ nameKey(path) {
904
+ return this.foldsCase ? path.toLowerCase() : path;
905
+ }
906
+ get logFile() {
907
+ return join5(this.home, "log.jsonl");
908
+ }
909
+ now() {
910
+ return (this.options.now ?? Date.now)();
911
+ }
912
+ /** One operation at a time, in order: sweeps, windows and undos never interleave. */
913
+ queue(task) {
914
+ if (this.closed)
915
+ return Promise.reject(new HistoryClosedError("This project's history is closed."));
916
+ const run = this.tail.then(task);
917
+ this.tail = run.catch(() => void 0);
918
+ return run;
919
+ }
920
+ /** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
921
+ whereFolder() {
922
+ const now = statSync3(this.dir, { throwIfNoEntry: false });
923
+ if (!now) return "gone";
924
+ const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
925
+ return same && readId(this.dir) === this.projectId ? "here" : "replaced";
926
+ }
927
+ assertWritable() {
928
+ const folder = this.whereFolder();
929
+ if (folder === "gone") throw new Error(`The project folder is gone: ${this.dir}`);
930
+ if (folder === "replaced") throw this.replaced();
931
+ }
932
+ persistLog(record) {
933
+ if (record) saveRecord(this.logFile, this.log, record);
934
+ else writeLog(this.logFile, this.log);
935
+ if (!existsSync2(join5(this.home, "project.json")))
936
+ recordProject(this.home, this.dir, this.folder);
937
+ }
938
+ replaced() {
939
+ return new HistoryClosedError(`${this.dir} is now another project.`);
940
+ }
941
+ assertOpen() {
942
+ if (this.closed) throw new HistoryClosedError("This project's history is closed.");
943
+ if (this.whereFolder() === "replaced") throw this.replaced();
944
+ }
945
+ async start() {
946
+ const log = readLog(
947
+ this.logFile,
948
+ (line) => this.options.onError?.(
949
+ new Error(`History log line ${line} could not be read and was skipped.`)
950
+ )
951
+ );
952
+ if (!log) return this.firstOpen();
953
+ this.log = withoutHiddenPaths(log);
954
+ if (!this.log.keepsLedger) await this.takeInUnnamedLedger();
955
+ const cache = this.readStatCache();
956
+ const last = this.log.entries.at(-1)?.id ?? START;
957
+ for (const [path, hash] of manifestAt(this.log, last) ?? []) {
958
+ const cached = cache.get(path);
959
+ this.tracked.set(path, { hash, stat: cached?.hash === hash ? cached.stat : "" });
960
+ }
961
+ const left = this.readAdopting();
962
+ const unadopted = (file) => left.has(file.path) && !this.tracked.has(file.path);
963
+ this.adoptMediaInBackground(historyFiles(this.dir).filter(unadopted), this.now());
964
+ this.reopenClosedWindow();
965
+ await this.settleAll();
966
+ }
967
+ hearWrites() {
968
+ const realDir = realFilePath(this.dir);
969
+ this.stopHearing = onFileOverwritten((absPath, version, bytes) => {
970
+ const after = hashOfVersion(version);
971
+ if (!after || !affectsProjectSignature(realDir, absPath)) return;
972
+ const replaced = hashOfVersion(fileContentVersion(bytes));
973
+ if (!replaced || replaced === after) return;
974
+ const path = relative2(realDir, absPath).split(sep2).join("/");
975
+ if (!isHistoryPath(path)) return;
976
+ this.overwritten.set(
977
+ path,
978
+ chainBackFrom(this.overwritten.get(path), replaced).set(after, replaced)
979
+ );
980
+ this.queue(async () => {
981
+ if (!this.blobs.has(replaced)) await this.storeBytes(bytes);
982
+ }).catch((error) => this.options.onError?.(error));
983
+ });
984
+ }
985
+ async storeBytes(bytes) {
986
+ const staged = join5(this.home, `overwrote-${randomUUID6()}`);
987
+ await mkdir2(this.home, { recursive: true });
988
+ await writeFile(staged, bytes);
989
+ try {
990
+ await this.blobs.put(staged);
991
+ } finally {
992
+ await rm3(staged, { force: true });
993
+ }
994
+ }
995
+ /** Read at a state no API write left, with no write heard since `heard`: no walk passes it, so forget the rest. */
996
+ forgetWritesBefore(path, hash, heard) {
997
+ if (this.overwritten.get(path) === heard && !(hash && heard?.has(hash)))
998
+ this.overwritten.delete(path);
999
+ }
1000
+ reopenClosedWindow() {
1001
+ const closed = this.options.closedWindow;
1002
+ if (!closed || this.log.entries.some((entry) => entry.id === closed.id)) return;
1003
+ const { id, who, label, startedAt, lastWriteAt, idleMs } = closed;
1004
+ this.windows.push({ id, who, label, startedAt, lastWriteAt, idleMs, changes: /* @__PURE__ */ new Map() });
1005
+ }
1006
+ async takeInUnnamedLedger() {
1007
+ const named = (path) => this.log.entries.some((entry) => entry.files.some((file) => file.path === path));
1008
+ const listed = () => historyFiles(this.dir).some((file) => file.path === MEDIA_LEDGER);
1009
+ const hash = this.log.baseline.has(MEDIA_LEDGER) || named(MEDIA_LEDGER) || !listed() ? null : await this.storeIfPresent(MEDIA_LEDGER);
1010
+ if (hash !== null) this.log.baseline.set(MEDIA_LEDGER, hash);
1011
+ this.log.keepsLedger = true;
1012
+ this.persistLog();
1013
+ }
1014
+ async firstOpen() {
1015
+ const sweptAt = this.now();
1016
+ const files = historyFiles(this.dir);
1017
+ for (const file of files.filter((file2) => file2.size <= COPY_LATER_ABOVE_BYTES)) {
1018
+ const hash = await this.storeIfPresent(file.path);
1019
+ if (this.whereFolder() !== "here") throw this.replaced();
1020
+ if (hash !== null) this.tracked.set(file.path, { hash, stat: statKey(file, sweptAt) });
1021
+ }
1022
+ this.log.baseline = this.manifest();
1023
+ this.log.keepsLedger = true;
1024
+ this.persistLog();
1025
+ this.saveStatCache();
1026
+ this.adoptMediaInBackground(
1027
+ files.filter((file) => file.size > COPY_LATER_ABOVE_BYTES),
1028
+ sweptAt
1029
+ );
1030
+ }
1031
+ adoptMediaInBackground(files, sweptAt) {
1032
+ let copied = Promise.resolve();
1033
+ for (const file of files) {
1034
+ copied = copied.then(() => this.adopt(file, sweptAt));
1035
+ this.adopting.set(file.path, copied);
1036
+ }
1037
+ this.saveAdopting();
1038
+ }
1039
+ saveAdopting() {
1040
+ const file = join5(this.home, "adopting.json");
1041
+ if (!this.adopting.size) return rmSync2(file, { force: true });
1042
+ mkdirSync4(this.home, { recursive: true });
1043
+ replaceFileAtomically(file, JSON.stringify([...this.adopting.keys()]), 420);
1044
+ }
1045
+ async adopt(file, sweptAt) {
1046
+ try {
1047
+ if (this.whereFolder() !== "here") return;
1048
+ const stored = await this.storeIfPresent(file.path);
1049
+ await this.queue(async () => {
1050
+ await this.recordAdopted(file, sweptAt, stored);
1051
+ this.adopting.delete(file.path);
1052
+ this.saveAdopting();
1053
+ });
1054
+ } catch (error) {
1055
+ if (!(error instanceof HistoryClosedError)) this.options.onError?.(error);
1056
+ }
1057
+ }
1058
+ async recordAdopted(file, sweptAt, stored) {
1059
+ if (stored === null || this.whereFolder() !== "here") return;
1060
+ const hash = this.blobs.has(stored) ? stored : await this.storeIfPresent(file.path);
1061
+ if (hash === null) return;
1062
+ const change = { path: file.path, before: null, after: hash };
1063
+ const found = this.overwrittenBy(change, void 0, /* @__PURE__ */ new Set());
1064
+ const start = found && this.blobs.has(found) ? found : hash;
1065
+ this.tracked.set(file.path, { hash, stat: statKey(file, sweptAt) });
1066
+ this.log.baseline.set(file.path, start);
1067
+ if (start !== hash) addChange(this.outsideGroup(), file.path, start, hash);
1068
+ this.persistLog(baselineRecord(this.log));
1069
+ this.saveStatCache();
1070
+ }
1071
+ manifest() {
1072
+ return new Map([...this.tracked].map(([path, file]) => [path, file.hash]));
1073
+ }
1074
+ readStatCache() {
1075
+ try {
1076
+ const files = JSON.parse(readFileSync6(join5(this.home, "stat.json"), "utf-8"));
1077
+ return new Map(Object.entries(files));
1078
+ } catch {
1079
+ return /* @__PURE__ */ new Map();
1080
+ }
1081
+ }
1082
+ readAdopting() {
1083
+ try {
1084
+ return new Set(
1085
+ JSON.parse(readFileSync6(join5(this.home, "adopting.json"), "utf-8"))
1086
+ );
1087
+ } catch {
1088
+ return /* @__PURE__ */ new Set();
1089
+ }
1090
+ }
1091
+ saveStatCache() {
1092
+ const files = JSON.stringify(Object.fromEntries(this.tracked));
1093
+ mkdirSync4(this.home, { recursive: true });
1094
+ replaceFileAtomically(join5(this.home, "stat.json"), files, 420);
1095
+ }
1096
+ /**
1097
+ * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
1098
+ * project (a stat per file, a hash only when the stat moved); per-path sweeps if projects reach tens of thousands.
1099
+ */
1100
+ async sweep() {
1101
+ if (this.whereFolder() !== "here") return;
1102
+ const sweptAt = this.now();
1103
+ const changedAt = (file) => Math.min(sweptAt, Math.max(file.mtimeMs, file.ctimeMs));
1104
+ const seen = historyFiles(this.dir).filter((file) => !this.adopting.has(file.path));
1105
+ const heard = new Map(this.overwritten);
1106
+ const present = new Set(seen.map((file) => file.path));
1107
+ const removed = [...this.tracked.keys()].filter((path) => !present.has(path)).map((path) => {
1108
+ const standing = this.standingAt(path);
1109
+ return { at: standing ? changedAt(standing.stat) : sweptAt, path, file: null };
1110
+ });
1111
+ const appearedAt = (file) => Math.max(
1112
+ changedAt(file),
1113
+ ...removed.filter((gone) => blocks(this.nameKey(gone.path), this.nameKey(file.path))).map((gone) => gone.at)
1114
+ );
1115
+ const events = [
1116
+ ...removed,
1117
+ ...seen.map((file) => ({ at: appearedAt(file), path: file.path, file }))
1118
+ ].sort((a, b) => a.at - b.at);
1119
+ let changed = false;
1120
+ for (const { at, path, file } of events) {
1121
+ if (file) {
1122
+ changed = await this.observe(path, statKey(file, sweptAt), at) || changed;
1123
+ continue;
1124
+ }
1125
+ const known = this.tracked.get(path);
1126
+ this.tracked.delete(path);
1127
+ this.forgetWritesBefore(path, null, heard.get(path));
1128
+ await this.record(path, known.hash, null, at);
1129
+ changed = true;
1130
+ }
1131
+ if (changed) this.saveStatCache();
1132
+ }
1133
+ /** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
1134
+ async storeIfPresent(path) {
1135
+ try {
1136
+ return await this.blobs.put(join5(this.dir, path));
1137
+ } catch (error) {
1138
+ if (error.code === "ENOENT") return null;
1139
+ throw error;
1140
+ }
1141
+ }
1142
+ async observe(path, stat2, at) {
1143
+ const known = this.tracked.get(path) ?? { hash: null, stat: null };
1144
+ if (stat2 && known.stat === stat2) return false;
1145
+ const heard = this.overwritten.get(path);
1146
+ const hash = await this.storeIfPresent(path);
1147
+ if (hash === null || this.whereFolder() !== "here") return false;
1148
+ this.tracked.set(path, { hash, stat: stat2 });
1149
+ this.forgetWritesBefore(path, hash, heard);
1150
+ if (known.hash !== hash) await this.record(path, known.hash, hash, at);
1151
+ return known.hash !== hash || known.stat !== stat2;
1152
+ }
1153
+ async record(path, before, after, at) {
1154
+ const endedBeforeThisWrite = this.windows.filter((open) => !takesWrite(open, at));
1155
+ if (endedBeforeThisWrite.length) await this.commitOutside();
1156
+ for (const window2 of endedBeforeThisWrite) await this.endWindow(window2);
1157
+ const window = this.windows.at(-1);
1158
+ if (window) this.touch(window, at);
1159
+ addChange(window ?? this.outsideGroup(), path, before, after);
1160
+ }
1161
+ /** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
1162
+ async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {} }) {
1163
+ await this.sweep();
1164
+ const taken = await this.takeClaimed(who, paths, overwrote);
1165
+ if (!taken.length) return this.claimNothing(coalesceKey);
1166
+ const held = this.heldFor(coalesceKey, taken);
1167
+ if (!held) await this.commitClaim();
1168
+ await this.commitPending();
1169
+ const group = held ?? this.newGroup(who, label);
1170
+ for (const change of taken) addChange(group, change.path, change.before, change.after);
1171
+ if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
1172
+ const entry = await this.commit(group);
1173
+ return entry && { id: entry.id };
1174
+ }
1175
+ /** A claim that took nothing still ends a held claim under another key. */
1176
+ async claimNothing(coalesceKey) {
1177
+ if (coalesceKey !== this.claimed?.key) await this.commitClaim();
1178
+ return null;
1179
+ }
1180
+ /** Commits every other writer's pending changes, oldest first; open windows stay open. */
1181
+ async commitPending() {
1182
+ await this.commitOutside();
1183
+ for (const window of [...this.windows])
1184
+ if (window.changes.size) await this.commitCut(window, [...window.changes.keys()]);
1185
+ }
1186
+ pendingElsewhere() {
1187
+ return Boolean(this.outside?.changes.size) || this.windows.some((open) => open.changes.size > 0);
1188
+ }
1189
+ /** The held claim `taken` continues: same key, each file from its own last change, no held file changed since. */
1190
+ heldFor(key, taken) {
1191
+ const group = key && this.claimed?.key === key && !this.pendingElsewhere() ? this.claimed.group : null;
1192
+ const after = (path, before) => {
1193
+ const own = group?.changes.get(path);
1194
+ return own ? own.after : before;
1195
+ };
1196
+ const takenPaths = new Set(taken.map((change) => change.path));
1197
+ const untouched = (change) => takenPaths.has(change.path) || (this.tracked.get(change.path)?.hash ?? null) === change.after;
1198
+ return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
1199
+ }
1200
+ logPath(path) {
1201
+ return relative2(this.dir, resolve3(this.dir, path)).split(sep2).join("/");
1202
+ }
1203
+ /** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
1204
+ async takeClaimed(who, paths, overwrote) {
1205
+ const wanted = new Set(paths.map((path) => this.logPath(path)));
1206
+ const at = new Map(
1207
+ Object.entries(overwrote).map(([path, version]) => [
1208
+ this.logPath(path),
1209
+ hashOfVersion(version)
1210
+ ])
1211
+ );
1212
+ const others = this.windows.filter((open) => !sameWho(open.who, who));
1213
+ const groups = this.outside ? [this.outside, ...others] : others;
1214
+ const hits = groups.flatMap(
1215
+ (group) => [...group.changes.values()].filter((change) => wanted.has(change.path)).map((change) => ({ group, change }))
1216
+ );
1217
+ const cuts = [];
1218
+ const walked = hits.map(() => /* @__PURE__ */ new Set());
1219
+ for (const [i, { change }] of hits.entries()) {
1220
+ const told = at.get(change.path);
1221
+ cuts.push(this.overwrittenBy(change, told, walked[i]) ?? told);
1222
+ }
1223
+ hits.forEach(({ change }, i) => this.forgetOverwritten(change.path, walked[i]));
1224
+ return hits.flatMap(({ group, change }, i) => this.cutOut(group, change, cuts[i]) ?? []);
1225
+ }
1226
+ /** Walks the server's writes back from `change.after` to the stored bytes they replaced. */
1227
+ overwrittenBy(change, told, seen) {
1228
+ let found;
1229
+ for (let hash = change.after; hash; ) {
1230
+ if (seen.has(hash)) return void 0;
1231
+ seen.add(hash);
1232
+ const replaced = this.overwritten.get(change.path)?.get(hash);
1233
+ if (replaced === void 0) break;
1234
+ found = hash = replaced;
1235
+ if (hash === change.before || hash === told) return hash;
1236
+ }
1237
+ return found;
1238
+ }
1239
+ forgetOverwritten(path, hashes) {
1240
+ const byAfter = this.overwritten.get(path);
1241
+ for (const hash of hashes) byAfter?.delete(hash);
1242
+ if (byAfter?.size === 0) this.overwritten.delete(path);
1243
+ }
1244
+ cutOut(group, change, cut) {
1245
+ const [kept, claimed] = splitAt(change, cut, cut !== void 0 && this.blobs.has(cut));
1246
+ group.changes.delete(change.path);
1247
+ if (kept) group.changes.set(kept.path, kept);
1248
+ return claimed;
1249
+ }
1250
+ /** Commits the window's cut `paths` as their own entry, other files staying: a window holds one change per file,
1251
+ * so the writer's next write to a cut file would otherwise span the claimer's edit. */
1252
+ async commitCut(window, paths) {
1253
+ const part = { ...this.newGroup(window.who, window.label), startedAt: window.startedAt };
1254
+ for (const path of paths) {
1255
+ part.changes.set(path, window.changes.get(path));
1256
+ window.changes.delete(path);
1257
+ }
1258
+ (window.parts ??= /* @__PURE__ */ new Set()).add(part.id);
1259
+ window.entry = await this.commit(part);
1260
+ }
1261
+ holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
1262
+ clearTimeout(this.claimed?.timer);
1263
+ const timer = Number.isFinite(idleMs) ? setTimeout(() => this.background(() => this.commitClaim()), idleMs) : void 0;
1264
+ timer?.unref?.();
1265
+ group.lastWriteAt = this.now();
1266
+ this.claimed = { group, key, timer };
1267
+ return group.changes.size ? { id: group.id } : null;
1268
+ }
1269
+ async commitClaim() {
1270
+ const held = this.claimed;
1271
+ this.claimed = null;
1272
+ if (!held) return;
1273
+ clearTimeout(held.timer);
1274
+ await this.commit(held.group);
1275
+ }
1276
+ outsideGroup() {
1277
+ const { quietMs = 2e3, maxGroupMs = 3e4 } = this.options;
1278
+ const closeIn = (ms) => {
1279
+ const timer = setTimeout(() => this.background(() => this.commitOutside()), ms);
1280
+ timer.unref?.();
1281
+ return timer;
1282
+ };
1283
+ if (!this.outside) {
1284
+ this.outside = this.newGroup(OUTSIDE, OUTSIDE_LABEL);
1285
+ this.maxTimer = closeIn(maxGroupMs);
1286
+ }
1287
+ clearTimeout(this.quietTimer);
1288
+ this.quietTimer = closeIn(quietMs);
1289
+ return this.outside;
1290
+ }
1291
+ newGroup(who, label) {
1292
+ return { id: randomUUID6(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
1293
+ }
1294
+ async commitOutside() {
1295
+ if (this.outside?.changes.size) await this.commitClaim();
1296
+ clearTimeout(this.quietTimer);
1297
+ clearTimeout(this.maxTimer);
1298
+ const group = this.outside;
1299
+ this.outside = null;
1300
+ if (group) await this.commit(group);
1301
+ }
1302
+ async commit(group, extra = {}) {
1303
+ const undoesEmptied = this.log.entries.find((entry2) => entry2.id === extra.undoes)?.files.length === 0;
1304
+ if (!group.changes.size && !undoesEmptied) return null;
1305
+ const pending = this.pendingEntry(group);
1306
+ const endedAt = Math.max(pending.endedAt, this.log.entries.at(-1)?.endedAt ?? 0);
1307
+ const entry = { ...pending, endedAt, ...extra };
1308
+ this.log.entries.push(entry);
1309
+ try {
1310
+ this.persistLog({ type: "entry", entry });
1311
+ } catch (error) {
1312
+ this.log.entries.pop();
1313
+ throw error;
1314
+ }
1315
+ for (const listener of this.listeners) listener(entry);
1316
+ for (const change of group.changes.values()) {
1317
+ const walked = /* @__PURE__ */ new Set();
1318
+ this.overwrittenBy(change, void 0, walked);
1319
+ this.forgetOverwritten(change.path, walked);
1320
+ }
1321
+ await this.keepWithinBudget();
1322
+ return entry;
1323
+ }
1324
+ /** Stored bytes beyond what the current files need: a project larger than the budget still keeps its history. */
1325
+ historyBytes() {
1326
+ let current = 0;
1327
+ for (const hash of new Set([...this.tracked.values()].map((file) => file.hash)))
1328
+ current += this.blobs.size(hash);
1329
+ return this.blobs.bytes() - current;
1330
+ }
1331
+ async keepWithinBudget() {
1332
+ const budget = this.options.budgetBytes ?? 2 * 1024 ** 3;
1333
+ let folded = false;
1334
+ while (this.historyBytes() > budget) {
1335
+ await this.blobs.prune(
1336
+ /* @__PURE__ */ new Set([
1337
+ ...referencedHashes(this.log, this.manifest()),
1338
+ ...this.pendingHashes(),
1339
+ ...[...this.overwritten.values()].flatMap((byAfter) => [...byAfter.values()])
1340
+ ])
1341
+ );
1342
+ if (this.historyBytes() <= budget || !foldOldest(this.log)) break;
1343
+ folded = true;
1344
+ }
1345
+ if (folded) this.persistLog();
1346
+ }
1347
+ /** Hashes only pending changes point at yet, such as a claim's stored cut. */
1348
+ pendingHashes() {
1349
+ const changes = [...this.windows, this.outside, this.claimed?.group].flatMap((group) => [
1350
+ ...group?.changes.values() ?? []
1351
+ ]);
1352
+ return changes.flatMap((change) => [change.before, change.after]).filter((hash) => hash !== null);
1353
+ }
1354
+ /** Before an operation or a window: every write is filed and every pending change committed, in write order. */
1355
+ async settle() {
1356
+ await this.sweep();
1357
+ await this.commitClaim();
1358
+ await this.commitPending();
1359
+ }
1360
+ /** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
1361
+ noteChange(path) {
1362
+ const absolute = resolve3(this.dir, path);
1363
+ if (this.notedTimer || !affectsProjectSignature(this.dir, absolute)) return;
1364
+ if (!isHistoryPath(relative2(this.dir, absolute).split(sep2).join("/"))) return;
1365
+ this.notedTimer = setTimeout(() => {
1366
+ this.notedTimer = null;
1367
+ this.background(() => this.sweep());
1368
+ }, 20);
1369
+ this.notedTimer.unref?.();
1370
+ }
1371
+ background(task) {
1372
+ this.queue(task).catch((error) => {
1373
+ if (!(error instanceof HistoryClosedError)) this.options.onError?.(error);
1374
+ });
1375
+ }
1376
+ beginWindow(who, label, idleMs) {
1377
+ return this.queue(async () => {
1378
+ await this.settle();
1379
+ const window = { ...this.newGroup(who, label), idleMs };
1380
+ this.windows.push(window);
1381
+ this.touch(window, this.now());
1382
+ const close = () => this.queue(() => this.sweepAndEnd(window));
1383
+ return { id: window.id, startedAt: window.startedAt, close };
1384
+ });
1385
+ }
1386
+ /** A window with no write for its idleMs ends, so a close that never comes cannot hold every later write. */
1387
+ touch(window, at) {
1388
+ window.lastWriteAt = Math.max(window.lastWriteAt ?? at, at);
1389
+ this.endWhenIdle(window, window.idleMs);
1390
+ }
1391
+ endWhenIdle(window, delay) {
1392
+ clearTimeout(window.idleTimer);
1393
+ if (window.idleMs === void 0 || !Number.isFinite(window.idleMs) || delay === void 0)
1394
+ return;
1395
+ window.idleTimer = setTimeout(
1396
+ () => {
1397
+ const left = window.idleMs - (this.now() - (window.lastWriteAt ?? this.now()));
1398
+ if (left > 0) this.endWhenIdle(window, left);
1399
+ else this.background(() => this.sweepAndEnd(window));
1400
+ },
1401
+ Math.min(delay, MAX_TIMER_MS)
1402
+ );
1403
+ window.idleTimer.unref?.();
1404
+ }
1405
+ async sweepAndEnd(window) {
1406
+ await this.sweep();
1407
+ return this.endWindow(window);
1408
+ }
1409
+ /** Commits an open window once; ending it again returns the entry it became. */
1410
+ async endWindow(window) {
1411
+ if (!this.windows.includes(window)) return window.entry ?? null;
1412
+ clearTimeout(window.idleTimer);
1413
+ await this.commitClaim();
1414
+ this.windows = this.windows.filter((open) => open !== window);
1415
+ window.entry = await this.commit(window) ?? window.entry ?? null;
1416
+ return window.entry;
1417
+ }
1418
+ /** Every pending write, open window and outside group, committed: for flush and close. */
1419
+ async settleAll() {
1420
+ await this.sweep();
1421
+ await this.commitClaim();
1422
+ for (const window of [...this.windows]) await this.endWindow(window);
1423
+ await this.commitOutside();
1424
+ }
1425
+ /** What took a removed path's place, dating its removal: an entry at the path, or a file at a folder above it. */
1426
+ standingAt(path) {
1427
+ const parts = path.split("/");
1428
+ for (let depth = 1; depth <= parts.length; depth += 1) {
1429
+ const at = parts.slice(0, depth).join("/");
1430
+ const stat2 = lstatSync2(join5(this.dir, at), { throwIfNoEntry: false });
1431
+ if (!stat2) return void 0;
1432
+ if (at === path || !stat2.isDirectory()) return { at, stat: stat2 };
1433
+ }
1434
+ return void 0;
1435
+ }
1436
+ /** What a write of `path` would have to delete: a file or link at or above it, or a file in a folder at it. */
1437
+ inTheWay(path, deleted) {
1438
+ const standing = this.standingAt(path);
1439
+ if (!standing) return void 0;
1440
+ if (standing.stat.isSymbolicLink()) return standing.at;
1441
+ if (standing.at === path && !standing.stat.isDirectory()) return void 0;
1442
+ const real = realpathSync.native(join5(this.dir, standing.at));
1443
+ const at = relative2(realpathSync.native(this.dir), real).split(sep2).join("/");
1444
+ if (standing.at !== path) return deleted.has(at) ? void 0 : at;
1445
+ return fileLeftIn(real, at, deleted);
1446
+ }
1447
+ /** Writes `target` (path to hash, null deletes) as one entry of `who`'s. */
1448
+ async writeAs(who, label, target, extra) {
1449
+ this.assertWritable();
1450
+ const changes = [...target].filter(([path, hash]) => (this.tracked.get(path)?.hash ?? null) !== hash).sort(([, a], [, b]) => Number(a !== null) - Number(b !== null));
1451
+ const deleted = new Set(changes.filter(([, hash]) => hash === null).map(([path]) => path));
1452
+ const blocked = changes.filter(([, hash]) => hash !== null).map(([path]) => this.inTheWay(path, deleted)).find(Boolean);
1453
+ if (blocked) throw new Error(`${blocked} is in the way; move or delete it, then try again.`);
1454
+ const group = this.newGroup(who, label);
1455
+ this.windows.push(group);
1456
+ try {
1457
+ for (const [path, hash] of changes) await this.writeProjectFile(path, hash);
1458
+ await this.sweep();
1459
+ } finally {
1460
+ this.windows = this.windows.filter((open) => open !== group);
1461
+ }
1462
+ return this.commit(group, extra);
1463
+ }
1464
+ /** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
1465
+ async writeProjectFile(path, hash) {
1466
+ this.assertWritable();
1467
+ const absPath = join5(this.dir, path);
1468
+ const { writeToken } = this;
1469
+ if (writeToken) {
1470
+ const version = hash === null ? DELETED_VERSION : hashVersion(hash);
1471
+ recordFileWriteReceipt(absPath, { path, version, writeToken });
1472
+ }
1473
+ if (hash === null) {
1474
+ this.assertWritable();
1475
+ rmSync2(absPath, { force: true });
1476
+ } else {
1477
+ await removeEmptyFolders(absPath);
1478
+ await this.blobs.writeTo(hash, absPath, () => this.assertWritable());
1479
+ }
1480
+ }
1481
+ entry(id) {
1482
+ const entry = this.log.entries.find((candidate) => candidate.id === id);
1483
+ if (!entry) throw new Error("That change is no longer kept in this project's history.");
1484
+ return entry;
1485
+ }
1486
+ undoLabel(entry) {
1487
+ if (!entry.undoes) return `Undid: ${entry.label}`;
1488
+ const original = this.log.entries.find((candidate) => candidate.id === entry.undoes);
1489
+ return `Redid: ${original?.label ?? entry.label}`;
1490
+ }
1491
+ async undoNow(id, who, mode) {
1492
+ const entry = this.entry(id);
1493
+ const changed = this.movedOn(entry);
1494
+ if (changed.length && !mode) return { ok: false, conflict: this.conflict(entry, changed) };
1495
+ if (mode === "back-to-before") {
1496
+ const index = this.log.entries.indexOf(entry);
1497
+ const point = index > 0 ? this.log.entries[index - 1].id : START;
1498
+ return {
1499
+ ok: true,
1500
+ entry: await this.restoreNow(point, who, `Went back to before: ${entry.label}`)
1501
+ };
1502
+ }
1503
+ const target = new Map(
1504
+ entry.files.filter((file) => mode !== "keep-later-edits" || !changed.includes(file)).map((file) => [file.path, file.before])
1505
+ );
1506
+ return {
1507
+ ok: true,
1508
+ entry: await this.writeAs(who, this.undoLabel(entry), target, { undoes: id })
1509
+ };
1510
+ }
1511
+ movedOn(entry) {
1512
+ return entry.files.filter((file) => (this.tracked.get(file.path)?.hash ?? null) !== file.after);
1513
+ }
1514
+ next(direction, who) {
1515
+ const mine = (author) => sameWho(author, who) || sameWho(author, OUTSIDE);
1516
+ const pending = [...[...this.windows].reverse(), this.outside, this.claimed?.group].find(
1517
+ (group) => group?.changes.size && mine(group.who)
1518
+ );
1519
+ if (pending) return direction === "back" ? this.pendingEntry(pending) : void 0;
1520
+ const everyone = this.options.undoScope === "everyone";
1521
+ const ofOpenTurn = (entry) => this.windows.some((open) => open.parts?.has(entry.id));
1522
+ return stepTarget(
1523
+ this.log.entries,
1524
+ direction,
1525
+ (entry) => entry.files.length > 0 && (mine(entry.who) || everyone && !ofOpenTurn(entry))
1526
+ );
1527
+ }
1528
+ /** A pending group as the entry it becomes once committed (a window's part gets a fresh id). */
1529
+ pendingEntry(group) {
1530
+ const files = [...group.changes.values()].sort((a, b) => a.path.localeCompare(b.path));
1531
+ const { id, who, label, startedAt, lastWriteAt } = group;
1532
+ return { id, who, label, startedAt, endedAt: lastWriteAt ?? this.now(), files };
1533
+ }
1534
+ conflict(entry, changed) {
1535
+ const paths = new Set(changed.map((file) => file.path));
1536
+ const after = this.log.entries.slice(this.log.entries.indexOf(entry) + 1);
1537
+ const newer = after.filter((later) => later.files.some((file) => paths.has(file.path)));
1538
+ return { files: [...paths], newer: newer.map((later) => later.id) };
1539
+ }
1540
+ async restoreNow(point, who, label) {
1541
+ const files = manifestAt(this.log, point);
1542
+ if (!files) throw new Error("That point is no longer kept in this project's history.");
1543
+ const target = new Map(
1544
+ [...this.tracked.keys()].map((path) => [path, null])
1545
+ );
1546
+ for (const [path, hash] of files) target.set(path, hash);
1547
+ return this.writeAs(who, label, target, { restoredTo: point });
1548
+ }
1549
+ /** Settles, then runs `task` with its writes labelled `writeToken`. */
1550
+ operation(writeToken, task) {
1551
+ return this.queue(async () => {
1552
+ await this.settle();
1553
+ this.writeToken = writeToken;
1554
+ try {
1555
+ return await task();
1556
+ } finally {
1557
+ this.writeToken = void 0;
1558
+ }
1559
+ });
1560
+ }
1561
+ pointLabel(point) {
1562
+ return point === START ? "the start" : this.entry(point).label;
1563
+ }
1564
+ api() {
1565
+ return {
1566
+ projectId: this.projectId,
1567
+ beginWindow: (who, label, options = {}) => this.beginWindow(who, label, options.idleMs ?? this.options.maxGroupMs ?? 3e4),
1568
+ claim: async (who, label, paths, options = {}) => {
1569
+ await Promise.all(paths.map((path) => this.adopting.get(this.logPath(path))));
1570
+ return this.queue(() => this.claimNow(who, label, paths, options));
1571
+ },
1572
+ noteChange: (path) => {
1573
+ if (!this.closed) this.noteChange(path);
1574
+ },
1575
+ list: () => {
1576
+ this.assertOpen();
1577
+ const undone = undoneIds(this.log.entries);
1578
+ return this.log.entries.map((entry) => ({
1579
+ ...entry,
1580
+ pinned: this.log.pins.has(entry.id),
1581
+ undone: undone.has(entry.id)
1582
+ }));
1583
+ },
1584
+ step: (direction, who, { writeToken } = {}) => this.operation(writeToken, async () => {
1585
+ const target = this.next(direction, who);
1586
+ return target ? this.undoNow(target.id, who) : { ok: true, entry: null };
1587
+ }),
1588
+ undo: (id, { who, mode, writeToken }) => this.operation(writeToken, () => this.undoNow(id, who, mode)),
1589
+ restore: (point, who, { writeToken } = {}) => this.operation(
1590
+ writeToken,
1591
+ () => this.restoreNow(point, who, `Restored: ${this.pointLabel(point)}`)
1592
+ ),
1593
+ peek: (point) => {
1594
+ this.assertOpen();
1595
+ const files = manifestAt(this.log, point);
1596
+ return files && Object.fromEntries(files);
1597
+ },
1598
+ checkout: (entryId, side, emptyDir) => this.queue(async () => {
1599
+ this.entry(entryId);
1600
+ if (!existsSync2(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
1601
+ const dest = resolve3(emptyDir);
1602
+ if (isWithin(this.dir, dest))
1603
+ throw new Error(`Checkout writes outside the project only: ${dest}`);
1604
+ if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
1605
+ throw new Error(`Checkout writes into an empty folder only: ${dest}`);
1606
+ const files = manifestAround(this.log, entryId, side);
1607
+ try {
1608
+ for (const [path, hash] of files) await this.blobs.writeTo(hash, join5(dest, path));
1609
+ } catch (error) {
1610
+ for (const name of await readdir3(dest).catch(() => []))
1611
+ await rm3(join5(dest, name), { recursive: true, force: true });
1612
+ throw error;
1613
+ }
1614
+ }),
1615
+ next: (direction, who) => {
1616
+ this.assertOpen();
1617
+ return this.next(direction, who);
1618
+ },
1619
+ readBlob: async (hash) => {
1620
+ this.assertOpen();
1621
+ return this.blobs.read(hash);
1622
+ },
1623
+ pin: (id, pinned) => {
1624
+ this.assertOpen();
1625
+ this.entry(id);
1626
+ if (pinned) this.log.pins.add(id);
1627
+ else this.log.pins.delete(id);
1628
+ this.persistLog({ type: "pin", id, pinned });
1629
+ },
1630
+ onEntry: (listener) => {
1631
+ this.listeners.add(listener);
1632
+ return () => this.listeners.delete(listener);
1633
+ },
1634
+ flush: () => this.queue(() => this.settleAll()),
1635
+ replacedAtPath: () => this.whereFolder() === "replaced",
1636
+ close: () => this.closing ??= (async () => {
1637
+ await Promise.all(this.adopting.values());
1638
+ if (this.notedTimer) clearTimeout(this.notedTimer);
1639
+ this.stopHearing?.();
1640
+ const settled = this.queue(() => this.settleAll());
1641
+ this.closed = true;
1642
+ await settled;
1643
+ })()
1644
+ };
1645
+ }
1646
+ };
1647
+ function isWithin(dir, path) {
1648
+ let probe = resolve3(path);
1649
+ while (!existsSync2(probe) && dirname4(probe) !== probe) probe = dirname4(probe);
1650
+ const rel = relative2(realpathSync.native(dir), realpathSync.native(probe));
1651
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
1652
+ }
1653
+ function missingIsEmpty(error) {
1654
+ if (error.code === "ENOENT") return [];
1655
+ throw error;
1656
+ }
1657
+ var HistoryClosedError = class extends Error {
1658
+ constructor(message) {
1659
+ super(message);
1660
+ this.name = "HistoryClosedError";
1661
+ }
1662
+ };
1663
+ async function openProjectHistory(options) {
1664
+ if (!existsSync2(options.projectDir))
1665
+ throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
1666
+ const projectId = projectHistoryId(options.projectDir, options.historyRoot);
1667
+ const release = await takeHistoryOwnership(
1668
+ join5(options.historyRoot, projectId),
1669
+ options.ownerWaitMs ?? 5e3
1670
+ );
1671
+ try {
1672
+ const blobs = await openBlobStore(join5(options.historyRoot, projectId, "blobs"));
1673
+ const engine = new Engine(options, projectId, blobs);
1674
+ await engine.queue(async () => {
1675
+ await engine.start();
1676
+ engine.hearWrites();
1677
+ });
1678
+ pruneGoneProjectHistoriesDaily(options.historyRoot, engine.now(), {
1679
+ onError: options.onError,
1680
+ budgetMs: options.pruneGoneProjectsBudgetMs
1681
+ });
1682
+ const api = engine.api();
1683
+ return {
1684
+ ...api,
1685
+ close: () => api.close().finally(release)
1686
+ };
1687
+ } catch (error) {
1688
+ release();
1689
+ throw error;
1690
+ }
1691
+ }
1692
+ function historyCache(open) {
1693
+ const histories = /* @__PURE__ */ new Map();
1694
+ const get = async (projectDir) => {
1695
+ let cached = histories.get(projectDir);
1696
+ if (!cached) histories.set(projectDir, cached = open(projectDir));
1697
+ const opened = await cached;
1698
+ if (!opened?.replacedAtPath()) return opened;
1699
+ const reopen = () => open(projectDir);
1700
+ if (histories.get(projectDir) === cached)
1701
+ histories.set(projectDir, opened.close().then(reopen, reopen));
1702
+ return get(projectDir);
1703
+ };
1704
+ return {
1705
+ get,
1706
+ /** The history if one was opened, without opening it. */
1707
+ peek: (projectDir) => histories.get(projectDir),
1708
+ forget: (projectDir) => histories.delete(projectDir),
1709
+ closeAll: () => Promise.all(
1710
+ [...histories.values()].map((opened) => opened.then((history) => history?.close()))
1711
+ )
1712
+ };
1713
+ }
1714
+
1715
+ export {
1716
+ fileContentVersion,
1717
+ DELETED_VERSION,
1718
+ settledFileTag,
1719
+ createWriteToken,
1720
+ recordFileWriteReceipt,
1721
+ clearFileWriteReceipt,
1722
+ identifyFileWrite,
1723
+ consumeFileWriteReceipt,
1724
+ STUDIO_SIGNATURE_MANIFEST_PATHS,
1725
+ affectsProjectSignature,
1726
+ resolveProjectSignature,
1727
+ resolveProjectAndSignature,
1728
+ createProjectSignature,
1729
+ HistoryIdError,
1730
+ HistoryBusyError,
1731
+ listProjectHistories,
1732
+ pruneGoneProjectHistories,
1733
+ START,
1734
+ DEFAULT_HISTORY_ROOT,
1735
+ MAX_WINDOW_IDLE_MS,
1736
+ UNDO_MODES,
1737
+ HistoryClosedError,
1738
+ openProjectHistory,
1739
+ historyCache
1740
+ };